chore: v2.8.15 — Oh My Pi login sharing, omp --allow-home, Markdown Preview default, first-login password

- omp-share.js: an admin's switch (S3) runs omp's own auth broker + gateway as
  the host on loopback; a member's omp pane reaches it as a scoped client keyed
  by its pane token (clients file = live omp panes only), runs on
  --model agenthub/<model>, never holds a credential.
- omp catalog: --allow-home, so a workspace at the account's home keeps its
  sessions there (they were recorded under a temp dir and dropped).
- Settings → Appearance → Open Markdown as: Preview (default) or Raw, for
  Files/Changes, the config viewer and a chat's file panel.
- First-login password change and password rules, avatar URLs, Artifacts
  "Mine" includes installed shared skills/plugins, a member's own hook pages.
This commit is contained in:
2026-10-22 10:04:30 +07:00
parent 8a6dc6a29e
commit 750d74a2af
66 changed files with 3021 additions and 696 deletions
+60
View File
@@ -119,6 +119,34 @@ describe("pages", () => {
assert.equal(artifacts.captureFromHook("s1", { tool_input: { action: "read" } }), null);
assert.equal(artifacts.captureFromHook("s1", { tool_response: "no link" }), null);
});
it("gives a page from a session no workspace owns to the member who sent the hook", () => {
const uid = db
.prepare("INSERT INTO users (username, password_hash) VALUES ('ann', 'x')")
.run().lastInsertRowid;
const hook = (url) => ({
tool_input: { action: "publish" },
tool_response: `Published: ${url}`,
});
const own = artifacts.captureFromHook("loose-1", hook("https://claude.ai/artifact/m1"), {
kind: "member",
name: "ann",
});
assert.equal(own.owner_id, Number(uid));
assert.equal(own.lane_id, null);
// The host account names nobody; a workspace's owner still wins over the peer.
const host = artifacts.captureFromHook("loose-2", hook("https://claude.ai/artifact/m2"), {
kind: "host",
});
assert.equal(host.owner_id, null);
const ws = artifacts.captureFromHook("s1", hook("https://claude.ai/artifact/m3"), {
kind: "member",
name: "ann",
});
const laneOwner = db.prepare("SELECT owner_id FROM lanes WHERE id = ?").get(lane.id).owner_id;
assert.equal(ws.owner_id, laneOwner);
assert.equal(ws.lane_id, lane.id);
});
});
describe("files", () => {
@@ -145,6 +173,12 @@ describe("files", () => {
assert.match(raw.text, /<h1>hi<\/h1>/);
assert.match(raw.headers.get("content-security-policy"), /sandbox/);
assert.equal(raw.headers.get("x-content-type-options"), "nosniff");
// Deleting the source on disk takes nothing away: only a delete in AgentHub does.
const live = path.join(ws, "out", "report.html");
fs.rmSync(live);
assert.equal((await request("GET", `/api/artifacts/${id}`)).status, 200);
assert.match((await request("GET", `/api/artifacts/${id}/raw`)).text, /<h1>hi<\/h1>/);
fs.writeFileSync(live, "changed");
const other = await request("GET", `/api/artifacts/${id}/raw?path=../../t.db`);
assert.equal(other.status, 404);
@@ -160,6 +194,32 @@ describe("files", () => {
});
});
describe("my library", () => {
it("adds the shared skills of others the caller installed, and only those", () => {
const ins = db.prepare(
"INSERT INTO artifacts (kind, title, owner_id, source, shared) VALUES ('skill', ?, ?, ?, ?)"
);
const theirs = Number(ins.run("theirs", 77, "lib-theirs", 1).lastInsertRowid);
const notInstalled = Number(ins.run("other", 77, "lib-other", 1).lastInsertRowid);
const unshared = Number(ins.run("private", 77, "lib-private", 0).lastInsertRowid);
const own = Number(ins.run("own", 5, "lib-own", 0).lastInsertRowid);
for (const name of ["lib-theirs", "lib-private"])
fs.mkdirSync(path.join(process.env.CLAUDE_HOME, "skills", name), { recursive: true });
const req = { user: { id: 5, role: "admin" } };
const ids = artifacts.list(req, { scope: "mine", kind: "skill" }).map((a) => [a.id, a.mine]);
assert.deepEqual(
ids.filter(([id]) => [theirs, notInstalled, unshared, own].includes(id)).sort(),
[
[theirs, false],
[own, true],
].sort()
);
db.prepare(
`DELETE FROM artifacts WHERE id IN (${[theirs, notInstalled, unshared, own].join(",")})`
).run();
});
});
describe("skills", () => {
it("snapshots a workspace skill and installs it into ~/.claude/skills and ~/.agents/skills", async () => {
const list = await request("GET", `/api/artifacts/skills?laneId=${lane.id}`);