mirror of
https://github.com/volcengine/OpenViking.git
synced 2026-09-30 09:17:51 +08:00
* feat(uri)!: reject uid-less current-user shorthand in favor of viking://~ viking://user/<segment> (memories/resources/skills/peers/privacy/sessions without a user id) was ambiguous with a user literally named after the segment, and a user actually named e.g. "memories" was unreachable for USER/ADMIN callers. Now that the viking://~ home alias (#4167) covers the same need unambiguously, the shorthand fails closed at the request boundary instead of expanding: - resolve_current_user_uri raises NamespaceShapeError with a corrective hint naming both viking://~/<rest> and the explicit-uid form. Silently parsing the reserved segment as a peer user id would misdirect reads and writes, so rejection is the only safe removal. - Bare viking://user falls through to the canonical parser and keeps container semantics (a user key listing it sees only its own space). - The self-id escape stays: a caller whose user_id equals a reserved name keeps viking://user/<own-id> as their canonical root. ROOT-role literal parsing and the legacy viking://session alias are unchanged. - AddTargetsConfig normalizes stored legacy config spellings (viking://user/resources|skills) to the viking://~ form at validation so existing ov.conf/user_config deployments keep working; the accepted per-user spelling is now viking://~/resources and viking://~/skills. - usage_reporter keeps canonicalizing the historical shorthand found in old transcripts and additionally recognizes viking://~/memories/. BREAKING CHANGE: requests using the uid-less viking://user/<segment> spelling now fail with 400; use viking://~/<segment> or an explicit viking://user/{user_id}/<segment> URI. * refactor(clients): migrate first-party emitters to the viking://~ home alias Every in-repo client that emitted the removed uid-less current-user shorthand now sends viking://~/... instead: vikingbot fallbacks and default sentinels, the LangChain store/tools defaults, the shared recall-core.mjs (all synced plugin copies), the codex/claude-code/ openclaw/openwebui/dsh/zcode/pi plugin emitters, quick-app examples, Go SDK example, tau2 benchmark targets, and the eval golden dataset. Compat kept where legacy strings live in stored user configs: bot and ov_dream sentinels accept both spellings while emitting only ~, and recall-core still rewrites legacy viking://user/<reserved> config values client-side. langchain_openviking._uri now classifies viking://~ with the explicit-user shape so canonicalized server responses keep matching a ~ root. Plugin READMEs note the server requirement for the alias. * docs: replace current-user shorthand guidance with the viking://~ home alias Rewrite every EN/ZH doc and model-facing prompt that advertised the uid-less viking://user/<segment> spelling: URI concept catalogue, context-types/storage/extraction/retrieval/session/privacy concepts, configuration guide (with the legacy add_targets auto-normalization note), resources/skills/sessions/retrieval/admin API references, FAQ, capability reference, and the openviking-memory / ov-experience-memory / openclaw / ov-resources skills. The stale MCP viking://user/<path> dialect passage in the MCP guide is replaced by ~ guidance, and bare viking://user is documented as the container of user spaces. * test(api): migrate live API session-used tests off the removed shorthand tests/api_test/sessions sent uid-less viking://user/skills/... URIs to record_used, which the request boundary now rejects with 400 (caught by the API & CLI Integration Tests CI job; these tests need a live server and are not part of the local suites). The api_test client authenticates as an admin-role user key, so the viking://~ home alias expands for it. tests/api_test/common/test_edge_cases.py is left as is: it asserts a 400 for a non-resource add target, which still holds.
120 lines
4.5 KiB
JavaScript
120 lines
4.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import test from "node:test";
|
|
import { evaluateZcodeUriGuard } from "./uri-guard.mjs";
|
|
|
|
const PLUGIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// URI Guard output schema (the #1 silent-failure mode in the adversarial review)
|
|
// ZCode's strict JSON schema rejects any unrecognized key. These tests assert
|
|
// the output contains ONLY ZCode-recognized keys.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("uri guard deny output contains only recognized keys", () => {
|
|
const output = evaluateZcodeUriGuard({
|
|
tool_name: "Read",
|
|
tool_input: { file_path: "viking://~/memories/profile.md" },
|
|
});
|
|
assert.ok(output.hookSpecificOutput);
|
|
assert.equal(output.hookSpecificOutput.hookEventName, "PreToolUse");
|
|
assert.equal(output.hookSpecificOutput.permissionDecision, "deny");
|
|
assert.ok(output.hookSpecificOutput.permissionDecisionReason);
|
|
// Must NOT contain Claude-Code-isms
|
|
assert.equal(output.decision, undefined);
|
|
assert.equal(output.hookSpecificOutput.decision, undefined);
|
|
});
|
|
|
|
test("uri guard deny reason includes MCP redirect", () => {
|
|
const output = evaluateZcodeUriGuard({
|
|
tool_name: "Read",
|
|
tool_input: { file_path: "viking://resources/myproject/docs.md" },
|
|
});
|
|
const reason = output.hookSpecificOutput?.permissionDecisionReason || "";
|
|
assert.ok(reason.includes("MCP"), "reason should mention MCP");
|
|
});
|
|
|
|
test("uri guard pass-through for non-viking URIs returns empty", () => {
|
|
const output = evaluateZcodeUriGuard({
|
|
tool_name: "Read",
|
|
tool_input: { file_path: "/tmp/local-file.txt" },
|
|
});
|
|
assert.equal(Object.keys(output).length, 0);
|
|
});
|
|
|
|
test("uri guard pass-through for Glob with local pattern", () => {
|
|
const output = evaluateZcodeUriGuard({
|
|
tool_name: "Glob",
|
|
tool_input: { pattern: "**/*.ts" },
|
|
});
|
|
assert.equal(Object.keys(output).length, 0);
|
|
});
|
|
|
|
test("uri guard deny for Glob with viking URI", () => {
|
|
const output = evaluateZcodeUriGuard({
|
|
tool_name: "Glob",
|
|
tool_input: { pattern: "viking://user/memories/**" },
|
|
});
|
|
assert.equal(output.hookSpecificOutput.permissionDecision, "deny");
|
|
});
|
|
|
|
test("uri guard handles alternative tool_input field names", () => {
|
|
const output = evaluateZcodeUriGuard({
|
|
tool_name: "Read",
|
|
toolInput: { file_path: "viking://user/test.md" },
|
|
});
|
|
assert.equal(output.hookSpecificOutput?.permissionDecision, "deny");
|
|
});
|
|
|
|
test("uri guard handles alternative tool_name field names", () => {
|
|
const output = evaluateZcodeUriGuard({
|
|
toolName: "Read",
|
|
tool_input: { file_path: "viking://user/test.md" },
|
|
});
|
|
assert.equal(output.hookSpecificOutput?.permissionDecision, "deny");
|
|
});
|
|
|
|
test("uri guard returns empty for unmatched tool name", () => {
|
|
const output = evaluateZcodeUriGuard({
|
|
tool_name: "Bash",
|
|
tool_input: { command: "cat viking://user/test.md" },
|
|
});
|
|
// Bash is not in the Read|Glob|Grep matcher — but evaluateAgentUriGuard
|
|
// may still detect viking:// in certain fields. The important assertion is
|
|
// that the output shape is valid (either empty or correct deny).
|
|
if (Object.keys(output).length > 0) {
|
|
assert.equal(output.hookSpecificOutput.hookEventName, "PreToolUse");
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Source-file conventions: hooks.json and .mcp.json must use ZCODE_PLUGIN_ROOT
|
|
// (not CLAUDE_PLUGIN_ROOT) in a .zcode-plugin/ plugin for naming consistency.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("hooks.json uses ZCODE_PLUGIN_ROOT, not CLAUDE_PLUGIN_ROOT", () => {
|
|
const hooksJson = readFileSync(join(PLUGIN_ROOT, "hooks", "hooks.json"), "utf8");
|
|
assert.ok(
|
|
!hooksJson.includes("CLAUDE_PLUGIN_ROOT"),
|
|
"hooks.json should use ${ZCODE_PLUGIN_ROOT}, not ${CLAUDE_PLUGIN_ROOT}",
|
|
);
|
|
assert.ok(
|
|
hooksJson.includes("ZCODE_PLUGIN_ROOT"),
|
|
"hooks.json should reference ${ZCODE_PLUGIN_ROOT}",
|
|
);
|
|
});
|
|
|
|
test(".mcp.json uses ZCODE_PLUGIN_ROOT, not CLAUDE_PLUGIN_ROOT", () => {
|
|
const mcpJson = readFileSync(join(PLUGIN_ROOT, ".mcp.json"), "utf8");
|
|
assert.ok(
|
|
!mcpJson.includes("CLAUDE_PLUGIN_ROOT"),
|
|
".mcp.json should use ${ZCODE_PLUGIN_ROOT}, not ${CLAUDE_PLUGIN_ROOT}",
|
|
);
|
|
assert.ok(
|
|
mcpJson.includes("ZCODE_PLUGIN_ROOT"),
|
|
".mcp.json should reference ${ZCODE_PLUGIN_ROOT}",
|
|
);
|
|
});
|