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.
- package/dist/agent/anthropic-adapter.d.ts +49 -5
- package/dist/agent/anthropic-adapter.js +86 -17
- package/dist/agent/capabilities.d.ts +23 -0
- package/dist/agent/capabilities.js +53 -6
- package/dist/agent/child-worker.d.ts +104 -0
- package/dist/agent/child-worker.js +250 -0
- package/dist/agent/orchestrator.d.ts +124 -6
- package/dist/agent/orchestrator.js +425 -58
- package/dist/agent/provider-adapter.d.ts +8 -0
- package/dist/agent/provider-adapter.js +12 -3
- package/dist/agent/retry.d.ts +1 -1
- package/dist/agent/retry.js +54 -12
- package/dist/agent/runtime.d.ts +84 -8
- package/dist/agent/runtime.js +352 -39
- package/dist/agent/stream-budget.d.ts +36 -0
- package/dist/agent/stream-budget.js +121 -0
- package/dist/agent/worktree-manager.d.ts +74 -0
- package/dist/agent/worktree-manager.js +189 -0
- package/dist/checkpoints/store.d.ts +9 -0
- package/dist/checkpoints/store.js +56 -5
- package/dist/cli/auth.js +16 -1
- package/dist/cli/commit.d.ts +31 -0
- package/dist/cli/commit.js +142 -0
- package/dist/cli/config.d.ts +54 -3
- package/dist/cli/config.js +146 -3
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +71 -6
- package/dist/cli/eval.d.ts +6 -1
- package/dist/cli/eval.js +9 -0
- package/dist/cli/hooks.d.ts +47 -0
- package/dist/cli/hooks.js +181 -0
- package/dist/cli/repl.js +196 -29
- package/dist/cli/run.d.ts +13 -11
- package/dist/cli/run.js +144 -20
- package/dist/cli/update.d.ts +5 -0
- package/dist/cli/update.js +62 -10
- package/dist/context/import-graph.d.ts +2 -0
- package/dist/context/import-graph.js +31 -3
- package/dist/context/klyro-md.js +4 -1
- package/dist/context/memory.d.ts +8 -0
- package/dist/context/memory.js +50 -2
- package/dist/context/project-map.d.ts +6 -0
- package/dist/context/project-map.js +50 -2
- package/dist/context/repo-map.d.ts +2 -0
- package/dist/context/repo-map.js +31 -1
- package/dist/events/catalog.d.ts +37 -0
- package/dist/events/catalog.js +9 -0
- package/dist/index.js +177 -8
- package/dist/mcp/client.d.ts +6 -4
- package/dist/mcp/client.js +83 -14
- package/dist/mcp/config.d.ts +10 -0
- package/dist/mcp/config.js +18 -1
- package/dist/mcp/registry.d.ts +23 -19
- package/dist/mcp/registry.js +127 -8
- package/dist/mcp/schema.d.ts +11 -4
- package/dist/mcp/schema.js +27 -16
- package/dist/mcp/trust.d.ts +20 -0
- package/dist/mcp/trust.js +74 -0
- package/dist/persistence/audit.d.ts +28 -0
- package/dist/persistence/audit.js +101 -1
- package/dist/persistence/store.d.ts +26 -2
- package/dist/persistence/store.js +140 -13
- package/dist/policy/approval.d.ts +14 -0
- package/dist/policy/approval.js +44 -2
- package/dist/policy/engine.d.ts +17 -0
- package/dist/policy/engine.js +162 -9
- package/dist/policy/path-guard.d.ts +24 -0
- package/dist/policy/path-guard.js +46 -0
- package/dist/policy/secret-redactor.js +4 -0
- package/dist/providers/model-info.d.ts +23 -0
- package/dist/providers/model-info.js +43 -2
- package/dist/repl.d.ts +6 -0
- package/dist/repl.js +12 -7
- package/dist/tools/agent/spawn-agent.js +5 -5
- package/dist/tools/agent/task-apply.d.ts +4 -0
- package/dist/tools/agent/task-apply.js +44 -0
- package/dist/tools/agent/task-stop.d.ts +6 -0
- package/dist/tools/agent/task-stop.js +39 -0
- package/dist/tools/agent/task-wait.d.ts +17 -0
- package/dist/tools/agent/task-wait.js +79 -0
- package/dist/tools/fs/apply-patch.js +77 -1
- package/dist/tools/fs/edit-file.js +69 -1
- package/dist/tools/fs/multi-edit.d.ts +4 -0
- package/dist/tools/fs/multi-edit.js +70 -1
- package/dist/tools/fs/write-file.js +83 -6
- package/dist/tools/plan/todo-write.js +1 -1
- package/dist/tools/registry.js +6 -0
- package/dist/tools/shell/background.js +6 -3
- package/dist/tools/shell/sandbox.d.ts +51 -0
- package/dist/tools/shell/sandbox.js +143 -0
- package/dist/tools/shell/shell-exec.d.ts +29 -0
- package/dist/tools/shell/shell-exec.js +170 -12
- package/dist/tools/shell/worker-entry.d.ts +12 -0
- package/dist/tools/shell/worker-entry.js +43 -0
- package/dist/tools/types.d.ts +6 -0
- package/dist/tools/verify/run-verify.js +3 -1
- package/dist/trace/writer.d.ts +20 -0
- package/dist/trace/writer.js +62 -4
- package/dist/tui/app.js +1 -1
- package/dist/tui/approval.js +20 -21
- package/dist/util.d.ts +1 -0
- package/dist/util.js +1 -0
- package/dist/verification/baseline.js +17 -3
- package/dist/verification/classify.js +27 -15
- package/dist/verification/engine.d.ts +8 -0
- package/dist/verification/engine.js +28 -1
- package/dist/verification/registry.d.ts +2 -0
- package/dist/verification/registry.js +44 -0
- package/dist/verification/scoped.js +64 -11
- 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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
222
|
-
state.usage.input = usage.input_tokens;
|
|
291
|
+
recordAnthropicUsage(state, usage);
|
|
223
292
|
return out;
|
|
224
293
|
}
|
|
225
294
|
case 'message_delta': {
|
|
226
|
-
|
|
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:
|
|
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:
|
|
310
|
-
message:
|
|
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
|
-
|
|
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
|
-
//
|
|
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>;
|