klyro 1.0.0 → 1.0.2

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 (110) hide show
  1. package/dist/agent/anthropic-adapter.d.ts +49 -5
  2. package/dist/agent/anthropic-adapter.js +86 -17
  3. package/dist/agent/capabilities.d.ts +23 -0
  4. package/dist/agent/capabilities.js +53 -6
  5. package/dist/agent/child-worker.d.ts +104 -0
  6. package/dist/agent/child-worker.js +250 -0
  7. package/dist/agent/orchestrator.d.ts +124 -6
  8. package/dist/agent/orchestrator.js +425 -58
  9. package/dist/agent/provider-adapter.d.ts +8 -0
  10. package/dist/agent/provider-adapter.js +12 -3
  11. package/dist/agent/retry.d.ts +1 -1
  12. package/dist/agent/retry.js +54 -12
  13. package/dist/agent/runtime.d.ts +84 -8
  14. package/dist/agent/runtime.js +352 -39
  15. package/dist/agent/stream-budget.d.ts +36 -0
  16. package/dist/agent/stream-budget.js +121 -0
  17. package/dist/agent/worktree-manager.d.ts +74 -0
  18. package/dist/agent/worktree-manager.js +189 -0
  19. package/dist/checkpoints/store.d.ts +9 -0
  20. package/dist/checkpoints/store.js +56 -5
  21. package/dist/cli/auth.js +16 -1
  22. package/dist/cli/commit.d.ts +31 -0
  23. package/dist/cli/commit.js +142 -0
  24. package/dist/cli/config.d.ts +54 -3
  25. package/dist/cli/config.js +146 -3
  26. package/dist/cli/doctor.d.ts +1 -0
  27. package/dist/cli/doctor.js +71 -6
  28. package/dist/cli/eval.d.ts +6 -1
  29. package/dist/cli/eval.js +9 -0
  30. package/dist/cli/hooks.d.ts +47 -0
  31. package/dist/cli/hooks.js +181 -0
  32. package/dist/cli/repl.js +196 -29
  33. package/dist/cli/run.d.ts +13 -11
  34. package/dist/cli/run.js +144 -20
  35. package/dist/cli/update.d.ts +5 -0
  36. package/dist/cli/update.js +62 -10
  37. package/dist/context/import-graph.d.ts +2 -0
  38. package/dist/context/import-graph.js +31 -3
  39. package/dist/context/klyro-md.js +4 -1
  40. package/dist/context/memory.d.ts +8 -0
  41. package/dist/context/memory.js +50 -2
  42. package/dist/context/project-map.d.ts +6 -0
  43. package/dist/context/project-map.js +50 -2
  44. package/dist/context/repo-map.d.ts +2 -0
  45. package/dist/context/repo-map.js +31 -1
  46. package/dist/events/catalog.d.ts +37 -0
  47. package/dist/events/catalog.js +9 -0
  48. package/dist/index.js +177 -8
  49. package/dist/mcp/client.d.ts +6 -4
  50. package/dist/mcp/client.js +83 -14
  51. package/dist/mcp/config.d.ts +10 -0
  52. package/dist/mcp/config.js +18 -1
  53. package/dist/mcp/registry.d.ts +23 -19
  54. package/dist/mcp/registry.js +127 -8
  55. package/dist/mcp/schema.d.ts +11 -4
  56. package/dist/mcp/schema.js +27 -16
  57. package/dist/mcp/trust.d.ts +20 -0
  58. package/dist/mcp/trust.js +74 -0
  59. package/dist/persistence/audit.d.ts +28 -0
  60. package/dist/persistence/audit.js +101 -1
  61. package/dist/persistence/store.d.ts +26 -2
  62. package/dist/persistence/store.js +140 -13
  63. package/dist/policy/approval.d.ts +14 -0
  64. package/dist/policy/approval.js +44 -2
  65. package/dist/policy/engine.d.ts +17 -0
  66. package/dist/policy/engine.js +162 -9
  67. package/dist/policy/path-guard.d.ts +24 -0
  68. package/dist/policy/path-guard.js +46 -0
  69. package/dist/policy/secret-redactor.js +4 -0
  70. package/dist/providers/model-info.d.ts +23 -0
  71. package/dist/providers/model-info.js +43 -2
  72. package/dist/repl.d.ts +6 -0
  73. package/dist/repl.js +12 -7
  74. package/dist/tools/agent/spawn-agent.js +5 -5
  75. package/dist/tools/agent/task-apply.d.ts +4 -0
  76. package/dist/tools/agent/task-apply.js +44 -0
  77. package/dist/tools/agent/task-stop.d.ts +6 -0
  78. package/dist/tools/agent/task-stop.js +39 -0
  79. package/dist/tools/agent/task-wait.d.ts +17 -0
  80. package/dist/tools/agent/task-wait.js +79 -0
  81. package/dist/tools/fs/apply-patch.js +77 -1
  82. package/dist/tools/fs/edit-file.js +69 -1
  83. package/dist/tools/fs/multi-edit.d.ts +4 -0
  84. package/dist/tools/fs/multi-edit.js +70 -1
  85. package/dist/tools/fs/write-file.js +83 -6
  86. package/dist/tools/plan/todo-write.js +1 -1
  87. package/dist/tools/registry.js +6 -0
  88. package/dist/tools/shell/background.js +6 -3
  89. package/dist/tools/shell/sandbox.d.ts +51 -0
  90. package/dist/tools/shell/sandbox.js +143 -0
  91. package/dist/tools/shell/shell-exec.d.ts +29 -0
  92. package/dist/tools/shell/shell-exec.js +170 -12
  93. package/dist/tools/shell/worker-entry.d.ts +12 -0
  94. package/dist/tools/shell/worker-entry.js +43 -0
  95. package/dist/tools/types.d.ts +6 -0
  96. package/dist/tools/verify/run-verify.js +3 -1
  97. package/dist/trace/writer.d.ts +20 -0
  98. package/dist/trace/writer.js +62 -4
  99. package/dist/tui/app.js +1 -1
  100. package/dist/tui/approval.js +20 -21
  101. package/dist/util.d.ts +1 -0
  102. package/dist/util.js +1 -0
  103. package/dist/verification/baseline.js +17 -3
  104. package/dist/verification/classify.js +27 -15
  105. package/dist/verification/engine.d.ts +8 -0
  106. package/dist/verification/engine.js +28 -1
  107. package/dist/verification/registry.d.ts +2 -0
  108. package/dist/verification/registry.js +44 -0
  109. package/dist/verification/scoped.js +64 -11
  110. package/package.json +1 -1
@@ -30,7 +30,14 @@ export interface AnthropicAdapterOptions {
30
30
  authHeader?: 'x-api-key' | 'Authorization';
31
31
  /** Beta features (e.g. ['prompt-caching-2024-07-31', 'tools-2024-04-04']). */
32
32
  betas?: string[];
33
+ /**
34
+ * Prompt caching: put a `cache_control: {type:'ephemeral'}` breakpoint on
35
+ * the stable system block and send the `prompt-caching-2024-07-31` beta
36
+ * (merged with user betas). Default true.
37
+ */
38
+ promptCache?: boolean;
33
39
  }
40
+ export declare const PROMPT_CACHING_BETA = "prompt-caching-2024-07-31";
34
41
  interface AnthropicMessage {
35
42
  role: 'user' | 'assistant';
36
43
  content: Array<{
@@ -51,6 +58,28 @@ interface AnthropicMessage {
51
58
  is_error?: boolean;
52
59
  }>;
53
60
  }
61
+ interface AnthropicRequest {
62
+ model: string;
63
+ system?: string | Array<{
64
+ type: 'text';
65
+ text: string;
66
+ cache_control?: {
67
+ type: 'ephemeral';
68
+ };
69
+ }>;
70
+ messages: AnthropicMessage[];
71
+ tools?: Array<{
72
+ name: string;
73
+ description: string;
74
+ input_schema: unknown;
75
+ cache_control?: {
76
+ type: 'ephemeral';
77
+ };
78
+ }>;
79
+ max_tokens: number;
80
+ temperature?: number;
81
+ stream: true;
82
+ }
54
83
  interface AnthropicSseEvent {
55
84
  type: string;
56
85
  [key: string]: unknown;
@@ -61,6 +90,21 @@ export declare class AnthropicApiError extends Error {
61
90
  constructor(status: number, body: string);
62
91
  }
63
92
  export declare function anthropicAdapter(opts: AnthropicAdapterOptions): ProviderAdapter;
93
+ /**
94
+ * Build the Anthropic `system` field. The stable system text gets the
95
+ * cache breakpoint; the volatile telemetry suffix (when present) rides as
96
+ * a second, uncached block so it never poisons the prefix cache.
97
+ * Exported via _internal for testing.
98
+ */
99
+ export declare function buildAnthropicSystem(system: string | undefined, suffix: string | undefined, promptCache: boolean): AnthropicRequest['system'];
100
+ /**
101
+ * Build the Anthropic `tools` array. When prompt caching is enabled, the
102
+ * last tool carries a `cache_control: {type:'ephemeral'}` breakpoint so
103
+ * the (usually stable) tool definitions join the cacheable prefix —
104
+ * mirroring the system-text breakpoint. OpenAI path untouched.
105
+ * Exported via _internal for testing.
106
+ */
107
+ export declare function buildAnthropicTools(tools: ToolDefinition[], promptCache: boolean): AnthropicRequest['tools'];
64
108
  /**
65
109
  * Mutable per-stream assembly state. Blocks are keyed by content_block
66
110
  * index; the tool id is carried inside the block entry. There is no global
@@ -79,18 +123,18 @@ interface AnthropicStreamState {
79
123
  usage: {
80
124
  input?: number;
81
125
  output?: number;
126
+ cacheRead?: number;
127
+ cacheWrite?: number;
82
128
  };
83
129
  }
84
130
  declare function translateSse(event: string, parsed: AnthropicSseEvent, state: AnthropicStreamState): StreamEvent[];
85
131
  declare function toAnthropicMessages(messages: Message[]): AnthropicMessage[];
86
- declare function toAnthropicTool(t: ToolDefinition): {
87
- name: string;
88
- description: string;
89
- input_schema: unknown;
90
- };
132
+ declare function toAnthropicTool(t: ToolDefinition): NonNullable<AnthropicRequest['tools']>[number];
91
133
  export declare const _internal: {
92
134
  toAnthropicMessages: typeof toAnthropicMessages;
93
135
  toAnthropicTool: typeof toAnthropicTool;
94
136
  translateSse: typeof translateSse;
137
+ buildAnthropicSystem: typeof buildAnthropicSystem;
138
+ buildAnthropicTools: typeof buildAnthropicTools;
95
139
  };
96
140
  export {};
@@ -19,6 +19,7 @@ import { assertSafeBaseURL } from '../chat.js';
19
19
  import { parseRetryAfterMs } from './provider-adapter.js';
20
20
  const DEFAULT_VERSION = '2023-06-01';
21
21
  const DEFAULT_TIMEOUT_MS = 120_000;
22
+ export const PROMPT_CACHING_BETA = 'prompt-caching-2024-07-31';
22
23
  export class AnthropicApiError extends Error {
23
24
  status;
24
25
  body;
@@ -35,7 +36,10 @@ export function anthropicAdapter(opts) {
35
36
  const baseURL = rawBase.replace(/\/+$/, '');
36
37
  const version = opts.anthropicVersion ?? DEFAULT_VERSION;
37
38
  const authHeader = opts.authHeader ?? 'x-api-key';
38
- const betas = opts.betas ?? [];
39
+ const promptCache = opts.promptCache ?? true;
40
+ const betas = [...(opts.betas ?? [])];
41
+ if (promptCache && !betas.includes(PROMPT_CACHING_BETA))
42
+ betas.push(PROMPT_CACHING_BETA);
39
43
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
40
44
  if (!fetchImpl) {
41
45
  throw new Error('anthropicAdapter: no fetch available — pass opts.fetchImpl or run on Node 18+');
@@ -45,17 +49,55 @@ export function anthropicAdapter(opts) {
45
49
  stream(req) {
46
50
  return streamAnthropic(req, {
47
51
  baseURL, apiKey: opts.apiKey, timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
48
- fetchImpl, version, authHeader, betas,
52
+ fetchImpl, version, authHeader, betas, promptCache,
49
53
  });
50
54
  },
51
55
  };
52
56
  }
57
+ /**
58
+ * Build the Anthropic `system` field. The stable system text gets the
59
+ * cache breakpoint; the volatile telemetry suffix (when present) rides as
60
+ * a second, uncached block so it never poisons the prefix cache.
61
+ * Exported via _internal for testing.
62
+ */
63
+ export function buildAnthropicSystem(system, suffix, promptCache) {
64
+ const hasSuffix = suffix !== undefined && suffix !== '';
65
+ const breakpoint = promptCache ? { cache_control: { type: 'ephemeral' } } : {};
66
+ if (hasSuffix) {
67
+ if (!system)
68
+ return promptCache ? [{ type: 'text', text: suffix, ...breakpoint }] : suffix;
69
+ return [
70
+ { type: 'text', text: system, ...breakpoint },
71
+ { type: 'text', text: suffix },
72
+ ];
73
+ }
74
+ if (!system)
75
+ return undefined;
76
+ return promptCache ? [{ type: 'text', text: system, ...breakpoint }] : system;
77
+ }
78
+ /**
79
+ * Build the Anthropic `tools` array. When prompt caching is enabled, the
80
+ * last tool carries a `cache_control: {type:'ephemeral'}` breakpoint so
81
+ * the (usually stable) tool definitions join the cacheable prefix —
82
+ * mirroring the system-text breakpoint. OpenAI path untouched.
83
+ * Exported via _internal for testing.
84
+ */
85
+ export function buildAnthropicTools(tools, promptCache) {
86
+ if (tools.length === 0)
87
+ return undefined;
88
+ const out = tools.map(toAnthropicTool);
89
+ if (promptCache) {
90
+ const last = out[out.length - 1];
91
+ last.cache_control = { type: 'ephemeral' };
92
+ }
93
+ return out;
94
+ }
53
95
  async function* streamAnthropic(req, opts) {
54
96
  const body = {
55
97
  model: req.model,
56
- system: req.system,
98
+ system: buildAnthropicSystem(req.system, req.systemSuffix, opts.promptCache),
57
99
  messages: toAnthropicMessages(req.messages),
58
- tools: req.tools.length > 0 ? req.tools.map(toAnthropicTool) : undefined,
100
+ tools: buildAnthropicTools(req.tools, opts.promptCache),
59
101
  max_tokens: req.maxTokens ?? 4096,
60
102
  temperature: req.temperature,
61
103
  stream: true,
@@ -98,11 +140,15 @@ async function* streamAnthropic(req, opts) {
98
140
  clearTimeout(timer);
99
141
  if (!resp.ok || !resp.body) {
100
142
  const text = await resp.text().catch(() => '<unreadable>');
101
- const retryable = resp.status >= 500 || resp.status === 429;
143
+ // 413 / request_too_large (or a too-long-prompt message) is context
144
+ // overflow, not transient: dedicated code, never retryable here — the
145
+ // runtime owns compress-and-retry.
146
+ const isOverflow = resp.status === 413 || /request_too_large|too_large|prompt_too_long|context_length|maximum context length|prompt is too long/i.test(text);
147
+ const retryable = !isOverflow && (resp.status >= 500 || resp.status === 429);
102
148
  const retryAfterMs = retryable ? parseRetryAfterMs(resp.headers?.get('retry-after')) : undefined;
103
149
  yield {
104
150
  kind: 'error',
105
- code: `http_${resp.status}`,
151
+ code: isOverflow ? 'REQUEST_TOO_LARGE' : `http_${resp.status}`,
106
152
  message: `Anthropic API returned ${resp.status}: ${text.slice(0, 500)}`,
107
153
  retryable,
108
154
  status: String(resp.status),
@@ -208,25 +254,45 @@ async function* streamAnthropic(req, opts) {
208
254
  kind: 'message_end',
209
255
  finishReason: 'stop',
210
256
  ...(state.usage.input !== undefined || state.usage.output !== undefined
211
- ? { usage: { input: state.usage.input ?? 0, output: state.usage.output ?? 0 } }
257
+ ? { usage: withCache(state.usage) }
212
258
  : {}),
213
259
  };
214
260
  }
215
261
  }
262
+ /** Copy input/output plus any cache counters into a message_end usage payload. */
263
+ function withCache(u) {
264
+ return {
265
+ input: u.input ?? 0,
266
+ output: u.output ?? 0,
267
+ ...(u.cacheRead !== undefined ? { cacheRead: u.cacheRead } : {}),
268
+ ...(u.cacheWrite !== undefined ? { cacheWrite: u.cacheWrite } : {}),
269
+ };
270
+ }
271
+ /** Fold an Anthropic `usage` object (message_start or message_delta) into stream state. */
272
+ function recordAnthropicUsage(state, u) {
273
+ const rec = u;
274
+ if (typeof rec?.input_tokens === 'number')
275
+ state.usage.input = rec.input_tokens;
276
+ if (typeof rec?.output_tokens === 'number') {
277
+ state.usage.output = (state.usage.output ?? 0) + rec.output_tokens;
278
+ }
279
+ if (typeof rec?.cache_creation_input_tokens === 'number') {
280
+ state.usage.cacheWrite = (state.usage.cacheWrite ?? 0) + rec.cache_creation_input_tokens;
281
+ }
282
+ if (typeof rec?.cache_read_input_tokens === 'number') {
283
+ state.usage.cacheRead = (state.usage.cacheRead ?? 0) + rec.cache_read_input_tokens;
284
+ }
285
+ }
216
286
  function translateSse(event, parsed, state) {
217
287
  const out = [];
218
288
  switch (event) {
219
289
  case 'message_start': {
220
290
  const usage = parsed.message?.usage;
221
- if (typeof usage?.input_tokens === 'number')
222
- state.usage.input = usage.input_tokens;
291
+ recordAnthropicUsage(state, usage);
223
292
  return out;
224
293
  }
225
294
  case 'message_delta': {
226
- const usage = parsed.usage;
227
- if (typeof usage?.output_tokens === 'number') {
228
- state.usage.output = (state.usage.output ?? 0) + usage.output_tokens;
229
- }
295
+ recordAnthropicUsage(state, parsed.usage);
230
296
  return out;
231
297
  }
232
298
  case 'content_block_start': {
@@ -297,17 +363,20 @@ function translateSse(event, parsed, state) {
297
363
  kind: 'message_end',
298
364
  finishReason: 'stop',
299
365
  ...(state.usage.input !== undefined || state.usage.output !== undefined
300
- ? { usage: { input: state.usage.input ?? 0, output: state.usage.output ?? 0 } }
366
+ ? { usage: withCache(state.usage) }
301
367
  : {}),
302
368
  });
303
369
  return out;
304
370
  }
305
371
  case 'error': {
306
372
  const err = parsed.error;
373
+ const rawType = err?.type ?? 'anthropic_error';
374
+ const rawMsg = err?.message ?? 'unknown Anthropic error';
375
+ const isOverflow = /too_large|too_long|context_length|prompt_too_long|prompt is too long/i.test(`${rawType} ${rawMsg}`);
307
376
  out.push({
308
377
  kind: 'error',
309
- code: err?.type ?? 'anthropic_error',
310
- message: err?.message ?? 'unknown Anthropic error',
378
+ code: isOverflow ? 'REQUEST_TOO_LARGE' : rawType,
379
+ message: rawMsg,
311
380
  retryable: false,
312
381
  });
313
382
  return out;
@@ -378,4 +447,4 @@ function toAnthropicTool(t) {
378
447
  };
379
448
  }
380
449
  // Re-export for testability.
381
- export const _internal = { toAnthropicMessages, toAnthropicTool, translateSse };
450
+ export const _internal = { toAnthropicMessages, toAnthropicTool, translateSse, buildAnthropicSystem, buildAnthropicTools };
@@ -29,6 +29,11 @@ export interface AgentCapabilities {
29
29
  model?: string;
30
30
  /** Recursion depth cap. Beyond this, spawn attempts are blocked. */
31
31
  maxDepth?: number;
32
+ /**
33
+ * Path allow-list for filesystem scope. `undefined` means unconstrained.
34
+ * A child's effective list is the intersection with its parent's.
35
+ */
36
+ allowedPaths?: string[];
32
37
  }
33
38
  export interface ResolveToolsInput {
34
39
  /** Tools the parent is allowed to use (or `null` for the root agent = all registered tools). */
@@ -93,6 +98,11 @@ export interface ResolvedCapabilities {
93
98
  maxDepth: number;
94
99
  readonly: boolean;
95
100
  canSpawn: boolean;
101
+ /**
102
+ * Effective filesystem scope: parent's `allowedPaths` intersected with the
103
+ * agent's own. `undefined` means unconstrained (neither side restricts).
104
+ */
105
+ allowedPaths?: string[];
96
106
  }
97
107
  /** Extra knobs consumed by `resolveCapabilities` on top of `ResolveToolsInput`. */
98
108
  export interface ResolveCapabilitiesExtra {
@@ -100,10 +110,23 @@ export interface ResolveCapabilitiesExtra {
100
110
  maxDepth: number;
101
111
  /** If true, the resolved `allowed` set may include tools that themselves spawn agents. */
102
112
  canSpawnOverride?: boolean;
113
+ /**
114
+ * The parent's filesystem allow-list (`undefined` = unconstrained parent —
115
+ * the agent's own list, if any, applies as-is).
116
+ */
117
+ parentAllowedPaths?: string[];
103
118
  }
104
119
  /** Combined input for the one-shot resolver used by the orchestrator. */
105
120
  export interface ResolveCapabilitiesInput extends ResolveToolsInput, ResolveCapabilitiesExtra {
106
121
  }
122
+ /**
123
+ * Intersect two filesystem allow-lists. `undefined` on either side means
124
+ * "unconstrained" — the other side applies as-is; both `undefined` stays
125
+ * `undefined`. Entries survive when they are equal to, or nested inside, an
126
+ * entry of the other list (so a parent `/repo` and a child `/repo/sub`
127
+ * intersect to `/repo/sub`). Comparison is case-insensitive on Windows.
128
+ */
129
+ export declare function intersectAllowedPaths(parentPaths?: string[], agentPaths?: string[]): string[] | undefined;
107
130
  /**
108
131
  * One-shot capability resolver: tool intersection + per-agent model/maxDepth.
109
132
  *
@@ -71,10 +71,11 @@ export function resolveAgentTools(input) {
71
71
  }
72
72
  }
73
73
  }
74
- // 4. readonly → strip write tools.
74
+ // 4. readonly → strip write tools (plus run_verify: verification can
75
+ // execute arbitrary commands, so it is not read-only).
75
76
  if (agent.readonly) {
76
77
  for (const name of [...allowed]) {
77
- if (writeTools.has(name)) {
78
+ if (writeTools.has(name) || name === 'run_verify') {
78
79
  allowed.delete(name);
79
80
  dropped.push({ tool: name, reason: 'readonly' });
80
81
  }
@@ -104,6 +105,44 @@ export function resolveAgentTools(input) {
104
105
  dropped: dropped.sort((a, b) => a.tool.localeCompare(b.tool)),
105
106
  };
106
107
  }
108
+ /**
109
+ * Intersect two filesystem allow-lists. `undefined` on either side means
110
+ * "unconstrained" — the other side applies as-is; both `undefined` stays
111
+ * `undefined`. Entries survive when they are equal to, or nested inside, an
112
+ * entry of the other list (so a parent `/repo` and a child `/repo/sub`
113
+ * intersect to `/repo/sub`). Comparison is case-insensitive on Windows.
114
+ */
115
+ export function intersectAllowedPaths(parentPaths, agentPaths) {
116
+ if (parentPaths === undefined)
117
+ return agentPaths;
118
+ if (agentPaths === undefined)
119
+ return [...parentPaths];
120
+ const out = [];
121
+ const seen = new Set();
122
+ const within = (candidate, scope) => {
123
+ const norm = (p) => {
124
+ const n = p.replace(/\\/g, '/').replace(/\/+$/, '');
125
+ return process.platform === 'win32' ? n.toLowerCase() : n;
126
+ };
127
+ const c = norm(candidate);
128
+ const s = norm(scope);
129
+ return c === s || c.startsWith(s + '/');
130
+ };
131
+ for (const a of agentPaths) {
132
+ for (const p of parentPaths) {
133
+ // The overlap of two nested scopes is the narrower one.
134
+ const narrower = within(a, p) ? a : within(p, a) ? p : undefined;
135
+ if (narrower !== undefined) {
136
+ const key = process.platform === 'win32' ? narrower.toLowerCase() : narrower;
137
+ if (!seen.has(key)) {
138
+ seen.add(key);
139
+ out.push(narrower);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ return out.sort();
145
+ }
107
146
  /**
108
147
  * One-shot capability resolver: tool intersection + per-agent model/maxDepth.
109
148
  *
@@ -118,7 +157,10 @@ export function resolveCapabilities(input) {
118
157
  const allowed = new Set(resolved.allowed);
119
158
  const model = input.agent.model;
120
159
  const maxDepth = input.agent.maxDepth ?? input.maxDepth;
121
- const canSpawn = input.canSpawnOverride ?? input.agent.canSpawn ?? false;
160
+ // Default matches resolveAgentTools: the root agent (parentTools === null)
161
+ // may spawn; children strip spawn tools unless explicitly enabled.
162
+ const canSpawn = input.canSpawnOverride ?? input.agent.canSpawn ?? (input.parentTools === null);
163
+ const allowedPaths = intersectAllowedPaths(input.parentAllowedPaths, input.agent.allowedPaths);
122
164
  return {
123
165
  allowed,
124
166
  dropped: resolved.dropped,
@@ -126,6 +168,7 @@ export function resolveCapabilities(input) {
126
168
  maxDepth,
127
169
  readonly: input.agent.readonly ?? false,
128
170
  canSpawn,
171
+ ...(allowedPaths !== undefined ? { allowedPaths } : {}),
129
172
  };
130
173
  }
131
174
  /** Sensible defaults for "what counts as a write tool" if a caller doesn't override. */
@@ -136,15 +179,19 @@ export const DEFAULT_WRITE_TOOLS = new Set([
136
179
  'apply_patch',
137
180
  'shell_exec',
138
181
  'memory_write',
139
- 'background_shell',
140
182
  'todo_write',
141
183
  ]);
142
184
  /** Default spawn tools — removed from children by default. */
143
185
  export const DEFAULT_SPAWN_TOOLS = new Set([
144
186
  'spawn_agent',
145
- 'subtask',
146
187
  ]);
147
188
  /** Default deny-list — these are NEVER allowed, even if explicitly requested. */
148
189
  export const DEFAULT_DENIED_TOOLS = new Set([
149
- // Add dangerous tools here. Empty by default — extend as policy matures.
190
+ // Intentionally empty. Deny happens per-pattern (shellDenyRule /
191
+ // DANGEROUS_PATTERNS, .env guards, repair-guard), not per-tool: a
192
+ // tool-granularity deny-all entry (e.g. banning `shell_exec` outright)
193
+ // would break legitimate flows that rely on the allowlist + approval
194
+ // path. Seed candidates considered and rejected: `shell_exec` (needed
195
+ // for tests/builds via approval), `run_verify` (needed by tester/
196
+ // implementer agents), `write_file`/`edit_file` (core agent function).
150
197
  ]);
@@ -0,0 +1,104 @@
1
+ /**
2
+ * G2 — Process isolation for sub-agents (V1: headless/CLI children).
3
+ *
4
+ * This module is the *child-process* end of spawn_agent. The orchestrator
5
+ * forks this file with `node` (never shares a process), passing a
6
+ * JSON-serializable payload on STDIN (not argv — the system prompt + memory
7
+ * can exceed argv/command-line length limits, which would fail the spawn with
8
+ * E2BIG). The child rebuilds a minimal but self-sufficient `RuntimeDeps`,
9
+ * runs the task through `run()`, emits ONE JSON `ChildResult` line on stdout,
10
+ * and exits 0/1.
11
+ *
12
+ * Isolation contract: a child that OOMs, segfaults, or enters an infinite
13
+ * loop fails ONLY this process — the parent harness survives and reports
14
+ * `CHILD_CRASH` exactly as it does for an in-process throw.
15
+ *
16
+ * V1 scope (documented honestly):
17
+ * - Headless children only. TUI children stay in-process because the Ink
18
+ * approval bridge is inherently tied to the parent terminal.
19
+ * - The child runs a system-prompt STRING (already assembled by the parent,
20
+ * so KLYRO.md + memory + trust decisions match) with a file-system tool
21
+ * subset and DenyAll approval — no MCP server forwarding, no live prompt.
22
+ * - Grandchild spawning is not forwarded: a process-isolated child rebuilds
23
+ * its own agent bridge-less deps, so its tools see NO_ORCHESTRATOR (same
24
+ * as a pre-bridge child). V2 may thread a task handle back via a socket.
25
+ *
26
+ * Secrets never cross the process boundary via argv/env leaks: the child
27
+ * reads its provider key/baseURL from the SAME env the parent CLI uses
28
+ * (KLYRO_API_KEY, KLYRO_BASE_URL), so credentials stay in-process/env,
29
+ * never in the payload.
30
+ */
31
+ import type { RunOptions, RunResult, RuntimeDeps } from './runtime.js';
32
+ export interface ChildWorkerPayload {
33
+ /** Working dir the child operates in (may be a worktree). */
34
+ cwd: string;
35
+ /** The task prompt. */
36
+ task: string;
37
+ /** Model string (drives adapter selection + rate table). */
38
+ model: string;
39
+ /** Pre-assembled system prompt (stable + volatile already spliced). */
40
+ systemPrompt: string;
41
+ /** Agent id, for the system prompt identity. */
42
+ agentId: string;
43
+ /** Cap caps. */
44
+ maxSteps?: number;
45
+ maxCost?: number;
46
+ maxTimeMs?: number;
47
+ maxTokens?: number;
48
+ }
49
+ /** The one-line result the child prints to stdout on success. */
50
+ export type ChildResult = Pick<RunResult, 'status' | 'steps' | 'toolCalls' | 'finalText' | 'hasEdits' | 'usage'>;
51
+ /**
52
+ * Differently-phrased crash, distinct from an in-process throw the parent
53
+ * would catch. The child either exited non-zero without a parseable result
54
+ * line, or produced a malformed result. Carries the raw stdout/stderr tail.
55
+ */
56
+ export declare class ChildCrashError extends Error {
57
+ readonly likelyCause: 'exit' | 'oom' | 'timeout';
58
+ readonly stderrTail?: string | undefined;
59
+ constructor(message: string, likelyCause: 'exit' | 'oom' | 'timeout', stderrTail?: string | undefined);
60
+ }
61
+ /**
62
+ * Resolve the compiled child-worker entry that *this* module's neighboring
63
+ * build (src or dist) produced. Both dev (tsx, .ts) and packaged (dist, .js)
64
+ * layouts live beside this file, so we probe for whichever exists.
65
+ */
66
+ export declare function workerEntryPath(): string;
67
+ export interface ForkChildOptions {
68
+ /** Abort the child process when this fires (wired to the parent signal). */
69
+ signal?: AbortSignal;
70
+ /** Abort+kill the child after this long (defaults to 10 min). */
71
+ timeoutMs?: number;
72
+ /** Capture point for stderr (for diagnostics, already included in error). */
73
+ onStderr?: (chunk: string) => void;
74
+ }
75
+ /**
76
+ * Fork the child-worker as a real OS process and await its single JSON
77
+ * ChildResult line. Secrets never reach argv: the payload rides stdin, the
78
+ * provider key/baseURL come from the inherited env. Non-zero exit / missing
79
+ * line → rejects with ChildCrashError so the orchestrator maps it to
80
+ * CHILD_CRASH exactly as an in-process throw does.
81
+ */
82
+ export declare function forkChild(entry: string, payload: ChildWorkerPayload, opts?: ForkChildOptions): Promise<ChildResult>;
83
+ /**
84
+ * Rebuild a minimal RuntimeDeps from the payload + inherited env. Centralized
85
+ * so tests can call it directly. Errors on missing provider config.
86
+ */
87
+ export declare function buildChildDeps(payload: ChildWorkerPayload): Promise<{
88
+ deps: RuntimeDeps;
89
+ options: RunOptions;
90
+ }>;
91
+ /**
92
+ * Read the JSON payload. Payloads arrive on stdin (the parent writes the JSON
93
+ * then ends stdin), never argv — large system prompts would otherwise fail
94
+ * the spawn with E2BIG on both Windows (32K cmdline) and Linux (MAX_ARG_STRLEN).
95
+ * Callers who must use argv (tests) can route through `runChildFromString`.
96
+ */
97
+ export declare function readPayloadFromStdin(): Promise<string>;
98
+ /**
99
+ * Main entry. Reads payload from stdin, runs, prints ONE JSON ChildResult
100
+ * line, exits 0 on complete, 1 on failure/crash (so the parent knows to mark
101
+ * CHILD_CRASH). Intended to be the sole behavior of this file when invoked as
102
+ * a real process.
103
+ */
104
+ export declare function main(): Promise<void>;