"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 < { const decision = evaluatePermissionWithGrants({ rpcMethod: "netcatty/exec", permissionMode: PERMISSION_MODES.CONFIRM, params: { chatSessionId: "chat-1", sessionId: "session-a", command: [ "cat 0< { 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 '< { const decision = evaluatePermissionWithGrants({ rpcMethod: "netcatty/exec", permissionMode: PERMISSION_MODES.CONFIRM, params: { chatSessionId: "chat-1", sessionId: "session-a", command: [ "cat < { 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); });