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
419 lines
12 KiB
TypeScript
419 lines
12 KiB
TypeScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
buildGrantFromApproval,
|
|
buildGrantsFromApproval,
|
|
listGrantableCapabilityIds,
|
|
matchPermissionGrant,
|
|
patternMatches,
|
|
type PermissionGrantRule,
|
|
} from './permissionGrants';
|
|
|
|
const baseRule = (overrides: Partial<PermissionGrantRule>): PermissionGrantRule => ({
|
|
id: 'grant-1',
|
|
capabilityId: 'terminal.execute',
|
|
sessionPattern: 'session-a',
|
|
createdAt: Date.now(),
|
|
...overrides,
|
|
});
|
|
|
|
describe('permissionGrants', () => {
|
|
it('matches wildcard session and command patterns', () => {
|
|
const rules = [baseRule({ sessionPattern: '*', commandPattern: 'ls *' })];
|
|
const matched = matchPermissionGrant(rules, {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'any-session',
|
|
args: { command: 'ls -la /tmp' },
|
|
});
|
|
assert.ok(matched);
|
|
});
|
|
|
|
it('ignores sessionPattern and matches globally by capability and command', () => {
|
|
const rules = [baseRule({ sessionPattern: 'old-session-uuid', commandPattern: 'ls *' })];
|
|
const matched = matchPermissionGrant(rules, {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'different-session',
|
|
args: { command: 'ls -la /tmp' },
|
|
});
|
|
assert.ok(matched);
|
|
});
|
|
|
|
it('does not match a different capability', () => {
|
|
const rules = [baseRule({ sessionPattern: '*' })];
|
|
const matched = matchPermissionGrant(rules, {
|
|
capabilityId: 'terminal.start',
|
|
sessionId: 'session-a',
|
|
args: { command: 'make' },
|
|
});
|
|
assert.equal(matched, null);
|
|
});
|
|
|
|
it('buildGrantFromApproval uses global scope and OpenCode-style command prefix patterns', () => {
|
|
const grant = buildGrantFromApproval('terminal.execute', {
|
|
sessionId: 'ssh-1',
|
|
command: 'systemctl status nginx',
|
|
}, 'chat-1');
|
|
assert.ok(grant);
|
|
assert.equal(grant.sessionPattern, '*');
|
|
assert.equal(grant.commandPattern, 'systemctl status *');
|
|
});
|
|
|
|
it('buildGrantFromApproval returns null when no command grant is produced', () => {
|
|
const grant = buildGrantFromApproval('terminal.execute', {
|
|
sessionId: 'ssh-1',
|
|
command: 'cd /tmp',
|
|
}, 'chat-1');
|
|
|
|
assert.equal(grant, null);
|
|
});
|
|
|
|
it('buildGrantsFromApproval emits one rule per chained command segment', () => {
|
|
const grants = buildGrantsFromApproval('terminal.execute', {
|
|
sessionId: 'ssh-1',
|
|
command: 'cd /tmp && lscpu',
|
|
}, 'chat-1');
|
|
assert.equal(grants.length, 1);
|
|
assert.equal(grants[0]?.commandPattern, 'lscpu *');
|
|
});
|
|
|
|
it('does not let a comment grant approve a multiline command', () => {
|
|
const rules = [baseRule({ sessionPattern: '*', commandPattern: '# *' })];
|
|
const matched = matchPermissionGrant(rules, {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: {
|
|
command: [
|
|
'# 1a) clear the kernel_options_post profile field',
|
|
'cobbler profile edit --name=openEuler-22.03-aarch64 --kernel-options-post=""',
|
|
].join('\n'),
|
|
},
|
|
});
|
|
|
|
assert.equal(matched, null);
|
|
});
|
|
|
|
it('requires every grantable command segment to be covered', () => {
|
|
const rules = [
|
|
baseRule({ id: 'grant-lscpu', sessionPattern: '*', commandPattern: 'lscpu *' }),
|
|
baseRule({ id: 'grant-grep', sessionPattern: '*', commandPattern: 'grep *' }),
|
|
];
|
|
const matched = matchPermissionGrant(rules, {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command: 'cd /tmp && lscpu | grep CPU' },
|
|
});
|
|
|
|
assert.ok(matched);
|
|
|
|
const missingPipeSegment = matchPermissionGrant([rules[0]!], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command: 'cd /tmp && lscpu | grep CPU' },
|
|
});
|
|
|
|
assert.equal(missingPipeSegment, null);
|
|
});
|
|
|
|
it('requires every background command segment to be covered', () => {
|
|
const command = 'cd /tmp; sleep 1 & rm -rf demo';
|
|
|
|
const matchedBySleepOnly = matchPermissionGrant([
|
|
baseRule({ id: 'grant-sleep', sessionPattern: '*', commandPattern: 'sleep *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.equal(matchedBySleepOnly, null);
|
|
|
|
const matchedByBoth = matchPermissionGrant([
|
|
baseRule({ id: 'grant-sleep', sessionPattern: '*', commandPattern: 'sleep *' }),
|
|
baseRule({ id: 'grant-rm', sessionPattern: '*', commandPattern: 'rm *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.ok(matchedByBoth);
|
|
});
|
|
|
|
it('requires cwd segments with shell substitutions to be covered', () => {
|
|
const command = 'cd "$(pwd)"; ls -la';
|
|
|
|
const matchedByLsOnly = matchPermissionGrant([
|
|
baseRule({ id: 'grant-ls', sessionPattern: '*', commandPattern: 'ls *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.equal(matchedByLsOnly, null);
|
|
|
|
const matchedByBoth = matchPermissionGrant([
|
|
baseRule({ id: 'grant-cd', sessionPattern: '*', commandPattern: 'cd *' }),
|
|
baseRule({ id: 'grant-ls', sessionPattern: '*', commandPattern: 'ls *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.ok(matchedByBoth);
|
|
});
|
|
|
|
it('does not let a here-doc body grant approve the wrapping command', () => {
|
|
const command = [
|
|
"cat <<'EOF'",
|
|
'rm -rf /tmp/demo',
|
|
'EOF',
|
|
].join('\n');
|
|
|
|
const matchedByBody = matchPermissionGrant([
|
|
baseRule({ id: 'grant-rm', sessionPattern: '*', commandPattern: 'rm *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.equal(matchedByBody, null);
|
|
|
|
const matchedByWrapper = matchPermissionGrant([
|
|
baseRule({ id: 'grant-cat', sessionPattern: '*', commandPattern: 'cat *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.ok(matchedByWrapper);
|
|
});
|
|
|
|
it('does not let a piped here-doc body grant approve the command', () => {
|
|
const command = [
|
|
'cat <<EOF | grep needle',
|
|
'rm -rf /tmp/demo',
|
|
'EOF',
|
|
].join('\n');
|
|
|
|
const matchedByBody = matchPermissionGrant([
|
|
baseRule({ id: 'grant-rm', sessionPattern: '*', commandPattern: 'rm *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.equal(matchedByBody, null);
|
|
|
|
const matchedByPipeline = matchPermissionGrant([
|
|
baseRule({ id: 'grant-cat', sessionPattern: '*', commandPattern: 'cat *' }),
|
|
baseRule({ id: 'grant-grep', sessionPattern: '*', commandPattern: 'grep *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.ok(matchedByPipeline);
|
|
});
|
|
|
|
it('does not let an fd-prefixed here-doc body grant approve the command', () => {
|
|
const command = [
|
|
'cat 0<<EOF',
|
|
'rm -rf /tmp/demo',
|
|
'EOF',
|
|
].join('\n');
|
|
|
|
const matchedByBody = matchPermissionGrant([
|
|
baseRule({ id: 'grant-rm', sessionPattern: '*', commandPattern: 'rm *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.equal(matchedByBody, null);
|
|
|
|
const matchedByWrapper = matchPermissionGrant([
|
|
baseRule({ id: 'grant-cat', sessionPattern: '*', commandPattern: 'cat *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.ok(matchedByWrapper);
|
|
});
|
|
|
|
it('does not let quoted here-doc operator text hide later commands', () => {
|
|
const command = [
|
|
"cd /tmp; echo '<<EOF'",
|
|
'rm -rf demo',
|
|
'EOF',
|
|
].join('\n');
|
|
|
|
const matchedByEchoOnly = matchPermissionGrant([
|
|
baseRule({ id: 'grant-echo', sessionPattern: '*', commandPattern: 'echo *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.equal(matchedByEchoOnly, null);
|
|
|
|
const matchedByAll = matchPermissionGrant([
|
|
baseRule({ id: 'grant-echo', sessionPattern: '*', commandPattern: 'echo *' }),
|
|
baseRule({ id: 'grant-rm', sessionPattern: '*', commandPattern: 'rm *' }),
|
|
baseRule({ id: 'grant-eof', sessionPattern: '*', commandPattern: 'EOF *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.ok(matchedByAll);
|
|
});
|
|
|
|
it('keeps commands after mixed-quoted here-doc delimiters grantable', () => {
|
|
const command = [
|
|
'cat <<E"OF"',
|
|
'body text',
|
|
'EOF',
|
|
'ls -la',
|
|
].join('\n');
|
|
|
|
const matchedByCatOnly = matchPermissionGrant([
|
|
baseRule({ id: 'grant-cat', sessionPattern: '*', commandPattern: 'cat *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.equal(matchedByCatOnly, null);
|
|
|
|
const matchedByBoth = matchPermissionGrant([
|
|
baseRule({ id: 'grant-cat', sessionPattern: '*', commandPattern: 'cat *' }),
|
|
baseRule({ id: 'grant-ls', sessionPattern: '*', commandPattern: 'ls *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.ok(matchedByBoth);
|
|
});
|
|
|
|
it('keeps commands after dollar-quoted here-doc delimiters grantable', () => {
|
|
const command = [
|
|
"cat <<$'EOF'",
|
|
'body text',
|
|
'EOF',
|
|
'rm -rf demo',
|
|
].join('\n');
|
|
|
|
const matchedByCatOnly = matchPermissionGrant([
|
|
baseRule({ id: 'grant-cat', sessionPattern: '*', commandPattern: 'cat *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.equal(matchedByCatOnly, null);
|
|
|
|
const matchedByBoth = matchPermissionGrant([
|
|
baseRule({ id: 'grant-cat', sessionPattern: '*', commandPattern: 'cat *' }),
|
|
baseRule({ id: 'grant-rm', sessionPattern: '*', commandPattern: 'rm *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.ok(matchedByBoth);
|
|
});
|
|
|
|
it('keeps commands after ANSI-C quoted here-doc delimiters grantable', () => {
|
|
const command = [
|
|
"cat <<$'E\\x4fF'",
|
|
'body text',
|
|
'EOF',
|
|
'rm -rf demo',
|
|
].join('\n');
|
|
|
|
const matchedByCatOnly = matchPermissionGrant([
|
|
baseRule({ id: 'grant-cat', sessionPattern: '*', commandPattern: 'cat *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.equal(matchedByCatOnly, null);
|
|
|
|
const matchedByBoth = matchPermissionGrant([
|
|
baseRule({ id: 'grant-cat', sessionPattern: '*', commandPattern: 'cat *' }),
|
|
baseRule({ id: 'grant-rm', sessionPattern: '*', commandPattern: 'rm *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.ok(matchedByBoth);
|
|
});
|
|
|
|
it('does not let arithmetic shifts hide following commands', () => {
|
|
const command = [
|
|
'ls $((1 << 2))',
|
|
'rm -rf demo',
|
|
].join('\n');
|
|
|
|
const matchedByLsOnly = matchPermissionGrant([
|
|
baseRule({ id: 'grant-ls', sessionPattern: '*', commandPattern: 'ls *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.equal(matchedByLsOnly, null);
|
|
|
|
const matchedByBoth = matchPermissionGrant([
|
|
baseRule({ id: 'grant-ls', sessionPattern: '*', commandPattern: 'ls *' }),
|
|
baseRule({ id: 'grant-rm', sessionPattern: '*', commandPattern: 'rm *' }),
|
|
], {
|
|
capabilityId: 'terminal.execute',
|
|
sessionId: 'session-a',
|
|
args: { command },
|
|
});
|
|
|
|
assert.ok(matchedByBoth);
|
|
});
|
|
|
|
it('OpenCode wildcard allows optional args after prefix', () => {
|
|
assert.equal(patternMatches('lscpu *', 'lscpu'), true);
|
|
assert.equal(patternMatches('lscpu *', 'lscpu -e'), true);
|
|
assert.equal(patternMatches('git checkout *', 'git checkout main'), true);
|
|
assert.equal(patternMatches('git checkout *', 'git commit'), false);
|
|
});
|
|
|
|
it('lists grantable capability ids from catalog policy', () => {
|
|
const ids = listGrantableCapabilityIds();
|
|
assert.ok(ids.includes('terminal.execute'));
|
|
assert.ok(ids.includes('sftp.write'));
|
|
assert.ok(!ids.includes('terminal.poll'));
|
|
});
|
|
|
|
it('patternMatches supports regex literals', () => {
|
|
assert.equal(patternMatches('/^ls\\b/', 'ls -la'), true);
|
|
assert.equal(patternMatches('/^ls\\b/', 'cat file'), false);
|
|
});
|
|
});
|