Files
NetMesh/electron/capabilities/policy.test.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

492 lines
15 KiB
JavaScript

"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const {
BUILTIN_WRITE_RPC_METHODS,
BUILTIN_APPROVAL_RPC_METHODS,
PUBLIC_CONFIRM_RPC_METHODS,
evaluateRpcPermission,
evaluatePermissionWithGrants,
OBSERVER_DENY_MESSAGE,
} = require("./policy.cjs");
const { CAPABILITY_SURFACES, PERMISSION_MODES } = require("./constants.cjs");
const { ALL_CAPABILITIES } = require("./catalog/index.cjs");
test("new vault management writes use the standard permission policy", () => {
const ids = [
"portforward.rules.create", "portforward.rules.update", "portforward.rules.duplicate", "portforward.rules.delete",
"vault.note.delete", "vault.group.create", "vault.group.update", "vault.group.delete",
];
for (const id of ids) {
const capability = ALL_CAPABILITIES.find((entry) => entry.id === id);
assert.ok(capability, id);
assert.equal(capability.policy.write, true, id);
assert.equal(capability.policy.bypassesObserverBlock, false, id);
assert.equal(capability.policy.bypassesApproval, false, id);
}
});
test("builtin write methods match legacy mcpServerBridge write set", () => {
const legacyWriteMethods = [
"netcatty/exec",
"netcatty/sftp/write",
"netcatty/sftp/download",
"netcatty/sftp/upload",
"netcatty/sftp/mkdir",
"netcatty/sftp/delete",
"netcatty/sftp/rename",
"netcatty/sftp/chmod",
"netcatty/jobStart",
"netcatty/jobStop",
];
assert.deepEqual(new Set(legacyWriteMethods), BUILTIN_WRITE_RPC_METHODS);
});
test("builtin approval methods exclude jobStop and non-write control rpc", () => {
assert.equal(BUILTIN_APPROVAL_RPC_METHODS.has("netcatty/jobStop"), false);
assert.equal(BUILTIN_APPROVAL_RPC_METHODS.has("netcatty/setCancelled"), false);
assert.equal(BUILTIN_APPROVAL_RPC_METHODS.has("netcatty/exec"), true);
assert.equal(BUILTIN_APPROVAL_RPC_METHODS.has("netcatty/sftp/write"), true);
});
test("observer mode blocks writes but allows terminal poll", () => {
const denied = evaluateRpcPermission({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.OBSERVER,
params: { chatSessionId: "chat-1" },
});
assert.equal(denied.allowed, false);
assert.match(denied.error, /observer/i);
const allowed = evaluateRpcPermission({
rpcMethod: "netcatty/jobPoll",
permissionMode: PERMISSION_MODES.OBSERVER,
params: { chatSessionId: "chat-1" },
});
assert.equal(allowed.allowed, true);
assert.equal(allowed.requiresApproval, false);
});
test("confirm mode requires approval for writes but not sftp list on builtin surface", () => {
const writeDecision = evaluateRpcPermission({
rpcMethod: "netcatty/sftp/write",
permissionMode: PERMISSION_MODES.CONFIRM,
params: { chatSessionId: "chat-1" },
});
assert.equal(writeDecision.allowed, true);
assert.equal(writeDecision.requiresApproval, true);
const readDecision = evaluateRpcPermission({
rpcMethod: "netcatty/sftp/list",
permissionMode: PERMISSION_MODES.CONFIRM,
params: { chatSessionId: "chat-1" },
});
assert.equal(readDecision.allowed, true);
assert.equal(readDecision.requiresApproval, false);
});
test("public surface treats sensitive reads as confirm-gated", () => {
const decision = evaluateRpcPermission({
rpcMethod: "public/sftp/list",
surface: CAPABILITY_SURFACES.PUBLIC,
permissionMode: PERMISSION_MODES.CONFIRM,
params: { sessionId: "sess-1" },
});
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, true);
assert.equal(PUBLIC_CONFIRM_RPC_METHODS.has("public/sftp/list"), true);
});
test("write operations require chatSessionId on builtin surface", () => {
const decision = evaluateRpcPermission({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.AUTO,
params: {},
});
assert.equal(decision.allowed, false);
assert.match(decision.error, /chatSessionId/i);
});
test("cancelled chat sessions block terminal writes", () => {
const decision = evaluateRpcPermission({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.AUTO,
params: { chatSessionId: "chat-1" },
context: { chatSessionCancelled: true },
});
assert.equal(decision.allowed, false);
assert.match(decision.error, /cancelled/i);
});
test("cancelled chat sessions block sftp writes", () => {
const decision = evaluateRpcPermission({
rpcMethod: "netcatty/sftp/write",
permissionMode: PERMISSION_MODES.AUTO,
params: { chatSessionId: "chat-1" },
context: { chatSessionCancelled: true },
});
assert.equal(decision.allowed, false);
assert.match(decision.error, /cancelled/i);
});
test("cancelled chat sessions still allow sftp reads", () => {
const decision = evaluateRpcPermission({
rpcMethod: "netcatty/sftp/list",
permissionMode: PERMISSION_MODES.AUTO,
params: { chatSessionId: "chat-1" },
context: { chatSessionCancelled: true },
});
assert.equal(decision.allowed, true);
});
test("jobStop bypasses observer and cancelled chat checks", () => {
const observerDecision = evaluateRpcPermission({
rpcMethod: "netcatty/jobStop",
permissionMode: PERMISSION_MODES.OBSERVER,
params: { chatSessionId: "chat-1" },
context: { chatSessionCancelled: true },
});
assert.equal(observerDecision.allowed, true);
assert.notEqual(observerDecision.error, OBSERVER_DENY_MESSAGE);
});
test("unknown rpc methods pass through policy checks", () => {
const decision = evaluateRpcPermission({
rpcMethod: "auth/verify",
permissionMode: PERMISSION_MODES.OBSERVER,
params: {},
});
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, false);
assert.equal(decision.capability, null);
});
test("confirm mode requires approval for portforward start and host notes set", () => {
const portforwardDecision = evaluateRpcPermission({
rpcMethod: "public/portforward/start",
surface: CAPABILITY_SURFACES.PUBLIC,
permissionMode: PERMISSION_MODES.CONFIRM,
params: { chatSessionId: "chat-1", ruleId: "rule-1" },
});
assert.equal(portforwardDecision.requiresApproval, true);
const notesDecision = evaluateRpcPermission({
rpcMethod: "vault/host/notes/set",
surface: CAPABILITY_SURFACES.GLOBAL,
permissionMode: PERMISSION_MODES.CONFIRM,
params: { chatSessionId: "chat-1", hostId: "host-1" },
});
assert.equal(notesDecision.requiresApproval, true);
const publicNotesDecision = evaluateRpcPermission({
rpcMethod: "public/vault/hostNotes/set",
surface: CAPABILITY_SURFACES.PUBLIC,
permissionMode: PERMISSION_MODES.CONFIRM,
params: { chatSessionId: "chat-1", hostId: "host-1" },
});
assert.equal(publicNotesDecision.requiresApproval, true);
});
test("evaluatePermissionWithGrants skips approval when a grant matches", () => {
const decision = evaluatePermissionWithGrants({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.CONFIRM,
params: {
chatSessionId: "chat-1",
sessionId: "session-a",
command: "ls -la",
},
context: { chatSessionCancelled: false },
}, [{
id: "grant-1",
capabilityId: "terminal.execute",
sessionPattern: "session-a",
commandPattern: "ls *",
createdAt: Date.now(),
}]);
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, false);
});
test("evaluatePermissionWithGrants does not let a comment grant approve a multiline command", () => {
const decision = evaluatePermissionWithGrants({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.CONFIRM,
params: {
chatSessionId: "chat-1",
sessionId: "session-a",
command: [
"# 1a) clear the kernel_options_post profile field",
"cobbler profile edit --name=openEuler-22.03-aarch64 --kernel-options-post=\"\"",
].join("\n"),
},
context: { chatSessionCancelled: false },
}, [{
id: "grant-comment",
capabilityId: "terminal.execute",
sessionPattern: "session-a",
commandPattern: "# *",
createdAt: Date.now(),
}]);
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, true);
});
test("evaluatePermissionWithGrants does not let a here-doc body grant approve the command", () => {
const decision = evaluatePermissionWithGrants({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.CONFIRM,
params: {
chatSessionId: "chat-1",
sessionId: "session-a",
command: [
"cat <<'EOF'",
"rm -rf /tmp/demo",
"EOF",
].join("\n"),
},
context: { chatSessionCancelled: false },
}, [{
id: "grant-rm",
capabilityId: "terminal.execute",
sessionPattern: "session-a",
commandPattern: "rm *",
createdAt: Date.now(),
}]);
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, true);
});
test("evaluatePermissionWithGrants does not let a piped here-doc body grant approve the command", () => {
const decision = evaluatePermissionWithGrants({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.CONFIRM,
params: {
chatSessionId: "chat-1",
sessionId: "session-a",
command: [
"cat <<EOF | grep needle",
"rm -rf /tmp/demo",
"EOF",
].join("\n"),
},
context: { chatSessionCancelled: false },
}, [{
id: "grant-rm",
capabilityId: "terminal.execute",
sessionPattern: "session-a",
commandPattern: "rm *",
createdAt: Date.now(),
}]);
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, true);
});
test("evaluatePermissionWithGrants does not let an fd-prefixed here-doc body grant approve the command", () => {
const decision = evaluatePermissionWithGrants({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.CONFIRM,
params: {
chatSessionId: "chat-1",
sessionId: "session-a",
command: [
"cat 0<<EOF",
"rm -rf /tmp/demo",
"EOF",
].join("\n"),
},
context: { chatSessionCancelled: false },
}, [{
id: "grant-rm",
capabilityId: "terminal.execute",
sessionPattern: "session-a",
commandPattern: "rm *",
createdAt: Date.now(),
}]);
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, true);
});
test("evaluatePermissionWithGrants does not let a background command grant approve the next command", () => {
const decision = evaluatePermissionWithGrants({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.CONFIRM,
params: {
chatSessionId: "chat-1",
sessionId: "session-a",
command: "cd /tmp; sleep 1 & rm -rf demo",
},
context: { chatSessionCancelled: false },
}, [{
id: "grant-sleep",
capabilityId: "terminal.execute",
sessionPattern: "session-a",
commandPattern: "sleep *",
createdAt: Date.now(),
}]);
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, true);
});
test("evaluatePermissionWithGrants does not let cwd substitutions hide before a later grant", () => {
const decision = evaluatePermissionWithGrants({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.CONFIRM,
params: {
chatSessionId: "chat-1",
sessionId: "session-a",
command: "cd \"$(pwd)\"; ls -la",
},
context: { chatSessionCancelled: false },
}, [{
id: "grant-ls",
capabilityId: "terminal.execute",
sessionPattern: "session-a",
commandPattern: "ls *",
createdAt: Date.now(),
}]);
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, true);
});
test("evaluatePermissionWithGrants does not let quoted here-doc operator text hide later commands", () => {
const decision = evaluatePermissionWithGrants({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.CONFIRM,
params: {
chatSessionId: "chat-1",
sessionId: "session-a",
command: [
"cd /tmp; echo '<<EOF'",
"rm -rf demo",
"EOF",
].join("\n"),
},
context: { chatSessionCancelled: false },
}, [{
id: "grant-echo",
capabilityId: "terminal.execute",
sessionPattern: "session-a",
commandPattern: "echo *",
createdAt: Date.now(),
}]);
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, true);
});
test("evaluatePermissionWithGrants keeps commands after mixed-quoted here-doc delimiters grantable", () => {
const decision = evaluatePermissionWithGrants({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.CONFIRM,
params: {
chatSessionId: "chat-1",
sessionId: "session-a",
command: [
"cat <<E\"OF\"",
"body text",
"EOF",
"ls -la",
].join("\n"),
},
context: { chatSessionCancelled: false },
}, [{
id: "grant-cat",
capabilityId: "terminal.execute",
sessionPattern: "session-a",
commandPattern: "cat *",
createdAt: Date.now(),
}]);
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, true);
});
test("evaluatePermissionWithGrants does not let arithmetic shifts hide following commands", () => {
const decision = evaluatePermissionWithGrants({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.CONFIRM,
params: {
chatSessionId: "chat-1",
sessionId: "session-a",
command: [
"ls $((1 << 2))",
"rm -rf demo",
].join("\n"),
},
context: { chatSessionCancelled: false },
}, [{
id: "grant-ls",
capabilityId: "terminal.execute",
sessionPattern: "session-a",
commandPattern: "ls *",
createdAt: Date.now(),
}]);
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, true);
});
test("evaluatePermissionWithGrants keeps commands after ANSI-C quoted here-doc delimiters grantable", () => {
const decision = evaluatePermissionWithGrants({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.CONFIRM,
params: {
chatSessionId: "chat-1",
sessionId: "session-a",
command: [
"cat <<$'E\\x4fF'",
"body text",
"EOF",
"rm -rf demo",
].join("\n"),
},
context: { chatSessionCancelled: false },
}, [{
id: "grant-cat",
capabilityId: "terminal.execute",
sessionPattern: "session-a",
commandPattern: "cat *",
createdAt: Date.now(),
}]);
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, true);
});
test("evaluatePermissionWithGrants keeps commands after dollar-quoted here-doc delimiters grantable", () => {
const decision = evaluatePermissionWithGrants({
rpcMethod: "netcatty/exec",
permissionMode: PERMISSION_MODES.CONFIRM,
params: {
chatSessionId: "chat-1",
sessionId: "session-a",
command: [
"cat <<$'EOF'",
"body text",
"EOF",
"rm -rf demo",
].join("\n"),
},
context: { chatSessionCancelled: false },
}, [{
id: "grant-cat",
capabilityId: "terminal.execute",
sessionPattern: "session-a",
commandPattern: "cat *",
createdAt: Date.now(),
}]);
assert.equal(decision.allowed, true);
assert.equal(decision.requiresApproval, true);
});