chore: v2.8.7 — Git SSH keys in tabs, safer Artifacts preview and search

- Settings → Git → Your git identity: SSH keys are one tab per host kind
  (GitHub · GitLab, AWS CodeCommit, More keys, Other hosts); opens on the
  AWS tab when only an RSA key exists.
- Artifacts: a file type the sandboxed preview cannot show (Office, zip,
  PDF) offers Download only, never a blank Preview.
- Artifacts search: `%`/`_`/`\` in a query match literally (LIKE ... ESCAPE).
- Skill frontmatter rewrite replaces a key with its continuation lines and
  quotes values YAML could misread.
- Plugin marketplace.json is read with a 1 MB streaming cap.
- Artifact lists look each owner up once.
This commit is contained in:
2026-10-15 17:17:47 +07:00
parent 4d6067650a
commit a58e95a957
16 changed files with 763 additions and 380 deletions
+168 -47
View File
@@ -42,19 +42,21 @@ function request(method, p, body, headers = {}) {
method,
headers: { "Content-Type": "application/json", ...headers },
body: body === undefined ? undefined : JSON.stringify(body),
}).then(async (r) => ({
status: r.status,
headers: r.headers,
text: await r.text(),
})).then((r) => {
let json = null;
try {
json = JSON.parse(r.text);
} catch {
/* not JSON */
}
return { ...r, body: json };
});
})
.then(async (r) => ({
status: r.status,
headers: r.headers,
text: await r.text(),
}))
.then((r) => {
let json = null;
try {
json = JSON.parse(r.text);
} catch {
/* not JSON */
}
return { ...r, body: json };
});
}
before(async () => {
@@ -63,18 +65,23 @@ before(async () => {
fs.writeFileSync(path.join(ws, "main.js"), "code");
const skill = path.join(ws, ".claude", "skills", "deploy");
fs.mkdirSync(path.join(skill, "scripts"), { recursive: true });
fs.writeFileSync(path.join(skill, "SKILL.md"), "---\nname: deploy\ndescription: Ship it\n---\nDo it.\n");
fs.writeFileSync(
path.join(skill, "SKILL.md"),
"---\nname: deploy\ndescription: Ship it\n---\nDo it.\n"
);
fs.writeFileSync(path.join(skill, "scripts", "run.sh"), "echo hi\n");
fs.mkdirSync(process.env.CLAUDE_HOME, { recursive: true });
lane = lanesLib.createLane({ cwd: ws, title: "ws" });
db.prepare("INSERT INTO sessions (id, name, status, cwd, lane_id) VALUES ('s1', 's1', 'active', ?, ?)").run(
ws,
lane.id
);
db.prepare(
"INSERT INTO sessions (id, name, status, cwd, lane_id) VALUES ('s1', 's1', 'active', ?, ?)"
).run(ws, lane.id);
const ev = db.prepare(
"INSERT INTO events (session_id, event_type, tool_name, data) VALUES ('s1', 'PostToolUse', ?, ?)"
);
ev.run("Write", JSON.stringify({ tool_input: { file_path: path.join(ws, "out", "report.html") } }));
ev.run(
"Write",
JSON.stringify({ tool_input: { file_path: path.join(ws, "out", "report.html") } })
);
ev.run("Edit", JSON.stringify({ tool_input: { file_path: path.join(ws, "main.js") } }));
ev.run("Write", JSON.stringify({ tool_input: { file_path: "/etc/passwd.md" } }));
server = http.createServer(createApp());
@@ -125,7 +132,10 @@ describe("files", () => {
});
it("pins a snapshot and serves it sandboxed, never a path outside it", async () => {
const r = await request("POST", "/api/artifacts/file", { laneId: lane.id, path: "out/report.html" });
const r = await request("POST", "/api/artifacts/file", {
laneId: lane.id,
path: "out/report.html",
});
assert.equal(r.status, 201, r.text);
const id = r.body.item.id;
// The live file changes; the artifact does not.
@@ -138,7 +148,10 @@ describe("files", () => {
const other = await request("GET", `/api/artifacts/${id}/raw?path=../../t.db`);
assert.equal(other.status, 404);
const escape = await request("POST", "/api/artifacts/file", { laneId: lane.id, path: "../t.db" });
const escape = await request("POST", "/api/artifacts/file", {
laneId: lane.id,
path: "../t.db",
});
assert.equal(escape.status, 400);
const del = await request("DELETE", `/api/artifacts/${id}`);
@@ -154,10 +167,7 @@ describe("skills", () => {
const r = await request("POST", "/api/artifacts/skill", { laneId: lane.id, name: "deploy" });
assert.equal(r.status, 201, r.text);
assert.equal(r.body.item.description, "Ship it");
assert.deepEqual(
r.body.item.files.map((f) => f.path).sort(),
["SKILL.md", "scripts/run.sh"]
);
assert.deepEqual(r.body.item.files.map((f) => f.path).sort(), ["SKILL.md", "scripts/run.sh"]);
const id = r.body.item.id;
const first = await request("POST", `/api/artifacts/${id}/install`, {});
@@ -240,7 +250,12 @@ function upload(p, files, fields = {}) {
const snapText = (item, file = "SKILL.md") =>
fs.readFileSync(
path.join(process.env.DASHBOARD_DATA_DIR, "artifacts", db.prepare("SELECT snapshot FROM artifacts WHERE id = ?").get(item.id).snapshot, file),
path.join(
process.env.DASHBOARD_DATA_DIR,
"artifacts",
db.prepare("SELECT snapshot FROM artifacts WHERE id = ?").get(item.id).snapshot,
file
),
"utf8"
);
@@ -263,7 +278,10 @@ describe("plugins from a marketplace link", () => {
n("https://git.example.vn/Org/skills/src/branch/main/"),
"https://git.example.vn/Org/skills"
);
assert.equal(n("https://git.example.vn/Org/skills.git"), "https://git.example.vn/Org/skills.git");
assert.equal(
n("https://git.example.vn/Org/skills.git"),
"https://git.example.vn/Org/skills.git"
);
assert.throws(() => n("file:///etc"), { code: "EBADMARKET" });
assert.throws(() => n("http://x/y.git"), { code: "EBADMARKET" });
});
@@ -281,7 +299,10 @@ describe("plugins from a marketplace link", () => {
assert.equal(r.status, 200, r.text);
assert.equal(asked, "https://git.example.vn/Org/skills.git");
assert.equal(r.body.name, "sg");
assert.deepEqual(r.body.plugins.map((p) => p.key), ["sg-dev@sg", "sg-common@sg"]);
assert.deepEqual(
r.body.plugins.map((p) => p.key),
["sg-dev@sg", "sg-common@sg"]
);
const add = (title) =>
request("POST", "/api/artifacts/plugin", { key: "sg-dev@sg", market: r.body.market, title });
@@ -312,14 +333,21 @@ describe("plugins from a marketplace link", () => {
describe("skills from files or written", () => {
it("makes one Markdown file the SKILL.md, named by the user, with a frontmatter", async () => {
const r = await upload("/api/artifacts/skill/upload", [["Notes cho đội.md", "# Do this\nStep 1\n"]], {
name: "Hướng dẫn deploy",
description: "Use when deploying",
});
const r = await upload(
"/api/artifacts/skill/upload",
[["Notes cho đội.md", "# Do this\nStep 1\n"]],
{
name: "Hướng dẫn deploy",
description: "Use when deploying",
}
);
assert.equal(r.status, 201, JSON.stringify(r.body));
const [item] = r.body.items;
assert.equal(item.source, "Huong-dan-deploy");
assert.deepEqual(item.files.map((f) => f.path), ["SKILL.md"]);
assert.deepEqual(
item.files.map((f) => f.path),
["SKILL.md"]
);
const text = snapText(item);
assert.match(text, /^---\nname: Huong-dan-deploy\ndescription: Use when deploying\n---\n/);
assert.match(text, /# Do this/);
@@ -364,15 +392,23 @@ describe("skills from files or written", () => {
["b/SKILL.md", "no frontmatter"],
]);
assert.equal(r.body.error.code, "ENODESCRIPTION");
assert.equal(db.prepare("SELECT COUNT(*) AS n FROM artifacts WHERE kind = 'skill'").get().n, before);
const secret = await upload(
"/api/artifacts/skill/upload",
[["s/SKILL.md", "---\ndescription: S\n---\n"], ["s/.env", "K=1"]]
assert.equal(
db.prepare("SELECT COUNT(*) AS n FROM artifacts WHERE kind = 'skill'").get().n,
before
);
const secret = await upload("/api/artifacts/skill/upload", [
["s/SKILL.md", "---\ndescription: S\n---\n"],
["s/.env", "K=1"],
]);
assert.equal(secret.body.error.code, "ESECRET");
const out = await upload("/api/artifacts/skill/upload", [["../SKILL.md", "x"]], { description: "d" });
const out = await upload("/api/artifacts/skill/upload", [["../SKILL.md", "x"]], {
description: "d",
});
assert.equal(out.body.error.code, "EBADPATH");
const none = await upload("/api/artifacts/skill/upload", [["a.png", "x"], ["b.png", "y"]]);
const none = await upload("/api/artifacts/skill/upload", [
["a.png", "x"],
["b.png", "y"],
]);
assert.equal(none.body.error.code, "ENOSKILLMD");
});
@@ -383,13 +419,22 @@ describe("skills from files or written", () => {
content: "Summarise.",
});
assert.equal(r.status, 201, r.text);
assert.match(snapText(r.body.item), /^---\nname: standup\ndescription: "Use for the daily standup: summarise yesterday"\n---\n\nSummarise\./);
const bad = await request("POST", "/api/artifacts/skill/new", { name: "!!!", description: "d", content: "c" });
assert.match(
snapText(r.body.item),
/^---\nname: standup\ndescription: "Use for the daily standup: summarise yesterday"\n---\n\nSummarise\./
);
const bad = await request("POST", "/api/artifacts/skill/new", {
name: "!!!",
description: "d",
content: "c",
});
assert.equal(bad.body.error.code, "EBADNAME");
});
it("keeps an uploaded file as a snapshot, by its own base name", async () => {
const r = await upload("/api/artifacts/file/upload", [["report.html", "<h1>x</h1>"]], { title: "Report" });
const r = await upload("/api/artifacts/file/upload", [["report.html", "<h1>x</h1>"]], {
title: "Report",
});
assert.equal(r.status, 201, JSON.stringify(r.body));
assert.equal(r.body.item.kind, "file");
assert.equal(r.body.item.title, "Report");
@@ -408,13 +453,25 @@ describe("sharing and visibility", () => {
});
it("shows a server-mode developer their own rows and shared ones only", () => {
db.prepare("INSERT INTO artifacts (kind, title, owner_id, url) VALUES ('page', 'a', 7, 'https://claude.ai/a')").run();
db.prepare("INSERT INTO artifacts (kind, title, owner_id, url, shared) VALUES ('page', 'b', 8, 'https://claude.ai/b', 1)").run();
db.prepare("INSERT INTO artifacts (kind, title, owner_id, url) VALUES ('page', 'c', 8, 'https://claude.ai/c')").run();
db.prepare(
"INSERT INTO artifacts (kind, title, owner_id, url) VALUES ('page', 'a', 7, 'https://claude.ai/a')"
).run();
db.prepare(
"INSERT INTO artifacts (kind, title, owner_id, url, shared) VALUES ('page', 'b', 8, 'https://claude.ai/b', 1)"
).run();
db.prepare(
"INSERT INTO artifacts (kind, title, owner_id, url) VALUES ('page', 'c', 8, 'https://claude.ai/c')"
).run();
const dev = { method: "GET", user: { id: 7, role: "developer", username: "d" } };
const titles = artifacts.list(dev).map((i) => i.title).sort();
const titles = artifacts
.list(dev)
.map((i) => i.title)
.sort();
assert.deepEqual(titles, ["a", "b"]);
assert.deepEqual(artifacts.list(dev, { scope: "hub" }).map((i) => i.title), ["b"]);
assert.deepEqual(
artifacts.list(dev, { scope: "hub" }).map((i) => i.title),
["b"]
);
const hidden = db.prepare("SELECT id FROM artifacts WHERE title = 'c'").get().id;
assert.equal(artifacts.canSeeId(dev, hidden), false);
const shared = db.prepare("SELECT id FROM artifacts WHERE title = 'b'").get().id;
@@ -431,3 +488,67 @@ describe("sharing and visibility", () => {
assert.equal(r.status, 403);
});
});
describe("search, frontmatter and marketplace limits", () => {
it("finds a title holding `_` or `%` literally, and only that one", () => {
db.prepare(
"INSERT INTO artifacts (kind, title, url) VALUES ('page', 'my_skill', 'https://claude.ai/u1')"
).run();
db.prepare(
"INSERT INTO artifacts (kind, title, url) VALUES ('page', 'myXskill', 'https://claude.ai/u2')"
).run();
db.prepare(
"INSERT INTO artifacts (kind, title, url) VALUES ('page', '100% done', 'https://claude.ai/u3')"
).run();
assert.deepEqual(
artifacts.list(null, { q: "my_skill" }).map((i) => i.title),
["my_skill"]
);
assert.deepEqual(
artifacts.list(null, { q: "100%" }).map((i) => i.title),
["100% done"]
);
});
it("replaces a multi-line description whole, and quotes one YAML would misread", () => {
const src =
"---\nname: x\ndescription: >\n old line one\n old line two\nallowed-tools: Read\n---\nbody\n";
assert.equal(
artifacts.withFrontmatter(src, "x", "new desc"),
"---\nname: x\ndescription: new desc\nallowed-tools: Read\n---\nbody\n"
);
assert.match(
artifacts.withFrontmatter("body", "x", "- Use for reports"),
/^description: "- Use for reports"$/m
);
assert.match(
artifacts.withFrontmatter("body", "x", "Use for reports"),
/^description: Use for reports$/m
);
});
it("stops reading a marketplace.json link past 1 MB", async () => {
const real = globalThis.fetch;
let pulled = 0;
globalThis.fetch = async () => {
const body = new ReadableStream({
pull(c) {
pulled += 1;
if (pulled > 100) return c.close();
c.enqueue(new Uint8Array(64 * 1024));
},
});
const res = new Response(body);
Object.defineProperty(res, "url", { value: "https://x.example/m.json" });
return res;
};
try {
await assert.rejects(artifacts.inspectMarket("https://x.example/m.json"), {
code: "ETOOBIG",
});
} finally {
globalThis.fetch = real;
}
assert.ok(pulled < 30, `read ${pulled} chunks`);
});
});