feat(authz): add atomic mobile knowledge writes

This commit is contained in:
冰朔 2026-08-03 22:27:09 +08:00
commit 5e2ba5b006
6 changed files with 527 additions and 19 deletions

View file

@ -54,6 +54,93 @@ test("knowledge provider exposes only the fixed main commit and archive", () =>
);
});
test("knowledge provider atomically writes Markdown and returns a verifiable commit receipt", () => {
const repository = createBareKnowledgeRepository();
const provider = new HoloLakeKnowledgeProvider({
repositoryId: "bingshuo/hololake-knowledge-base",
repositoryPath: repository.bare,
});
const base = provider.manifest().commit;
const content = "# 手机知识页\n\n由 HoloLake Agent 写入。\n";
const receipt = provider.writePage({
path: "个人知识/手机知识页.md",
content,
baseCommit: base,
});
assert.equal(receipt.schema, "guanghu.hololake-knowledge-write-receipt/v1");
assert.equal(receipt.repository, "bingshuo/hololake-knowledge-base");
assert.equal(receipt.path, "个人知识/手机知识页.md");
assert.equal(receipt.base_commit, base);
assert.match(receipt.commit, /^[0-9a-f]{40}$/);
assert.notEqual(receipt.commit, base);
assert.equal(
receipt.content_sha256,
require("node:crypto").createHash("sha256").update(content).digest("hex"),
);
assert.equal(provider.manifest().commit, receipt.commit);
assert.equal(
childProcess.execFileSync(
"git",
[`--git-dir=${repository.bare}`, "show", `${receipt.commit}:个人知识/手机知识页.md`],
{ encoding: "utf8" },
),
content,
);
assert.doesNotMatch(JSON.stringify(receipt), /由 HoloLake Agent 写入/);
});
test("knowledge provider rejects secret pages, unsafe paths, oversized content, and stale commits", () => {
const repository = createBareKnowledgeRepository();
const provider = new HoloLakeKnowledgeProvider({
repositoryId: "bingshuo/hololake-knowledge-base",
repositoryPath: repository.bare,
});
const base = provider.manifest().commit;
for (const invalidPath of [
"本地密钥/openai.md",
"../outside.md",
"notes/plain.txt",
"/absolute.md",
"notes//empty.md",
]) {
assert.throws(
() => provider.writePage({
path: invalidPath,
content: "# blocked\n",
baseCommit: base,
}),
/knowledge_page_path_invalid|knowledge_secret_page_forbidden/,
);
}
assert.throws(
() => provider.writePage({
path: "notes/large.md",
content: "x".repeat(1024 * 1024 + 1),
baseCommit: base,
}),
/knowledge_page_too_large/,
);
const first = provider.writePage({
path: "notes/first.md",
content: "# first\n",
baseCommit: base,
});
assert.match(first.commit, /^[0-9a-f]{40}$/);
assert.throws(
() => provider.writePage({
path: "notes/stale.md",
content: "# stale\n",
baseCommit: base,
}),
/knowledge_commit_conflict/,
);
assert.equal(provider.manifest().commit, first.commit);
});
test("AI gateway accepts only registered provider models and never leaks the key", async () => {
const requests = [];
const gateway = new HoloLakeAiGateway({
@ -115,6 +202,69 @@ test("AI gateway accepts only registered provider models and never leaks the key
assert.equal(requests.length, 1);
});
test("AI gateway forwards bounded tool contracts and tool receipts without exposing server secrets", async () => {
const requests = [];
const gateway = new HoloLakeAiGateway({
providers: {
default: {
baseUrl: "https://models.example.invalid/v1",
apiKey: "server-secret-key",
models: ["gpt-test"],
},
},
fetchImpl: async (_url, options) => {
requests.push(JSON.parse(options.body));
return new Response(JSON.stringify({
choices: [{ message: { role: "assistant", content: "saved" } }],
}), { status: 200 });
},
});
const result = await gateway.execute({
provider: "default",
model: "gpt-test",
messages: [
{ role: "user", content: "save this" },
{
role: "assistant",
content: null,
tool_calls: [{
id: "call-1",
type: "function",
function: { name: "create_note", arguments: "{\"path\":\"notes/a.md\"}" },
}],
},
{ role: "tool", tool_call_id: "call-1", content: "commit=abc" },
],
tools: [{
type: "function",
function: {
name: "create_note",
description: "Create one Markdown page.",
parameters: {
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
},
},
}],
tool_choice: "auto",
});
assert.equal(result.response.choices[0].message.content, "saved");
assert.equal(requests[0].tools[0].function.name, "create_note");
assert.equal(requests[0].messages[2].role, "tool");
assert.equal(requests[0].tool_choice, "auto");
assert.doesNotMatch(JSON.stringify(result), /server-secret-key/);
await assert.rejects(
() => gateway.execute({
provider: "default",
model: "gpt-test",
messages: [{ role: "tool", tool_call_id: "../bad", content: "x" }],
}),
/ai_message_invalid/,
);
});
test("AI gateway bounds message shape, count, text, and requested output", async () => {
const gateway = new HoloLakeAiGateway({
providers: {