@namzu/sdk 44.3.0 → 45.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/dist/authorization/command-line.d.ts +66 -19
- package/dist/authorization/command-line.d.ts.map +1 -1
- package/dist/authorization/command-line.js +130 -270
- package/dist/authorization/command-line.js.map +1 -1
- package/dist/authorization/gate.d.ts +7 -0
- package/dist/authorization/gate.d.ts.map +1 -1
- package/dist/authorization/gate.js +13 -3
- package/dist/authorization/gate.js.map +1 -1
- package/dist/authorization/rules.d.ts +10 -1
- package/dist/authorization/rules.d.ts.map +1 -1
- package/dist/authorization/rules.js +55 -8
- package/dist/authorization/rules.js.map +1 -1
- package/dist/authorization/shell-lexer.d.ts +152 -0
- package/dist/authorization/shell-lexer.d.ts.map +1 -0
- package/dist/authorization/shell-lexer.js +2156 -0
- package/dist/authorization/shell-lexer.js.map +1 -0
- package/dist/authorization/skill-grant.d.ts +182 -0
- package/dist/authorization/skill-grant.d.ts.map +1 -0
- package/dist/authorization/skill-grant.js +314 -0
- package/dist/authorization/skill-grant.js.map +1 -0
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +2 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +1 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/directory/types.d.ts +2 -0
- package/dist/directory/types.d.ts.map +1 -1
- package/dist/directory/types.js.map +1 -1
- package/dist/manager/resident/outbox.d.ts +4 -4
- package/dist/persona/assembler.d.ts.map +1 -1
- package/dist/persona/assembler.js +5 -2
- package/dist/persona/assembler.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +1 -0
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/public-runtime.d.ts +5 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +15 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +4 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +12 -1
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +9 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.d.ts +2 -2
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +6 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/declined.d.ts +12 -0
- package/dist/runtime/query/declined.d.ts.map +1 -0
- package/dist/runtime/query/declined.js +12 -0
- package/dist/runtime/query/declined.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +36 -40
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +95 -53
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +8 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +13 -0
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +13 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/handoff.d.ts +22 -0
- package/dist/runtime/query/iteration/phases/handoff.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/handoff.js +65 -0
- package/dist/runtime/query/iteration/phases/handoff.js.map +1 -0
- package/dist/runtime/query/iteration/phases/index.d.ts +1 -0
- package/dist/runtime/query/iteration/phases/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/index.js +1 -0
- package/dist/runtime/query/iteration/phases/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +56 -3
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +3 -2
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +11 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +36 -3
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +3 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/schedules/cron.d.ts +21 -0
- package/dist/schedules/cron.d.ts.map +1 -0
- package/dist/schedules/cron.js +167 -0
- package/dist/schedules/cron.js.map +1 -0
- package/dist/schedules/describe.d.ts +14 -0
- package/dist/schedules/describe.d.ts.map +1 -0
- package/dist/schedules/describe.js +133 -0
- package/dist/schedules/describe.js.map +1 -0
- package/dist/schedules/errors.d.ts +11 -0
- package/dist/schedules/errors.d.ts.map +1 -0
- package/dist/schedules/errors.js +15 -0
- package/dist/schedules/errors.js.map +1 -0
- package/dist/schedules/evaluate.d.ts +35 -0
- package/dist/schedules/evaluate.d.ts.map +1 -0
- package/dist/schedules/evaluate.js +158 -0
- package/dist/schedules/evaluate.js.map +1 -0
- package/dist/schedules/index.d.ts +11 -0
- package/dist/schedules/index.d.ts.map +1 -0
- package/dist/schedules/index.js +8 -0
- package/dist/schedules/index.js.map +1 -0
- package/dist/schedules/next-fire.d.ts +50 -0
- package/dist/schedules/next-fire.d.ts.map +1 -0
- package/dist/schedules/next-fire.js +250 -0
- package/dist/schedules/next-fire.js.map +1 -0
- package/dist/schedules/spec.d.ts +30 -0
- package/dist/schedules/spec.d.ts.map +1 -0
- package/dist/schedules/spec.js +169 -0
- package/dist/schedules/spec.js.map +1 -0
- package/dist/schedules/types.d.ts +144 -0
- package/dist/schedules/types.d.ts.map +1 -0
- package/dist/schedules/types.js +11 -0
- package/dist/schedules/types.js.map +1 -0
- package/dist/schedules/tz.d.ts +44 -0
- package/dist/schedules/tz.d.ts.map +1 -0
- package/dist/schedules/tz.js +141 -0
- package/dist/schedules/tz.js.map +1 -0
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/loader.d.ts +21 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +51 -1
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +18 -6
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/browser-url.d.ts +83 -0
- package/dist/tools/builtins/browser-url.d.ts.map +1 -0
- package/dist/tools/builtins/browser-url.js +240 -0
- package/dist/tools/builtins/browser-url.js.map +1 -0
- package/dist/tools/builtins/browser.d.ts +367 -0
- package/dist/tools/builtins/browser.d.ts.map +1 -0
- package/dist/tools/builtins/browser.js +704 -0
- package/dist/tools/builtins/browser.js.map +1 -0
- package/dist/tools/builtins/skill.d.ts +2 -9
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +74 -51
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/command-shell.d.ts +90 -0
- package/dist/tools/command-shell.d.ts.map +1 -0
- package/dist/tools/command-shell.js +129 -0
- package/dist/tools/command-shell.js.map +1 -0
- package/dist/tools/defineTool.d.ts +13 -0
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +30 -1
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/tools/schedules/index.d.ts +5 -0
- package/dist/tools/schedules/index.d.ts.map +1 -0
- package/dist/tools/schedules/index.js +4 -0
- package/dist/tools/schedules/index.js.map +1 -0
- package/dist/tools/schedules/loop-tool.d.ts +14 -0
- package/dist/tools/schedules/loop-tool.d.ts.map +1 -0
- package/dist/tools/schedules/loop-tool.js +81 -0
- package/dist/tools/schedules/loop-tool.js.map +1 -0
- package/dist/tools/schedules/present.d.ts +16 -0
- package/dist/tools/schedules/present.d.ts.map +1 -0
- package/dist/tools/schedules/present.js +69 -0
- package/dist/tools/schedules/present.js.map +1 -0
- package/dist/tools/schedules/prompt-scan.d.ts +17 -0
- package/dist/tools/schedules/prompt-scan.d.ts.map +1 -0
- package/dist/tools/schedules/prompt-scan.js +92 -0
- package/dist/tools/schedules/prompt-scan.js.map +1 -0
- package/dist/tools/schedules/schedule-tool.d.ts +16 -0
- package/dist/tools/schedules/schedule-tool.d.ts.map +1 -0
- package/dist/tools/schedules/schedule-tool.js +327 -0
- package/dist/tools/schedules/schedule-tool.js.map +1 -0
- package/dist/tools/schedules/types.d.ts +184 -0
- package/dist/tools/schedules/types.d.ts.map +1 -0
- package/dist/tools/schedules/types.js +11 -0
- package/dist/tools/schedules/types.js.map +1 -0
- package/dist/types/authorization/index.d.ts +86 -9
- package/dist/types/authorization/index.d.ts.map +1 -1
- package/dist/types/authorization/index.js +11 -1
- package/dist/types/authorization/index.js.map +1 -1
- package/dist/types/browser/index.d.ts +280 -0
- package/dist/types/browser/index.d.ts.map +1 -0
- package/dist/types/browser/index.js +12 -0
- package/dist/types/browser/index.js.map +1 -0
- package/dist/types/hitl/index.d.ts +23 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/session/events.d.ts +6 -0
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +23 -0
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +9 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/tool/index.d.ts +108 -5
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +7 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/dist/utils/frontmatter.d.ts +35 -3
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +45 -5
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/id.d.ts +8 -0
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +12 -0
- package/dist/utils/id.js.map +1 -1
- package/package.json +1 -1
- package/src/authorization/command-line.ts +148 -293
- package/src/authorization/gate.ts +22 -2
- package/src/authorization/rules.ts +67 -8
- package/src/authorization/shell-lexer.ts +2349 -0
- package/src/authorization/skill-grant.ts +400 -0
- package/src/bridge/a2a/mapper.ts +2 -0
- package/src/bridge/sse/mapper.ts +1 -0
- package/src/directory/types.ts +2 -0
- package/src/persona/assembler.ts +5 -2
- package/src/prompt/coding-agent-doctrine.ts +1 -0
- package/src/public-runtime.ts +37 -0
- package/src/public-tools.ts +37 -1
- package/src/public-types.ts +57 -0
- package/src/runtime/jobs/registry.ts +19 -11
- package/src/runtime/query/declined.ts +12 -0
- package/src/runtime/query/executor.ts +116 -56
- package/src/runtime/query/index.ts +8 -0
- package/src/runtime/query/iteration/index.ts +13 -0
- package/src/runtime/query/iteration/phases/context.ts +13 -0
- package/src/runtime/query/iteration/phases/handoff.ts +74 -0
- package/src/runtime/query/iteration/phases/index.ts +1 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +55 -3
- package/src/runtime/query/resume-pending.ts +3 -2
- package/src/runtime/query/review-policy.ts +57 -3
- package/src/runtime/query/tooling.ts +4 -0
- package/src/schedules/cron.ts +202 -0
- package/src/schedules/describe.ts +138 -0
- package/src/schedules/errors.ts +15 -0
- package/src/schedules/evaluate.ts +178 -0
- package/src/schedules/index.ts +18 -0
- package/src/schedules/next-fire.ts +257 -0
- package/src/schedules/spec.ts +210 -0
- package/src/schedules/types.ts +163 -0
- package/src/schedules/tz.ts +155 -0
- package/src/skills/index.ts +1 -1
- package/src/skills/loader.ts +59 -1
- package/src/tools/builtins/bash.ts +24 -6
- package/src/tools/builtins/browser-url.ts +251 -0
- package/src/tools/builtins/browser.ts +817 -0
- package/src/tools/builtins/skill.ts +92 -53
- package/src/tools/command-shell.ts +166 -0
- package/src/tools/defineTool.ts +36 -1
- package/src/tools/schedules/index.ts +4 -0
- package/src/tools/schedules/loop-tool.ts +85 -0
- package/src/tools/schedules/present.ts +86 -0
- package/src/tools/schedules/prompt-scan.ts +96 -0
- package/src/tools/schedules/schedule-tool.ts +376 -0
- package/src/tools/schedules/types.ts +197 -0
- package/src/types/authorization/index.ts +60 -2
- package/src/types/browser/index.ts +341 -0
- package/src/types/hitl/index.ts +21 -0
- package/src/types/session/events.ts +6 -0
- package/src/types/session/records.ts +10 -0
- package/src/types/tool/index.ts +109 -5
- package/src/types/tool/presentation.ts +7 -0
- package/src/utils/frontmatter.ts +81 -5
- package/src/utils/id.ts +14 -0
|
@@ -32,8 +32,9 @@
|
|
|
32
32
|
* caller must read the two decisions differently, and {@link evaluateRule}
|
|
33
33
|
* does:
|
|
34
34
|
*
|
|
35
|
-
* - **deny** matches when ANY segment matches
|
|
36
|
-
*
|
|
35
|
+
* - **deny** matches when ANY segment matches, or when any command's decoded
|
|
36
|
+
* words ({@link decodedCommands}) do. One prohibited command poisons the
|
|
37
|
+
* line it rides on, however it is quoted.
|
|
37
38
|
* - **allow** matches only when EVERY segment matches, and never when the line
|
|
38
39
|
* is {@link CommandLineDecomposition.opaque}. Permission is a claim about the
|
|
39
40
|
* whole line, and a claim that cannot be checked is not granted.
|
|
@@ -41,35 +42,53 @@
|
|
|
41
42
|
* That asymmetry is the same one `refuse-do-not-degrade` describes: when the
|
|
42
43
|
* analysis is uncertain, the uncertainty spends against the permissive answer.
|
|
43
44
|
*
|
|
45
|
+
* ## Where the commands come from
|
|
46
|
+
*
|
|
47
|
+
* One lexer, {@link lexShellCommandLine}, reads the line the way bash does and
|
|
48
|
+
* is the only thing in the SDK that knows bash's quoting. This module and
|
|
49
|
+
* {@link writesThroughRedirection} are views of its result. There used to be
|
|
50
|
+
* three hand-written walkers here, each with its own idea of where a quote
|
|
51
|
+
* ends, and every disagreement between them was a way to run a command the
|
|
52
|
+
* rules never saw.
|
|
53
|
+
*
|
|
44
54
|
* ## What `opaque` means
|
|
45
55
|
*
|
|
46
56
|
* Some lines contain text that is not the command that runs. Command
|
|
47
57
|
* substitution (`$(…)`, backticks, `<(…)`) executes something whose text is
|
|
48
|
-
* not in the line at all, and `eval` runs a string assembled at
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
58
|
+
* not in the line at all, and `eval` or `source` runs a string assembled at
|
|
59
|
+
* runtime. The lexer also reports a line opaque when it does not parse, or
|
|
60
|
+
* contains a construct it does not model. No decomposition of the source can
|
|
61
|
+
* be a decomposition of what ran, so `allow` declines it. `deny` still tests
|
|
62
|
+
* what is visible, because a deny that matches too much costs a prompt and a
|
|
63
|
+
* deny that matches too little costs the thing it was written to prevent.
|
|
53
64
|
*
|
|
54
65
|
* ## What it deliberately does not do
|
|
55
66
|
*
|
|
56
|
-
* A value
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
67
|
+
* A value that is one plain command comes back as itself, byte for byte. That
|
|
68
|
+
* keeps every rule about a non-command argument — a path, a number, a URL —
|
|
69
|
+
* behaving exactly as it did, and confines this machinery to the case that
|
|
70
|
+
* motivated it.
|
|
60
71
|
*
|
|
61
|
-
* It is a decomposition, not a shell. `xargs sh -c`,
|
|
62
|
-
* file, and a shell invoked through an interpreter it does not
|
|
63
|
-
* pass through as ordinary text. Each of those either denies as
|
|
64
|
-
* an allow rule, fails to match every segment and so declines.
|
|
65
|
-
* is a prompt, never a silent grant.
|
|
72
|
+
* It is a decomposition, not a shell. `xargs sh -c`, `env git push`, a command
|
|
73
|
+
* read from a file, and a shell invoked through an interpreter it does not
|
|
74
|
+
* recognise all pass through as ordinary text. Each of those either denies as
|
|
75
|
+
* before or, for an allow rule, fails to match every segment and so declines.
|
|
76
|
+
* The failure mode is a prompt, never a silent grant.
|
|
66
77
|
*/
|
|
67
78
|
|
|
79
|
+
import {
|
|
80
|
+
type ShellDialect,
|
|
81
|
+
type ShellLexResult,
|
|
82
|
+
type ShellRedirection,
|
|
83
|
+
basename,
|
|
84
|
+
lexShellCommandLine,
|
|
85
|
+
} from './shell-lexer.js'
|
|
86
|
+
|
|
68
87
|
/** The commands a line runs, and whether that list can be trusted as complete. */
|
|
69
88
|
export interface CommandLineDecomposition {
|
|
70
89
|
/**
|
|
71
|
-
* The individual commands, in
|
|
72
|
-
* decomposes to nothing yields the original.
|
|
90
|
+
* The individual commands' source text, in the order they were read. Never
|
|
91
|
+
* empty: a line that decomposes to nothing yields the original.
|
|
73
92
|
*/
|
|
74
93
|
readonly segments: readonly string[]
|
|
75
94
|
/**
|
|
@@ -79,312 +98,148 @@ export interface CommandLineDecomposition {
|
|
|
79
98
|
readonly opaque: boolean
|
|
80
99
|
}
|
|
81
100
|
|
|
82
|
-
/**
|
|
83
|
-
* Shells whose `-c` argument is another command line.
|
|
84
|
-
*
|
|
85
|
-
* Matched on the basename, so `/bin/bash` and `bash` are the same entry. An
|
|
86
|
-
* interpreter absent from this list is not a hole that grants anything: its
|
|
87
|
-
* payload stays inside one segment, where an allow rule fails to match it.
|
|
88
|
-
*/
|
|
89
|
-
const NESTED_SHELLS = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh', 'ash', 'busybox'])
|
|
90
|
-
|
|
91
101
|
/** Commands whose argument is code assembled at runtime. */
|
|
92
102
|
const RUNTIME_EVALUATORS = new Set(['eval', 'source', '.'])
|
|
93
103
|
|
|
94
104
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* A line that exceeds either is reported opaque rather than truncated: a
|
|
105
|
+
* Width limit. A line past it is reported opaque rather than truncated: a
|
|
98
106
|
* shortened list of segments would read as complete to `allow`, which is the
|
|
99
107
|
* one reading that must never be wrong.
|
|
100
108
|
*/
|
|
101
|
-
const MAX_DEPTH = 4
|
|
102
109
|
const MAX_SEGMENTS = 64
|
|
103
110
|
|
|
104
|
-
export function decomposeCommandLine(command: string): CommandLineDecomposition {
|
|
105
|
-
const state: WalkState = { opaque: false, structured: false }
|
|
106
|
-
const segments = split(command, state, 0)
|
|
107
|
-
|
|
108
|
-
// The untouched-value case, kept exact. Nothing was cut and nothing was
|
|
109
|
-
// unpacked, so there is no decomposition to report and the value goes back
|
|
110
|
-
// as it arrived — which is what keeps a rule about a path or a URL seeing
|
|
111
|
-
// the string it always saw, punctuation and surrounding space included.
|
|
112
|
-
if (!state.structured) return { segments: [command], opaque: state.opaque }
|
|
113
|
-
|
|
114
|
-
if (segments.length === 0) return { segments: [command], opaque: state.opaque }
|
|
115
|
-
if (segments.length > MAX_SEGMENTS) {
|
|
116
|
-
return { segments: segments.slice(0, MAX_SEGMENTS), opaque: true }
|
|
117
|
-
}
|
|
118
|
-
return { segments, opaque: state.opaque }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
interface WalkState {
|
|
122
|
-
opaque: boolean
|
|
123
|
-
/**
|
|
124
|
-
* Whether anything was cut or unpacked. False means the value is not a
|
|
125
|
-
* command line as far as this module can tell, and it goes back untouched.
|
|
126
|
-
*/
|
|
127
|
-
structured: boolean
|
|
128
|
-
}
|
|
129
|
-
|
|
130
111
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* b"` is one command that prints a literal, and a splitter that cannot tell
|
|
135
|
-
* would report a second command named `b"` — inventing a segment is as wrong as
|
|
136
|
-
* missing one, because `allow` requires every segment to match.
|
|
112
|
+
* `dialect` is the shell that will run the line (see `ShellDialect`); a
|
|
113
|
+
* caller that does not know passes `sh`, whose reading holds for any POSIX
|
|
114
|
+
* shell.
|
|
137
115
|
*/
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
for (let i = 0; i < command.length; i += 1) {
|
|
151
|
-
const char = command[i] as string
|
|
152
|
-
|
|
153
|
-
if (quote === "'") {
|
|
154
|
-
// Single quotes suspend everything, including the backslash. This is
|
|
155
|
-
// the branch that keeps `echo 'a && b'` one command.
|
|
156
|
-
if (char === "'") quote = null
|
|
157
|
-
current += char
|
|
158
|
-
continue
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (char === '\\') {
|
|
162
|
-
// An escaped separator is a literal, so both characters go through
|
|
163
|
-
// untouched and the next loop never sees the separator as one.
|
|
164
|
-
current += char + (command[i + 1] ?? '')
|
|
165
|
-
i += 1
|
|
166
|
-
continue
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (quote === '"') {
|
|
170
|
-
if (char === '"') quote = null
|
|
171
|
-
// Substitution is live inside double quotes, which is exactly where
|
|
172
|
-
// it hides best.
|
|
173
|
-
else if (isSubstitutionStart(command, i)) state.opaque = true
|
|
174
|
-
current += char
|
|
175
|
-
continue
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (char === "'" || char === '"') {
|
|
179
|
-
quote = char
|
|
180
|
-
current += char
|
|
181
|
-
continue
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (isSubstitutionStart(command, i)) {
|
|
185
|
-
state.opaque = true
|
|
186
|
-
current += char
|
|
187
|
-
continue
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const separator = separatorAt(command, i)
|
|
191
|
-
if (separator > 0) {
|
|
192
|
-
state.structured = true
|
|
193
|
-
cut()
|
|
194
|
-
i += separator - 1
|
|
195
|
-
continue
|
|
116
|
+
export function decomposeCommandLine(
|
|
117
|
+
command: string,
|
|
118
|
+
dialect: ShellDialect = 'bash',
|
|
119
|
+
): CommandLineDecomposition {
|
|
120
|
+
const lexed = lex(command, dialect)
|
|
121
|
+
let opaque = lexed.opaque
|
|
122
|
+
for (const each of lexed.commands) {
|
|
123
|
+
const head = each.words[each.assignments]
|
|
124
|
+
if (head !== undefined && !head.expands && RUNTIME_EVALUATORS.has(basename(head.value))) {
|
|
125
|
+
// The argument is source text assembled elsewhere. Even when it is a
|
|
126
|
+
// visible literal, what runs is decided at runtime.
|
|
127
|
+
opaque = true
|
|
196
128
|
}
|
|
197
|
-
|
|
198
|
-
current += char
|
|
199
129
|
}
|
|
200
130
|
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
if (
|
|
131
|
+
// A line that does not parse runs none of the text from its error on, and
|
|
132
|
+
// what it ran before that is in `decodedCommands` for deny. The value goes
|
|
133
|
+
// back untouched, the way a path or a URL that is not shell at all does.
|
|
134
|
+
if (!lexed.complete || lexed.commands.length === 0) return { segments: [command], opaque }
|
|
135
|
+
|
|
136
|
+
// The untouched-value case, kept exact: one command whose text is the
|
|
137
|
+
// whole line. Nothing was cut and nothing was unpacked, so the value goes
|
|
138
|
+
// back as it arrived — which is what keeps a rule about a path or a URL
|
|
139
|
+
// seeing the string it always saw, surrounding space included.
|
|
140
|
+
const only = lexed.commands[0]
|
|
141
|
+
if (
|
|
142
|
+
lexed.commands.length === 1 &&
|
|
143
|
+
only !== undefined &&
|
|
144
|
+
only.origin === 'line' &&
|
|
145
|
+
only.text === command.trim()
|
|
146
|
+
) {
|
|
147
|
+
return { segments: [command], opaque }
|
|
148
|
+
}
|
|
205
149
|
|
|
206
|
-
|
|
207
|
-
|
|
150
|
+
const segments = lexed.commands.map((each) => each.text)
|
|
151
|
+
if (segments.length > MAX_SEGMENTS)
|
|
152
|
+
return { segments: segments.slice(0, MAX_SEGMENTS), opaque: true }
|
|
153
|
+
return { segments, opaque }
|
|
208
154
|
}
|
|
209
155
|
|
|
210
156
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
157
|
+
* Each command's words as bash passes them (quotes removed, `$'…'` decoded),
|
|
158
|
+
* joined by single spaces — and, for a command led by assignments, the same
|
|
159
|
+
* without them. For `deny` only.
|
|
160
|
+
*
|
|
161
|
+
* A deny rule written as `^git push` must not be evaded by `'git' push`,
|
|
162
|
+
* `g\it push`, `$'git' push` or `GIT_DIR=x git push`: the source text of each
|
|
163
|
+
* differs from the pattern and the command that runs does not. `allow` does
|
|
164
|
+
* not use these. Its subject stays the source text, so a pattern that names
|
|
165
|
+
* quotes keeps meaning what its author wrote, and a decoded form can only ever
|
|
166
|
+
* add a match — which for `deny` is the safe direction and for `allow` is not.
|
|
217
167
|
*/
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return 1
|
|
168
|
+
export function decodedCommands(
|
|
169
|
+
command: string,
|
|
170
|
+
dialect: ShellDialect = 'bash',
|
|
171
|
+
): readonly string[] {
|
|
172
|
+
const out: string[] = []
|
|
173
|
+
for (const each of lex(command, dialect).commands) {
|
|
174
|
+
if (each.words.length === 0) continue
|
|
175
|
+
out.push(each.words.map((word) => word.value).join(' '))
|
|
176
|
+
if (each.assignments > 0 && each.words.length > each.assignments) {
|
|
177
|
+
out.push(
|
|
178
|
+
each.words
|
|
179
|
+
.slice(each.assignments)
|
|
180
|
+
.map((word) => word.value)
|
|
181
|
+
.join(' '),
|
|
182
|
+
)
|
|
183
|
+
}
|
|
235
184
|
}
|
|
236
|
-
return
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** Whether a command substitution opens here. */
|
|
240
|
-
function isSubstitutionStart(command: string, index: number): boolean {
|
|
241
|
-
const char = command[index]
|
|
242
|
-
if (char === '`') return true
|
|
243
|
-
if (char === '$' && command[index + 1] === '(') return true
|
|
244
|
-
// Process substitution: `diff <(a) <(b)` runs `a` and `b`.
|
|
245
|
-
if ((char === '<' || char === '>') && command[index + 1] === '(') return true
|
|
246
|
-
return false
|
|
185
|
+
return out
|
|
247
186
|
}
|
|
248
187
|
|
|
249
188
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
189
|
+
* Whether a command line sends output into a file through a shell redirection.
|
|
190
|
+
*
|
|
191
|
+
* A permission pattern names commands. `>`, `>>`, `>|`, `&>`, `&>>`, `<>` and
|
|
192
|
+
* `>&word` open a file for writing whose path is not the command's argument,
|
|
193
|
+
* so a pattern that covers `git status *` would otherwise also cover
|
|
194
|
+
* `git status > ~/.bashrc`. Callers that grant on a pattern's say-so decline
|
|
195
|
+
* such a line.
|
|
196
|
+
*
|
|
197
|
+
* Not writes: a target of `/dev/null`, descriptor duplication and closing
|
|
198
|
+
* (`2>&1`, `>&2`, `>&-`), and anything quoted or escaped so that it is not an
|
|
199
|
+
* operator. Anything whose target is not known before the line runs — a
|
|
200
|
+
* target built from a variable, a glob or a tilde — counts as a write, and so
|
|
201
|
+
* does a line that does not parse or holds a process substitution: the
|
|
202
|
+
* uncertainty spends against the grant.
|
|
255
203
|
*/
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
204
|
+
export function writesThroughRedirection(command: string, dialect: ShellDialect = 'bash'): boolean {
|
|
205
|
+
const lexed = lex(command, dialect)
|
|
206
|
+
if (!lexed.complete) return true
|
|
207
|
+
if (lexed.reasons.includes('process substitution')) return true
|
|
208
|
+
return lexed.redirections.some(writes)
|
|
261
209
|
}
|
|
262
210
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (RUNTIME_EVALUATORS.has(basename(head.text))) {
|
|
281
|
-
// The argument is source text assembled elsewhere. Even when it is a
|
|
282
|
-
// visible literal, what runs is decided at runtime.
|
|
283
|
-
state.opaque = true
|
|
284
|
-
return [segment]
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (!NESTED_SHELLS.has(basename(head.text))) return [segment]
|
|
288
|
-
|
|
289
|
-
const flag = words.findIndex(
|
|
290
|
-
(word, index) => index > 0 && word.quoted === null && word.text === '-c',
|
|
291
|
-
)
|
|
292
|
-
if (flag < 0) return [segment]
|
|
293
|
-
|
|
294
|
-
const payload = words[flag + 1]
|
|
295
|
-
if (payload === undefined) {
|
|
296
|
-
// `bash -c` with nothing after it is either a syntax error or an
|
|
297
|
-
// argument this tokenizer failed to read. Neither may be reported as
|
|
298
|
-
// "there is no nested command".
|
|
299
|
-
state.opaque = true
|
|
300
|
-
return [segment]
|
|
211
|
+
function writes(redirection: ShellRedirection): boolean {
|
|
212
|
+
const { operator, target } = redirection
|
|
213
|
+
switch (operator) {
|
|
214
|
+
case '<':
|
|
215
|
+
case '<&':
|
|
216
|
+
case '<<':
|
|
217
|
+
case '<<-':
|
|
218
|
+
case '<<<':
|
|
219
|
+
return false
|
|
220
|
+
case '>&':
|
|
221
|
+
// `>&N`, `>&N-`, `>&-` duplicate or close a descriptor. `>&word`
|
|
222
|
+
// with any other word redirects both streams into that file.
|
|
223
|
+
if (!target.expands && /^(?:\d+-?|-)$/.test(target.value)) return false
|
|
224
|
+
return target.expands || target.value !== '/dev/null'
|
|
225
|
+
default:
|
|
226
|
+
return target.expands || target.value !== '/dev/null'
|
|
301
227
|
}
|
|
302
|
-
|
|
303
|
-
if (depth + 1 >= MAX_DEPTH) {
|
|
304
|
-
state.opaque = true
|
|
305
|
-
return [segment]
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const nested = split(payload.text, state, depth + 1)
|
|
309
|
-
if (nested.length === 0) return [segment]
|
|
310
|
-
state.structured = true
|
|
311
|
-
return [segment, ...nested]
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
interface Word {
|
|
315
|
-
readonly text: string
|
|
316
|
-
/** The quote that wrapped it, or null when it was bare. */
|
|
317
|
-
readonly quoted: "'" | '"' | null
|
|
318
228
|
}
|
|
319
229
|
|
|
320
230
|
/**
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
* The quote is reported rather than discarded because `-c` must be the flag and
|
|
324
|
-
* not a literal: `echo "-c"` names no nested shell, and treating its next word
|
|
325
|
-
* as a command line would decompose a string that never runs.
|
|
231
|
+
* One gate evaluation tests the same line against every rule, and each rule
|
|
232
|
+
* asks for it again. The last line lexed is kept so that is one lexing.
|
|
326
233
|
*/
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
let quote: "'" | '"' | null = null
|
|
331
|
-
let sawQuote: "'" | '"' | null = null
|
|
332
|
-
let open = false
|
|
333
|
-
|
|
334
|
-
const push = (): void => {
|
|
335
|
-
if (open) words.push({ text: current, quoted: sawQuote })
|
|
336
|
-
current = ''
|
|
337
|
-
sawQuote = null
|
|
338
|
-
open = false
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
for (let i = 0; i < segment.length; i += 1) {
|
|
342
|
-
const char = segment[i] as string
|
|
343
|
-
|
|
344
|
-
if (quote === "'") {
|
|
345
|
-
// Single quotes suspend the backslash too, so this branch precedes
|
|
346
|
-
// the escape below rather than sharing it.
|
|
347
|
-
if (char === "'") quote = null
|
|
348
|
-
else current += char
|
|
349
|
-
open = true
|
|
350
|
-
continue
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (char === '\\' && i + 1 < segment.length) {
|
|
354
|
-
current += segment[i + 1]
|
|
355
|
-
i += 1
|
|
356
|
-
open = true
|
|
357
|
-
continue
|
|
358
|
-
}
|
|
234
|
+
let cached:
|
|
235
|
+
| { readonly command: string; readonly dialect: ShellDialect; readonly result: ShellLexResult }
|
|
236
|
+
| undefined
|
|
359
237
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
open = true
|
|
364
|
-
continue
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (char === "'" || char === '"') {
|
|
368
|
-
quote = char
|
|
369
|
-
sawQuote = char
|
|
370
|
-
open = true
|
|
371
|
-
continue
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (char === ' ' || char === '\t') {
|
|
375
|
-
push()
|
|
376
|
-
continue
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
current += char
|
|
380
|
-
open = true
|
|
238
|
+
function lex(command: string, dialect: ShellDialect): ShellLexResult {
|
|
239
|
+
if (cached !== undefined && cached.command === command && cached.dialect === dialect) {
|
|
240
|
+
return cached.result
|
|
381
241
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
return
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function basename(word: string): string {
|
|
388
|
-
const cut = word.lastIndexOf('/')
|
|
389
|
-
return cut < 0 ? word : word.slice(cut + 1)
|
|
242
|
+
const result = lexShellCommandLine(command, { dialect })
|
|
243
|
+
cached = { command, dialect, result }
|
|
244
|
+
return result
|
|
390
245
|
}
|
|
@@ -10,11 +10,18 @@ import type { ToolDefinition } from '../types/tool/index.js'
|
|
|
10
10
|
import { SCOPE_ATTRIBUTE } from '../utils/log/types.js'
|
|
11
11
|
import type { Logger } from '../utils/logger.js'
|
|
12
12
|
import { evaluateRule } from './rules.js'
|
|
13
|
+
import type { ShellDialect } from './shell-lexer.js'
|
|
13
14
|
|
|
14
15
|
export interface ToolCallContext {
|
|
15
16
|
readonly toolName: string
|
|
16
17
|
readonly toolInput: unknown
|
|
17
18
|
readonly toolDef: ToolDefinition | undefined
|
|
19
|
+
/**
|
|
20
|
+
* The shell this call's command line will run in, when the caller knows
|
|
21
|
+
* (`ToolDefinition.commandDialect`). Absent, a command line is read in the
|
|
22
|
+
* `sh` dialect, which holds whichever shell runs it.
|
|
23
|
+
*/
|
|
24
|
+
readonly commandDialect?: ShellDialect
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
/**
|
|
@@ -50,15 +57,27 @@ export function describeRule(rule: AuthorizationRule): string {
|
|
|
50
57
|
// Names the argument, not just the pattern. That is what tells a
|
|
51
58
|
// model whether a different value could get through — which is the
|
|
52
59
|
// difference between rewording once and rewording forever.
|
|
53
|
-
const verb =
|
|
60
|
+
const verb =
|
|
61
|
+
rule.decision === 'deny'
|
|
62
|
+
? 'denied'
|
|
63
|
+
: rule.decision === 'review'
|
|
64
|
+
? 'sent for review'
|
|
65
|
+
: 'allowed'
|
|
54
66
|
return `${verb} because the \`${rule.argument}\` argument matched ${rule.pattern} (this rule applies to ${rule.toolNames.join(', ')})`
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
case 'custom_pattern': {
|
|
58
70
|
const where = rule.target === 'both' ? 'name or arguments' : rule.target
|
|
59
|
-
const verb =
|
|
71
|
+
const verb =
|
|
72
|
+
rule.decision === 'deny'
|
|
73
|
+
? 'denied'
|
|
74
|
+
: rule.decision === 'review'
|
|
75
|
+
? 'sent for review'
|
|
76
|
+
: 'allowed'
|
|
60
77
|
return `${verb} by a pattern rule matching the ${where}: ${rule.pattern}`
|
|
61
78
|
}
|
|
79
|
+
case 'predicate':
|
|
80
|
+
return rule.description
|
|
62
81
|
default: {
|
|
63
82
|
const exhaustive: never = rule
|
|
64
83
|
return `matched an unrecognised rule: ${JSON.stringify(exhaustive)}`
|
|
@@ -206,6 +225,7 @@ export class AuthorizationGate {
|
|
|
206
225
|
ctx.toolDef,
|
|
207
226
|
this.compiledPatterns.get(i),
|
|
208
227
|
this.nameSets.get(i),
|
|
228
|
+
ctx.commandDialect !== undefined ? { commandDialect: ctx.commandDialect } : {},
|
|
209
229
|
)
|
|
210
230
|
|
|
211
231
|
if (decision !== null) {
|