@namzu/sdk 44.3.0 → 45.0.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.
Files changed (116) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/authorization/command-line.d.ts +66 -19
  3. package/dist/authorization/command-line.d.ts.map +1 -1
  4. package/dist/authorization/command-line.js +130 -270
  5. package/dist/authorization/command-line.js.map +1 -1
  6. package/dist/authorization/gate.d.ts +7 -0
  7. package/dist/authorization/gate.d.ts.map +1 -1
  8. package/dist/authorization/gate.js +1 -1
  9. package/dist/authorization/gate.js.map +1 -1
  10. package/dist/authorization/rules.d.ts +10 -1
  11. package/dist/authorization/rules.d.ts.map +1 -1
  12. package/dist/authorization/rules.js +21 -5
  13. package/dist/authorization/rules.js.map +1 -1
  14. package/dist/authorization/shell-lexer.d.ts +138 -0
  15. package/dist/authorization/shell-lexer.d.ts.map +1 -0
  16. package/dist/authorization/shell-lexer.js +2143 -0
  17. package/dist/authorization/shell-lexer.js.map +1 -0
  18. package/dist/authorization/skill-grant.d.ts +182 -0
  19. package/dist/authorization/skill-grant.d.ts.map +1 -0
  20. package/dist/authorization/skill-grant.js +314 -0
  21. package/dist/authorization/skill-grant.js.map +1 -0
  22. package/dist/persona/assembler.d.ts.map +1 -1
  23. package/dist/persona/assembler.js +5 -2
  24. package/dist/persona/assembler.js.map +1 -1
  25. package/dist/public-runtime.d.ts +1 -0
  26. package/dist/public-runtime.d.ts.map +1 -1
  27. package/dist/public-runtime.js +4 -0
  28. package/dist/public-runtime.js.map +1 -1
  29. package/dist/public-tools.d.ts.map +1 -1
  30. package/dist/public-tools.js +2 -1
  31. package/dist/public-tools.js.map +1 -1
  32. package/dist/public-types.d.ts +3 -1
  33. package/dist/public-types.d.ts.map +1 -1
  34. package/dist/runtime/jobs/registry.d.ts +2 -2
  35. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  36. package/dist/runtime/jobs/registry.js +6 -2
  37. package/dist/runtime/jobs/registry.js.map +1 -1
  38. package/dist/runtime/query/executor.d.ts +34 -40
  39. package/dist/runtime/query/executor.d.ts.map +1 -1
  40. package/dist/runtime/query/executor.js +81 -51
  41. package/dist/runtime/query/executor.js.map +1 -1
  42. package/dist/runtime/query/index.d.ts.map +1 -1
  43. package/dist/runtime/query/index.js +7 -0
  44. package/dist/runtime/query/index.js.map +1 -1
  45. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  46. package/dist/runtime/query/iteration/index.js +6 -0
  47. package/dist/runtime/query/iteration/index.js.map +1 -1
  48. package/dist/runtime/query/iteration/phases/context.d.ts +7 -0
  49. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  50. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  51. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  52. package/dist/runtime/query/iteration/phases/tool-review.js +48 -0
  53. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  54. package/dist/runtime/query/review-policy.d.ts +11 -0
  55. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  56. package/dist/runtime/query/review-policy.js +32 -0
  57. package/dist/runtime/query/review-policy.js.map +1 -1
  58. package/dist/runtime/query/tooling.d.ts +3 -0
  59. package/dist/runtime/query/tooling.d.ts.map +1 -1
  60. package/dist/runtime/query/tooling.js +1 -0
  61. package/dist/runtime/query/tooling.js.map +1 -1
  62. package/dist/skills/loader.d.ts +8 -0
  63. package/dist/skills/loader.d.ts.map +1 -1
  64. package/dist/skills/loader.js +7 -1
  65. package/dist/skills/loader.js.map +1 -1
  66. package/dist/tools/builtins/bash.d.ts.map +1 -1
  67. package/dist/tools/builtins/bash.js +18 -6
  68. package/dist/tools/builtins/bash.js.map +1 -1
  69. package/dist/tools/builtins/skill.d.ts +2 -9
  70. package/dist/tools/builtins/skill.d.ts.map +1 -1
  71. package/dist/tools/builtins/skill.js +59 -51
  72. package/dist/tools/builtins/skill.js.map +1 -1
  73. package/dist/tools/command-shell.d.ts +90 -0
  74. package/dist/tools/command-shell.d.ts.map +1 -0
  75. package/dist/tools/command-shell.js +129 -0
  76. package/dist/tools/command-shell.js.map +1 -0
  77. package/dist/tools/defineTool.d.ts +11 -0
  78. package/dist/tools/defineTool.d.ts.map +1 -1
  79. package/dist/tools/defineTool.js +29 -1
  80. package/dist/tools/defineTool.js.map +1 -1
  81. package/dist/types/hitl/index.d.ts +23 -0
  82. package/dist/types/hitl/index.d.ts.map +1 -1
  83. package/dist/types/hitl/index.js.map +1 -1
  84. package/dist/types/tool/index.d.ts +67 -5
  85. package/dist/types/tool/index.d.ts.map +1 -1
  86. package/dist/types/tool/index.js.map +1 -1
  87. package/dist/utils/frontmatter.d.ts +17 -1
  88. package/dist/utils/frontmatter.d.ts.map +1 -1
  89. package/dist/utils/frontmatter.js +32 -2
  90. package/dist/utils/frontmatter.js.map +1 -1
  91. package/package.json +1 -1
  92. package/src/authorization/command-line.ts +148 -293
  93. package/src/authorization/gate.ts +8 -0
  94. package/src/authorization/rules.ts +33 -4
  95. package/src/authorization/shell-lexer.ts +2319 -0
  96. package/src/authorization/skill-grant.ts +400 -0
  97. package/src/persona/assembler.ts +5 -2
  98. package/src/public-runtime.ts +9 -0
  99. package/src/public-tools.ts +2 -1
  100. package/src/public-types.ts +7 -0
  101. package/src/runtime/jobs/registry.ts +19 -11
  102. package/src/runtime/query/executor.ts +99 -55
  103. package/src/runtime/query/index.ts +7 -0
  104. package/src/runtime/query/iteration/index.ts +5 -0
  105. package/src/runtime/query/iteration/phases/context.ts +7 -0
  106. package/src/runtime/query/iteration/phases/tool-review.ts +45 -0
  107. package/src/runtime/query/review-policy.ts +53 -0
  108. package/src/runtime/query/tooling.ts +4 -0
  109. package/src/skills/loader.ts +8 -1
  110. package/src/tools/builtins/bash.ts +24 -6
  111. package/src/tools/builtins/skill.ts +74 -53
  112. package/src/tools/command-shell.ts +166 -0
  113. package/src/tools/defineTool.ts +33 -1
  114. package/src/types/hitl/index.ts +21 -0
  115. package/src/types/tool/index.ts +67 -5
  116. package/src/utils/frontmatter.ts +52 -2
@@ -59,6 +59,22 @@ export type FrontmatterValue = {
59
59
  readonly kind: 'mapping';
60
60
  readonly entries: Readonly<Record<string, string>>;
61
61
  };
62
+ /** What a caller lets {@link parseFrontmatter} accept beyond flat scalars. */
63
+ export interface FrontmatterOptions {
64
+ /**
65
+ * Keys that may be written as a YAML list — a block sequence (`- item`
66
+ * lines) or a flow sequence (`[a, b]`) — and come back as ONE scalar, the
67
+ * items joined with `", "`.
68
+ *
69
+ * Opt-in per key, because a list is refused everywhere else for a reason:
70
+ * a key the caller reads as a scalar would otherwise come back absent. A
71
+ * caller names a key here only when a comma-separated scalar is already
72
+ * a spelling it reads, so the joined value means what the list did.
73
+ * `allowed-tools` is the case: the Agent Skills format writes it either
74
+ * way.
75
+ */
76
+ readonly lists?: readonly string[];
77
+ }
62
78
  export interface ParsedFrontmatter {
63
79
  /**
64
80
  * Every top-level key, in the order the file declared it.
@@ -86,5 +102,5 @@ export interface ParsedFrontmatter {
86
102
  * does not implement. It never returns a partial or empty result to stand in
87
103
  * for a file it could not read.
88
104
  */
89
- export declare function parseFrontmatter(raw: string, source: string): ParsedFrontmatter;
105
+ export declare function parseFrontmatter(raw: string, source: string, options?: FrontmatterOptions): ParsedFrontmatter;
90
106
  //# sourceMappingURL=frontmatter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../src/utils/frontmatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AA8DH;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,gBAAgB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,CAAA;AAEnF,MAAM,WAAW,iBAAiB;IACjC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAA;IAE3D,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAoG/E"}
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../src/utils/frontmatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AA8DH;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,gBAAgB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,CAAA;AAEnF,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IAClC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAClC;AAED,MAAM,WAAW,iBAAiB;IACjC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAA;IAE3D,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC/B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,kBAAuB,GAC9B,iBAAiB,CAiInB"}
@@ -106,7 +106,8 @@ const UNSUPPORTED_YAML = [
106
106
  * does not implement. It never returns a partial or empty result to stand in
107
107
  * for a file it could not read.
108
108
  */
109
- export function parseFrontmatter(raw, source) {
109
+ export function parseFrontmatter(raw, source, options = {}) {
110
+ const listKeys = new Set(options.lists ?? []);
110
111
  const trimmed = raw.trimStart();
111
112
  if (!trimmed.startsWith(FRONTMATTER_DELIMITER)) {
112
113
  throw new Error(`${source} has no YAML frontmatter`);
@@ -130,6 +131,7 @@ export function parseFrontmatter(raw, source) {
130
131
  // round trip is safe at both ends.
131
132
  const data = new Map();
132
133
  const blocks = new Map();
134
+ const lists = new Map();
133
135
  let currentKey;
134
136
  for (const line of frontmatterRaw.split(LINE_SPLIT)) {
135
137
  if (!line.trim() || line.trimStart().startsWith('#'))
@@ -137,7 +139,8 @@ export function parseFrontmatter(raw, source) {
137
139
  if (/^\s/.test(line)) {
138
140
  if (!currentKey)
139
141
  continue;
140
- // A block sequence item. Refused, not skipped.
142
+ // A block sequence item. Read as a list when the caller named this
143
+ // key in `options.lists`; refused, not skipped, everywhere else.
141
144
  //
142
145
  // These lines carry no `:`, so the `continue` below used to drop them
143
146
  // and the key — having no scalar value and no mapping entries — came
@@ -150,6 +153,18 @@ export function parseFrontmatter(raw, source) {
150
153
  // and silently did not get it is indistinguishable from one that never
151
154
  // asked. A capability quietly not granted is the worst thing this
152
155
  // reader can produce.
156
+ if (listKeys.has(currentKey) && !data.has(currentKey) && /^\s*-(\s|$)/.test(line)) {
157
+ const item = normalizeScalar(line.replace(/^\s*-/, ''));
158
+ if (item) {
159
+ let list = lists.get(currentKey);
160
+ if (!list) {
161
+ list = [];
162
+ lists.set(currentKey, list);
163
+ }
164
+ list.push(item);
165
+ }
166
+ continue;
167
+ }
153
168
  if (/^\s*-\s/.test(line)) {
154
169
  throw new Error(`${source}: "${currentKey}" uses a block sequence (a "- " list), which this reader does not support. Write it as a single-line value instead. Refusing rather than reading "${currentKey}" as absent, which is what silently dropping the list would mean.`);
155
170
  }
@@ -173,6 +188,17 @@ export function parseFrontmatter(raw, source) {
173
188
  continue;
174
189
  const key = line.slice(0, colonIdx).trim();
175
190
  const value = normalizeScalar(line.slice(colonIdx + 1));
191
+ if (listKeys.has(key) && /^\[.*\]$/.test(value)) {
192
+ currentKey = key;
193
+ const items = value
194
+ .slice(1, -1)
195
+ .split(',')
196
+ .map(normalizeScalar)
197
+ .filter((item) => item.length > 0);
198
+ // `[]` is a list the author declared empty, which is not absence.
199
+ data.set(key, items.join(', '));
200
+ continue;
201
+ }
176
202
  assertReadableScalar(key, value, source);
177
203
  currentKey = key;
178
204
  if (value)
@@ -182,6 +208,10 @@ export function parseFrontmatter(raw, source) {
182
208
  // that — so refusing here is what makes the illegal state unrepresentable
183
209
  // in the returned type rather than merely undocumented. The alternative,
184
210
  // picking a precedence, would silently drop half of what the author wrote.
211
+ for (const [key, items] of lists) {
212
+ if (!data.has(key))
213
+ data.set(key, items.join(', '));
214
+ }
185
215
  for (const key of blocks.keys()) {
186
216
  if (!data.has(key))
187
217
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../../src/utils/frontmatter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../../src/utils/frontmatter.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namzu/sdk",
3
- "version": "44.3.0",
3
+ "version": "45.0.0",
4
4
  "description": "Open-source AI agent SDK with a built-in runtime. Nothing between you and your agents.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "engines": {
@@ -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. One prohibited command poisons
36
- * the line it rides on.
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 runtime. No
49
- * decomposition of the source can be a decomposition of what ran, so the line
50
- * is marked opaque and `allow` declines it. `deny` still tests what is visible,
51
- * because a deny that matches too much costs a prompt and a deny that matches
52
- * too little costs the thing it was written to prevent.
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 with no chain operator, no nested shell and nothing opaque comes back
57
- * as itself, byte for byte. That keeps every rule about a non-command argument
58
- * — a path, a number, a URL — behaving exactly as it did, and confines this
59
- * machinery to the case that motivated it.
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`, a command read from a
62
- * file, and a shell invoked through an interpreter it does not recognise all
63
- * pass through as ordinary text. Each of those either denies as before or, for
64
- * an allow rule, fails to match every segment and so declines. The failure mode
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 source order. Never empty: a line that
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
- * Depth and width limits.
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
- * Walk the line once, quote-aware, cutting at every top-level separator.
132
- *
133
- * Quote tracking is the whole reason this is not a `String.split`: `echo "a &&
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 split(command: string, state: WalkState, depth: number): string[] {
139
- const segments: string[] = []
140
- let current = ''
141
- let quote: "'" | '"' | null = null
142
-
143
- const cut = (): void => {
144
- const trimmed = trimSegment(current)
145
- current = ''
146
- if (trimmed === '') return
147
- for (const piece of expand(trimmed, state, depth)) segments.push(piece)
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
- // An unterminated quote means the line does not parse. Whatever it runs is
202
- // not what this walk saw, so the caller must not treat the result as a
203
- // complete account.
204
- if (quote !== null) state.opaque = true
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
- cut()
207
- return segments
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
- * Length of the separator starting at `index`, or 0.
212
- *
213
- * The redirection cases are why this is a function. `2>&1` and `&>log` contain
214
- * `&` and are not separators; splitting there would manufacture a segment named
215
- * `1`, which no allow rule matches, and a command that redirects its output
216
- * would stop being approvable for a reason nobody could see.
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 separatorAt(command: string, index: number): number {
219
- const char = command[index]
220
- const next = command[index + 1]
221
-
222
- if (char === '\n') return 1
223
- if (char === ';') return next === ';' ? 2 : 1
224
- if (char === '&') {
225
- if (next === '&') return 2
226
- if (next === '>') return 0
227
- if (command[index - 1] === '>') return 0
228
- return 1
229
- }
230
- if (char === '|') {
231
- if (next === '|') return 2
232
- // `|&` pipes stderr as well; still a pipe, and both sides still run.
233
- if (next === '&') return 2
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 0
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
- * Strip the grouping punctuation a split leaves behind.
251
- *
252
- * `(cd build && make)` cuts into `(cd build` and `make)`. Leaving the bracket on
253
- * would stop an allow rule matching a command it names, and — worse — stop a
254
- * deny rule matching one, since `^make` does not match `make)`.
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 trimSegment(segment: string): string {
257
- return segment
258
- .trim()
259
- .replace(/^[({\s]+/, '')
260
- .replace(/[)}\s]+$/, '')
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
- * Turn one segment into the commands it stands for.
265
- *
266
- * A shell invoked with `-c` carries a whole second command line in an argument,
267
- * and that argument is where the smuggling this module exists for is easiest:
268
- * `bash -c "git push"` contains no separator at all, so nothing above this
269
- * function would have looked inside it.
270
- *
271
- * The outer segment is kept alongside the inner ones. A rule that denies the
272
- * interpreter itself must still fire, and for `allow` the extra segment only
273
- * makes the requirement stricter — which is the safe direction.
274
- */
275
- function expand(segment: string, state: WalkState, depth: number): string[] {
276
- const words = tokenize(segment)
277
- const head = words[0]
278
- if (head === undefined) return [segment]
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
- * Split a segment into words, removing one layer of quoting.
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
- function tokenize(segment: string): Word[] {
328
- const words: Word[] = []
329
- let current = ''
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
- if (quote === '"') {
361
- if (char === '"') quote = null
362
- else current += char
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
- push()
384
- return words
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
  }