wave-agent-sdk 1.1.4 → 1.2.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 (89) hide show
  1. package/dist/agent.d.ts +72 -24
  2. package/dist/agent.js +123 -31
  3. package/dist/builtin/plugins.js +17 -2
  4. package/dist/builtin/skills/settings.js +6 -8
  5. package/dist/core/plugin.d.ts +4 -0
  6. package/dist/core/plugin.js +7 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +2 -1
  9. package/dist/managers/aiManager.d.ts +0 -24
  10. package/dist/managers/aiManager.js +23 -181
  11. package/dist/managers/bashModeManager.d.ts +33 -0
  12. package/dist/managers/bashModeManager.js +110 -0
  13. package/dist/managers/hookManager.d.ts +5 -0
  14. package/dist/managers/hookManager.js +7 -0
  15. package/dist/managers/liveConfigManager.js +3 -3
  16. package/dist/managers/mcpManager.d.ts +23 -0
  17. package/dist/managers/mcpManager.js +162 -14
  18. package/dist/managers/messageManager.d.ts +12 -13
  19. package/dist/managers/messageManager.js +63 -60
  20. package/dist/managers/permissionManager.d.ts +29 -0
  21. package/dist/managers/permissionManager.js +158 -70
  22. package/dist/managers/planManager.d.ts +9 -0
  23. package/dist/managers/planManager.js +19 -1
  24. package/dist/managers/skillManager.d.ts +31 -0
  25. package/dist/managers/skillManager.js +122 -12
  26. package/dist/managers/slashCommandManager.js +9 -28
  27. package/dist/managers/subagentManager.d.ts +7 -0
  28. package/dist/managers/subagentManager.js +61 -9
  29. package/dist/managers/workflowManager.js +6 -0
  30. package/dist/prompts/index.d.ts +0 -1
  31. package/dist/prompts/index.js +0 -4
  32. package/dist/services/MarketplaceService.js +36 -14
  33. package/dist/services/configurationService.d.ts +34 -2
  34. package/dist/services/configurationService.js +123 -16
  35. package/dist/services/initializationService.js +2 -2
  36. package/dist/services/jsonlHandler.d.ts +14 -0
  37. package/dist/services/jsonlHandler.js +44 -1
  38. package/dist/services/memory.d.ts +14 -0
  39. package/dist/services/memory.js +33 -0
  40. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  41. package/dist/services/officialMarketplaceMirror.js +289 -0
  42. package/dist/services/remoteSettingsService.js +4 -4
  43. package/dist/services/session.js +30 -13
  44. package/dist/services/worktreeHooks.js +6 -1
  45. package/dist/stdio/index.d.ts +10 -0
  46. package/dist/stdio/index.js +10 -0
  47. package/dist/stdio/notificationRouter.d.ts +38 -0
  48. package/dist/stdio/notificationRouter.js +96 -0
  49. package/dist/stdio/rpcClient.d.ts +18 -0
  50. package/dist/stdio/rpcClient.js +10 -0
  51. package/dist/stdio/stdioAgent.d.ts +222 -0
  52. package/dist/stdio/stdioAgent.js +341 -0
  53. package/dist/tools/bashTool.js +2 -0
  54. package/dist/tools/exitPlanMode.js +10 -2
  55. package/dist/types/agent.d.ts +8 -0
  56. package/dist/types/commands.d.ts +7 -0
  57. package/dist/types/configuration.d.ts +6 -1
  58. package/dist/types/hooks.d.ts +1 -0
  59. package/dist/types/hooks.js +19 -0
  60. package/dist/types/mcp.d.ts +3 -0
  61. package/dist/types/messaging.d.ts +1 -8
  62. package/dist/types/skills.d.ts +11 -0
  63. package/dist/utils/bashParser.d.ts +17 -0
  64. package/dist/utils/bashParser.js +72 -0
  65. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  66. package/dist/utils/bashStructure/bashLexer.js +676 -0
  67. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  68. package/dist/utils/bashStructure/bashParser.js +606 -0
  69. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  70. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  71. package/dist/utils/bashStructure/index.d.ts +26 -0
  72. package/dist/utils/bashStructure/index.js +27 -0
  73. package/dist/utils/bashStructure/types.d.ts +62 -0
  74. package/dist/utils/bashStructure/types.js +47 -0
  75. package/dist/utils/container.d.ts +6 -0
  76. package/dist/utils/container.js +9 -0
  77. package/dist/utils/containerSetup.d.ts +11 -1
  78. package/dist/utils/containerSetup.js +26 -11
  79. package/dist/utils/fileUtils.d.ts +11 -0
  80. package/dist/utils/fileUtils.js +37 -0
  81. package/dist/utils/messageOperations.d.ts +0 -18
  82. package/dist/utils/messageOperations.js +0 -62
  83. package/dist/utils/subagentParser.js +9 -2
  84. package/dist/utils/tokenCalculation.js +0 -8
  85. package/dist/utils/worktreeUtils.d.ts +2 -1
  86. package/dist/utils/worktreeUtils.js +64 -34
  87. package/package.json +6 -1
  88. package/dist/managers/bangManager.d.ts +0 -26
  89. package/dist/managers/bangManager.js +0 -78
@@ -0,0 +1,477 @@
1
+ /**
2
+ * Semantic layer for the structure-aware bash parser.
3
+ *
4
+ * Walks the parse tree from `bashParser.ts` and expands every executable unit
5
+ * into a flat list of "leaf" simple commands (BashLeaf). The permission layer
6
+ * (P2) classifies each leaf with the exact rules already used for plain
7
+ * commands (read-only sets, path Safe Zone checks, allow rules).
8
+ *
9
+ * Semantics mirrors Claude Code's collectCommands / resolveSimpleExpansion:
10
+ *
11
+ * - Compound commands contribute the leaves of *every* branch/iteration body:
12
+ * statically we cannot know which branch runs, so each is checked; an
13
+ * unsafe leaf anywhere forces the whole command to ask.
14
+ * - `$(...)` / backticks are expanded recursively into their own leaves. A
15
+ * substitution that appears inside one of a leaf's own argument words marks
16
+ * that leaf `unsafe` (`command-substitution`) — its expansion is dynamic.
17
+ * - Assignments are tracked in a scope so later leaves in the same sequential
18
+ * chain can resolve bare references (spec: `x=abc; echo $x` — the value is a
19
+ * statically known literal). Anything not literally known stays UNKNOWN.
20
+ * - A bare (unquoted) reference to an UNKNOWN variable marks the leaf unsafe
21
+ * (`bare-variable`): word-splitting/globbing could inject extra arguments.
22
+ * Quoted `"$f"` references are always safe (a single word). `$@`/`$*` are
23
+ * never safe bare *or* quoted — they expand to any number of words.
24
+ * - Arithmetic/brace/process substitutions, heredocs, eval/trap and any parse
25
+ * failure fail closed: the whole parse reports `unsupported` (ask).
26
+ *
27
+ * Scope rules mirror bash execution conservatively:
28
+ * - Sequential `;` / `&&` steps share a scope (assignments propagate).
29
+ * - Fork steps (`||`, `|`, `|&`, `&`) and compound bodies run from a copy, so
30
+ * an assignment on one side is never assumed on the other.
31
+ * - Assignment-prefixed commands (`VAR=x cmd`) keep the *incoming* scope for
32
+ * their own argument expansion (bash expands args before applying prefix).
33
+ */
34
+ import { tokenize, isRedirectWord, redirectOpKind, } from "./bashLexer.js";
35
+ import { parseTokens, } from "./bashParser.js";
36
+ import { SAFE_ENV_VARS, } from "./types.js";
37
+ /** Sentinel: variable value cannot be statically known. */
38
+ const UNKNOWN = "\u0000unknown";
39
+ /** Sentinel: value comes from $@/$* (any number of words). */
40
+ const MULTIWORD = "\u0000multiword";
41
+ /** Sentinel: $?/$$/… special var (dynamic but a single word). */
42
+ const SPECIAL_SINGLE = "\u0000special";
43
+ /** Aborts the walk with a fail-closed reason. */
44
+ class Unsupported extends Error {
45
+ constructor(reason, message) {
46
+ super(message ?? reason);
47
+ this.reason = reason;
48
+ }
49
+ }
50
+ /** Bare-variable values containing these can word-split / glob. */
51
+ const BARE_UNSAFE_RE = /[ \t\n*?[]/;
52
+ export class BashSemantics {
53
+ constructor() {
54
+ this.leaves = [];
55
+ }
56
+ /** Parse and expand a full command string. */
57
+ static analyze(command) {
58
+ const s = new BashSemantics();
59
+ return s.run(command);
60
+ }
61
+ run(command) {
62
+ if (command.trim() === "")
63
+ return { status: "ok", leaves: [] };
64
+ const tokens = tokenize(command);
65
+ if (tokens === null)
66
+ return this.unsupportedFor(command);
67
+ const program = parseTokens(tokens);
68
+ if (program === null)
69
+ return { status: "unsupported", reason: "syntax" };
70
+ try {
71
+ this.walkProgram(program, new Map());
72
+ }
73
+ catch (err) {
74
+ if (err instanceof Unsupported) {
75
+ return {
76
+ status: "unsupported",
77
+ reason: err.reason,
78
+ message: err.message,
79
+ };
80
+ }
81
+ throw err;
82
+ }
83
+ return { status: "ok", leaves: this.leaves };
84
+ }
85
+ /** Best-effort reason classification when the lexer rejects the input. */
86
+ unsupportedFor(command) {
87
+ if (/<<-?[A-Za-z0-9_]/.test(command)) {
88
+ return { status: "unsupported", reason: "heredoc" };
89
+ }
90
+ if (command.includes("$((`")) {
91
+ return { status: "unsupported", reason: "syntax" };
92
+ }
93
+ if (command.includes("$((")) {
94
+ return { status: "unsupported", reason: "arithmetic-expansion" };
95
+ }
96
+ if (/\{[^}]*[,.][^}]*\}/.test(command)) {
97
+ return { status: "unsupported", reason: "brace-expansion" };
98
+ }
99
+ return { status: "unsupported", reason: "syntax" };
100
+ }
101
+ // ── Program / statement walking ───────────────────────────────────────────
102
+ walkProgram(stmts, scope) {
103
+ let current = scope;
104
+ for (const stmt of stmts) {
105
+ // Top-level statements separated by ; / newline run sequentially in the
106
+ // same shell, so assignments propagate.
107
+ current = this.walkStmt(stmt, current, current);
108
+ }
109
+ }
110
+ walkStmt(stmt, scope, entry) {
111
+ switch (stmt.kind) {
112
+ case "cmd":
113
+ return this.walkCmd(stmt, scope);
114
+ case "for":
115
+ return this.walkFor(stmt, scope);
116
+ case "while":
117
+ return this.walkWhile(stmt, scope);
118
+ case "if":
119
+ return this.walkIf(stmt, scope);
120
+ case "case":
121
+ return this.walkCase(stmt, scope);
122
+ case "function":
123
+ throw new Unsupported("unknown-command", "function definitions cannot be statically expanded");
124
+ case "subshell": {
125
+ // `( a; b )` — runs in a subshell; assignments never escape.
126
+ this.walkProgram(stmt.body, new Map());
127
+ return scope;
128
+ }
129
+ case "group": {
130
+ // `{ a; b; }` — runs in the current shell sequentially.
131
+ this.walkProgram(stmt.body, scope);
132
+ return scope;
133
+ }
134
+ case "list": {
135
+ let working = copyScope(scope);
136
+ const entryCopy = copyScope(entry);
137
+ for (const step of stmt.steps) {
138
+ if (step.forkAfter)
139
+ working = copyScope(entryCopy);
140
+ for (const node of step.nodes) {
141
+ working = this.walkStmt(node, working, entryCopy);
142
+ }
143
+ }
144
+ return working;
145
+ }
146
+ case "redirects": {
147
+ const innerScope = this.walkStmt(stmt.target, scope, entry);
148
+ this.mergeRedirectsIntoLastLeaf(stmt.redirects);
149
+ return innerScope;
150
+ }
151
+ default: {
152
+ const _never = stmt;
153
+ void _never;
154
+ return scope;
155
+ }
156
+ }
157
+ }
158
+ /** Bind compound-level redirects to the last leaf produced by the target. */
159
+ mergeRedirectsIntoLastLeaf(redirects) {
160
+ const redirText = redirects.map((w) => w.raw).join(" ");
161
+ if (this.leaves.length === 0) {
162
+ // No leaf (empty body): still surface the redirect so text-level write
163
+ // detection can reject `> out`.
164
+ this.leaves.push({
165
+ argv: [],
166
+ command: "",
167
+ text: redirText,
168
+ unsafe: false,
169
+ reasons: [],
170
+ });
171
+ return;
172
+ }
173
+ const last = this.leaves[this.leaves.length - 1];
174
+ last.text = `${last.text} ${redirText}`;
175
+ }
176
+ // ── Statement implementations ─────────────────────────────────────────────
177
+ walkCmd(node, scope) {
178
+ const words = node.words;
179
+ // Split leading assignment prefix (`VAR=x cmd …`) from the command words.
180
+ let i = 0;
181
+ while (i < words.length && parseAssignment(words[i]) !== null)
182
+ i++;
183
+ const assignments = words.slice(0, i);
184
+ const commandWords = words.slice(i);
185
+ if (commandWords.length === 0) {
186
+ // Assignment-only statement: `x=$(cmd)`. Command substitutions execute;
187
+ // the value (unknown) propagates to the sequential scope.
188
+ for (const w of assignments) {
189
+ const a = parseAssignment(w);
190
+ if (a)
191
+ this.applyAssignment(a, scope);
192
+ }
193
+ return scope;
194
+ }
195
+ // Prefix assignments apply only to the command's own environment, and bash
196
+ // expands the command's words *before* applying them — expand with the
197
+ // incoming scope. They do NOT leak to the sequential scope (`FOO=x echo hi`
198
+ // leaves FOO unset afterwards).
199
+ const leaf = this.buildLeaf(commandWords, scope);
200
+ if (leaf)
201
+ this.leaves.push(leaf);
202
+ return scope;
203
+ }
204
+ walkFor(node, scope) {
205
+ // Iteration words: command substitutions in them execute (their output is
206
+ // the data iterated over) — collect those leaves. The loop variable value
207
+ // is unknown for every iteration.
208
+ for (const w of node.iter)
209
+ this.collectWordCmdSubs(w);
210
+ const bodyScope = copyScope(scope);
211
+ bodyScope.set(node.varName, UNKNOWN);
212
+ this.walkProgram(node.body, bodyScope);
213
+ // Assignments inside the body may not run (zero iterations): don't leak.
214
+ return scope;
215
+ }
216
+ walkWhile(node, scope) {
217
+ const condScope = copyScope(scope);
218
+ this.walkProgram(node.cond, condScope);
219
+ // `while read var` captures stdin into var → unknown in the body.
220
+ for (const stmt of node.cond)
221
+ this.collectReadCaptures(stmt, condScope);
222
+ const bodyScope = copyScope(condScope);
223
+ this.walkProgram(node.body, bodyScope);
224
+ return scope;
225
+ }
226
+ walkIf(node, scope) {
227
+ for (const clause of node.clauses) {
228
+ this.walkProgram(clause.cond, copyScope(scope));
229
+ this.walkProgram(clause.body, copyScope(scope));
230
+ }
231
+ if (node.elseBody)
232
+ this.walkProgram(node.elseBody, copyScope(scope));
233
+ return scope;
234
+ }
235
+ walkCase(node, scope) {
236
+ this.collectWordCmdSubs(node.word);
237
+ for (const arm of node.arms) {
238
+ for (const pat of arm.patterns)
239
+ this.collectWordCmdSubs(pat);
240
+ this.walkProgram(arm.body, copyScope(scope));
241
+ }
242
+ return scope;
243
+ }
244
+ // ── Leaf construction ─────────────────────────────────────────────────────
245
+ /**
246
+ * Build a BashLeaf from a command's words. Emits leaves for command
247
+ * substitutions embedded in argument words. Throws Unsupported on
248
+ * fail-closed constructs.
249
+ */
250
+ buildLeaf(words, scope) {
251
+ const argv = [];
252
+ const reasons = [];
253
+ const redirects = [];
254
+ for (let idx = 0; idx < words.length; idx++) {
255
+ const w = words[idx];
256
+ // Redirection operator (lexer emits them as words, optionally with an fd
257
+ // prefix like `2>` or a folded dup target like `2>&1`): keep the operator
258
+ // and (for unfolded forms) its following target word in `text` rather
259
+ // than argv so legacy text checkers (hasWriteRedirections / path zone)
260
+ // still see them.
261
+ if (isRedirectWord(w)) {
262
+ redirects.push(w.raw);
263
+ if (redirectOpKind(w.raw) === "plain") {
264
+ const target = words[idx + 1];
265
+ if (target && !isRedirectWord(target)) {
266
+ redirects.push(target.raw);
267
+ idx++;
268
+ }
269
+ }
270
+ continue;
271
+ }
272
+ this.classifyWordParts(w, scope, reasons);
273
+ argv.push(w.raw);
274
+ }
275
+ if (argv.length === 0) {
276
+ if (redirects.length === 0)
277
+ return null;
278
+ return {
279
+ argv: [],
280
+ command: "",
281
+ text: redirects.join(" "),
282
+ unsafe: false,
283
+ reasons: [],
284
+ };
285
+ }
286
+ const command = commandNameOf(argv[0]);
287
+ if (command === "eval" || command === "trap") {
288
+ throw new Unsupported("eval", `\`${command}\` is dynamic execution`);
289
+ }
290
+ const text = redirects.length
291
+ ? `${argv.join(" ")} ${redirects.join(" ")}`
292
+ : argv.join(" ");
293
+ return {
294
+ argv,
295
+ command,
296
+ text,
297
+ unsafe: reasons.length > 0,
298
+ reasons,
299
+ };
300
+ }
301
+ /**
302
+ * Classify one word's parts for structural safety. Pushes reasons onto
303
+ * `reasons` for unsafe leaf conditions. Throws Unsupported for fail-closed
304
+ * construct kinds.
305
+ */
306
+ classifyWordParts(w, scope, reasons) {
307
+ for (const part of w.parts) {
308
+ switch (part.t) {
309
+ case "lit":
310
+ if (part.q === "none" &&
311
+ part.v.includes("{") &&
312
+ part.v.includes("}") &&
313
+ (part.v.includes(",") || part.v.includes(".."))) {
314
+ throw new Unsupported("brace-expansion", `brace expansion in \`${w.raw}\``);
315
+ }
316
+ break;
317
+ case "var": {
318
+ const value = resolveVar(part.name, scope);
319
+ if (part.q === "double")
320
+ break; // quoted → one word, always safe
321
+ if (SAFE_ENV_VARS.has(part.name))
322
+ break;
323
+ if (value === UNKNOWN || value === MULTIWORD) {
324
+ reasons.push("bare-variable");
325
+ }
326
+ else if (typeof value === "string" && BARE_UNSAFE_RE.test(value)) {
327
+ reasons.push("bare-variable");
328
+ }
329
+ break;
330
+ }
331
+ case "special":
332
+ if (part.name === "@" || part.name === "*") {
333
+ reasons.push("bare-variable");
334
+ }
335
+ break;
336
+ case "cmdsub":
337
+ // Executes inside an argument word → dynamic, ask.
338
+ this.emitSubstitutionLeaves(part.text);
339
+ reasons.push("command-substitution");
340
+ break;
341
+ case "arith":
342
+ throw new Unsupported("arithmetic-expansion", `$((…)) in \`${w.raw}\``);
343
+ case "brace":
344
+ throw new Unsupported("brace-expansion", `parameter/brace expansion in \`${w.raw}\``);
345
+ case "process":
346
+ throw new Unsupported("process-substitution", `process substitution in \`${w.raw}\``);
347
+ }
348
+ }
349
+ }
350
+ // ── Assignments / scope ───────────────────────────────────────────────────
351
+ applyAssignment(a, scope) {
352
+ let value = "";
353
+ for (const part of a.valueParts) {
354
+ switch (part.t) {
355
+ case "lit":
356
+ value += part.v;
357
+ break;
358
+ case "var": {
359
+ const v = resolveVar(part.name, scope);
360
+ if (v === MULTIWORD)
361
+ value += MULTIWORD;
362
+ else
363
+ value += v;
364
+ break;
365
+ }
366
+ case "special":
367
+ value += SPECIAL_SINGLE;
368
+ break;
369
+ case "cmdsub":
370
+ this.emitSubstitutionLeaves(part.text);
371
+ value += UNKNOWN;
372
+ break;
373
+ case "arith":
374
+ throw new Unsupported("arithmetic-expansion", "$((…)) in assignment");
375
+ case "brace":
376
+ throw new Unsupported("brace-expansion", "brace expansion in assignment");
377
+ case "process":
378
+ throw new Unsupported("process-substitution", "process substitution in assignment");
379
+ }
380
+ }
381
+ if (value === "" ||
382
+ value.includes(UNKNOWN) ||
383
+ value.includes(SPECIAL_SINGLE)) {
384
+ scope.set(a.name, UNKNOWN);
385
+ }
386
+ else {
387
+ scope.set(a.name, value);
388
+ }
389
+ }
390
+ // ── Command substitution expansion ───────────────────────────────────────
391
+ /** Collect leaves of command substitutions in a word (iter/case positions). */
392
+ collectWordCmdSubs(w) {
393
+ for (const part of w.parts) {
394
+ if (part.t === "cmdsub")
395
+ this.emitSubstitutionLeaves(part.text);
396
+ }
397
+ }
398
+ /** Recursively parse a $() body and append its leaves. */
399
+ emitSubstitutionLeaves(text) {
400
+ const sub = new BashSemantics();
401
+ const result = sub.run(text);
402
+ if (result.status !== "ok") {
403
+ throw new Unsupported(result.status === "unsupported" ? result.reason : "syntax", `command substitution body \`${text}\``);
404
+ }
405
+ this.leaves.push(...result.leaves);
406
+ }
407
+ /** Mark variables captured by `read` in a condition list as unknown. */
408
+ collectReadCaptures(stmt, scope) {
409
+ switch (stmt.kind) {
410
+ case "cmd": {
411
+ const words = stmt.words;
412
+ if (words.length === 0 || words[0]?.raw !== "read")
413
+ return;
414
+ for (let i = 1; i < words.length; i++) {
415
+ const raw = words[i]?.raw ?? "";
416
+ if (raw.startsWith("-"))
417
+ continue;
418
+ const name = raw.replace(/^["']|["']$/g, "");
419
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
420
+ scope.set(name, UNKNOWN);
421
+ }
422
+ return;
423
+ }
424
+ case "list":
425
+ for (const step of stmt.steps) {
426
+ for (const n of step.nodes)
427
+ this.collectReadCaptures(n, scope);
428
+ }
429
+ return;
430
+ case "redirects":
431
+ this.collectReadCaptures(stmt.target, scope);
432
+ return;
433
+ default:
434
+ return;
435
+ }
436
+ }
437
+ }
438
+ // ── Free helpers ────────────────────────────────────────────────────────────
439
+ function resolveVar(name, scope) {
440
+ return scope.get(name) ?? UNKNOWN;
441
+ }
442
+ function copyScope(scope) {
443
+ return new Map(scope);
444
+ }
445
+ /**
446
+ * A bare command name (first argv word, quotes stripped). If it is still not a
447
+ * plain name (e.g. a variable), the caller treats it as unresolvable.
448
+ */
449
+ function commandNameOf(raw) {
450
+ return raw.replace(/^["']|["']$/g, "");
451
+ }
452
+ /**
453
+ * Detect a leading assignment in a word (`NAME=value`). The name part must be
454
+ * unquoted literal text. Returns the name and the value parts, or null.
455
+ */
456
+ function parseAssignment(w) {
457
+ const parts = w.parts;
458
+ const first = parts[0];
459
+ if (!first || first.t !== "lit" || first.q !== "none")
460
+ return null;
461
+ const v = first.v;
462
+ const eq = v.indexOf("=");
463
+ if (eq <= 0)
464
+ return null;
465
+ const name = v.slice(0, eq);
466
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
467
+ return null;
468
+ const valueParts = [];
469
+ const remainder = v.slice(eq + 1);
470
+ if (remainder.length > 0)
471
+ valueParts.push({ t: "lit", v: remainder, q: "none" });
472
+ for (let i = 1; i < parts.length; i++)
473
+ valueParts.push(parts[i]);
474
+ if (valueParts.length === 0)
475
+ valueParts.push({ t: "lit", v: "", q: "none" });
476
+ return { name, valueParts };
477
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Structure-aware bash parsing: expand compound commands and command
3
+ * substitutions into flat leaf commands for permission classification.
4
+ *
5
+ * Public entry point:
6
+ * parseBashStructure(command) → BashStructureResult
7
+ *
8
+ * { status: "ok", leaves: BashLeaf[] } — every executable command,
9
+ * ready for per-leaf checks
10
+ * { status: "unsupported", reason } — construct could not be
11
+ * statically expanded; caller
12
+ * must ask (fail closed)
13
+ */
14
+ import type { BashStructureResult } from "./types.js";
15
+ /**
16
+ * Parse a bash command string into a flat list of executable leaf commands,
17
+ * expanding shell control structure and command substitutions.
18
+ *
19
+ * The parser is conservative (fail-closed): any construct that cannot be
20
+ * statically reduced to leaves (heredocs, arithmetic/brace/process
21
+ * substitution, eval/trap, parse failures, parser limits) returns
22
+ * `unsupported` and the permission layer must ask instead of auto-allowing.
23
+ */
24
+ export declare function parseBashStructure(command: string): BashStructureResult;
25
+ export type { BashStructureResult, BashStructureOk, BashStructureUnsupported, BashLeaf, BashLeafReason, BashUnsupportedReason, } from "./types.js";
26
+ export { SAFE_ENV_VARS, SAFE_SHELL_BUILTINS } from "./types.js";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Structure-aware bash parsing: expand compound commands and command
3
+ * substitutions into flat leaf commands for permission classification.
4
+ *
5
+ * Public entry point:
6
+ * parseBashStructure(command) → BashStructureResult
7
+ *
8
+ * { status: "ok", leaves: BashLeaf[] } — every executable command,
9
+ * ready for per-leaf checks
10
+ * { status: "unsupported", reason } — construct could not be
11
+ * statically expanded; caller
12
+ * must ask (fail closed)
13
+ */
14
+ import { BashSemantics } from "./bashSemantics.js";
15
+ /**
16
+ * Parse a bash command string into a flat list of executable leaf commands,
17
+ * expanding shell control structure and command substitutions.
18
+ *
19
+ * The parser is conservative (fail-closed): any construct that cannot be
20
+ * statically reduced to leaves (heredocs, arithmetic/brace/process
21
+ * substitution, eval/trap, parse failures, parser limits) returns
22
+ * `unsupported` and the permission layer must ask instead of auto-allowing.
23
+ */
24
+ export function parseBashStructure(command) {
25
+ return BashSemantics.analyze(command);
26
+ }
27
+ export { SAFE_ENV_VARS, SAFE_SHELL_BUILTINS } from "./types.js";
@@ -0,0 +1,62 @@
1
+ /** A structural reason why an individual leaf may not be auto-allowed. */
2
+ export type BashLeafReason =
3
+ /** An unquoted variable (loop var, read capture, substitution capture,
4
+ * positional param, or any var without a literal value) appears in a bare
5
+ * argument position. Its value is unknown: word-splitting / globbing may
6
+ * inject extra arguments. Quoted occurrences ("$f") are fine. */
7
+ "bare-variable"
8
+ /** The leaf's raw text still contains a $( ) / backtick command
9
+ * substitution (used when the substitution is itself one of the leaf's
10
+ * arguments, so the leaf text cannot be validated as a plain command). */
11
+ | "command-substitution";
12
+ /** Why a whole command cannot be statically expanded (must ask). */
13
+ export type BashUnsupportedReason = "process-substitution" | "arithmetic-expansion" | "brace-expansion" | "heredoc" | "eval" | "syntax" | "too-complex" | "unknown-command";
14
+ /** A single expanded executable command. */
15
+ export interface BashLeaf {
16
+ /** Expanded command words. Quotes are preserved as written so the raw text
17
+ * can still be run through the legacy text-based checkers. Variables are
18
+ * kept literally ($f), not resolved. */
19
+ argv: string[];
20
+ /** The command name as written (argv[0] after any leading assignments are
21
+ * stripped), or "" if the leaf is an assignment-only statement. */
22
+ command: string;
23
+ /** Reconstructed command text (words joined by single spaces). */
24
+ text: string;
25
+ /** true when this leaf must never be auto-allowed for structural reasons
26
+ * (bare unknown variables, …); the permission layer must ask. */
27
+ unsafe: boolean;
28
+ /** Structural reasons (empty when unsafe is false). */
29
+ reasons: BashLeafReason[];
30
+ }
31
+ /** Successful parse: a flat list of leaves to classify. */
32
+ export interface BashStructureOk {
33
+ status: "ok";
34
+ leaves: BashLeaf[];
35
+ }
36
+ /** Failed parse: the command cannot be statically expanded, ask. */
37
+ export interface BashStructureUnsupported {
38
+ status: "unsupported";
39
+ reason: BashUnsupportedReason;
40
+ /** Optional human-readable detail (e.g. which construct was hit). */
41
+ message?: string;
42
+ }
43
+ export type BashStructureResult = BashStructureOk | BashStructureUnsupported;
44
+ /** A simple command's words, already split out from the token stream. */
45
+ export interface SimpleWord {
46
+ /** The word as written in the source (quotes kept). */
47
+ raw: string;
48
+ }
49
+ /**
50
+ * Variables we consider safe to reference unquoted: their values are either
51
+ * fixed or read-only environment information that cannot turn into attacker
52
+ * controlled word-splitting in the contexts we auto-allow (mirrors CC's
53
+ * safe-env-vars handling). Reading `$PATH` unquoted in `echo $PATH` is fine.
54
+ */
55
+ export declare const SAFE_ENV_VARS: ReadonlySet<string>;
56
+ /**
57
+ * Shell builtins that only read stdin into shell variables / print, and do not
58
+ * touch the filesystem. Auto-allowed like READ_ONLY_COMMANDS when they carry no
59
+ * write redirections (spec: `while read -r line; do …` must auto-allow).
60
+ */
61
+ export declare const SAFE_SHELL_BUILTINS: ReadonlySet<string>;
62
+ export declare const ALL_SAFE_COMMANDS: ReadonlySet<string>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared types for the structure-aware bash parser (bashStructure).
3
+ *
4
+ * The parser expands compound commands (for/while/until/if/case/select/
5
+ * function) and command substitutions ($( ) / backticks) into a flat list of
6
+ * "leaf" simple commands. Each leaf is a single executable command that the
7
+ * permission layer can classify with the same rules used for a plain command.
8
+ *
9
+ * Any construct that cannot be statically reconstructed (heredoc bodies,
10
+ * arithmetic expansion, brace expansion, process substitution, eval/trap,
11
+ * unterminated quotes, parser limits) fails closed: the whole parse returns
12
+ * `unsupported` and the permission layer must ask instead of auto-allowing.
13
+ *
14
+ * Mirrors Claude Code's ParseForSecurityResult shape:
15
+ * simple → leaves[], unsupported → ask (fail-closed).
16
+ */
17
+ import { READ_ONLY_COMMANDS } from "../bashParser.js";
18
+ /**
19
+ * Variables we consider safe to reference unquoted: their values are either
20
+ * fixed or read-only environment information that cannot turn into attacker
21
+ * controlled word-splitting in the contexts we auto-allow (mirrors CC's
22
+ * safe-env-vars handling). Reading `$PATH` unquoted in `echo $PATH` is fine.
23
+ */
24
+ export const SAFE_ENV_VARS = new Set([
25
+ "HOME",
26
+ "PATH",
27
+ "PWD",
28
+ "USER",
29
+ "LOGNAME",
30
+ "SHELL",
31
+ "TERM",
32
+ "LANG",
33
+ "LC_ALL",
34
+ ]);
35
+ /**
36
+ * Shell builtins that only read stdin into shell variables / print, and do not
37
+ * touch the filesystem. Auto-allowed like READ_ONLY_COMMANDS when they carry no
38
+ * write redirections (spec: `while read -r line; do …` must auto-allow).
39
+ */
40
+ export const SAFE_SHELL_BUILTINS = new Set([
41
+ "read",
42
+ "echo", // echo already in READ_ONLY_COMMANDS; kept here for completeness
43
+ ]);
44
+ export const ALL_SAFE_COMMANDS = new Set([
45
+ ...READ_ONLY_COMMANDS,
46
+ ...SAFE_SHELL_BUILTINS,
47
+ ]);
@@ -27,4 +27,10 @@ export declare class Container {
27
27
  * Check if a service exists
28
28
  */
29
29
  has(token: string): boolean;
30
+ /**
31
+ * Remove all registered services and factories. Call during teardown so the
32
+ * container does not retain references to per-agent services after the
33
+ * agent is destroyed (breaking the agent's object graph).
34
+ */
35
+ clear(): void;
30
36
  }
@@ -52,6 +52,15 @@ export class Container {
52
52
  this.factories.has(token) ||
53
53
  (this.parent ? this.parent.has(token) : false));
54
54
  }
55
+ /**
56
+ * Remove all registered services and factories. Call during teardown so the
57
+ * container does not retain references to per-agent services after the
58
+ * agent is destroyed (breaking the agent's object graph).
59
+ */
60
+ clear() {
61
+ this.services.clear();
62
+ this.factories.clear();
63
+ }
55
64
  }
56
65
  // Example usage for ToolManager:
57
66
  /*