klyro 0.1.63 → 1.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.
- package/dist/agent/anthropic-adapter.js +6 -1
- package/dist/agent/capabilities.d.ts +122 -0
- package/dist/agent/capabilities.js +150 -0
- package/dist/agent/orchestrator.d.ts +131 -0
- package/dist/agent/orchestrator.js +269 -0
- package/dist/agent/provider-adapter.d.ts +9 -0
- package/dist/agent/provider-adapter.js +24 -1
- package/dist/agent/registry.d.ts +1 -0
- package/dist/agent/registry.js +1 -0
- package/dist/agent/retry.d.ts +12 -1
- package/dist/agent/retry.js +19 -1
- package/dist/agent/runtime.d.ts +20 -0
- package/dist/agent/runtime.js +9 -1
- package/dist/agent/scoped-registry.d.ts +22 -0
- package/dist/agent/scoped-registry.js +42 -0
- package/dist/agent/task-manager.d.ts +115 -0
- package/dist/agent/task-manager.js +250 -0
- package/dist/agent/worker-spawner.d.ts +17 -12
- package/dist/agent/worker-spawner.js +26 -20
- package/dist/cli/dotenv.d.ts +3 -0
- package/dist/cli/dotenv.js +57 -0
- package/dist/cli/repl.js +41 -2
- package/dist/cli/run.d.ts +3 -0
- package/dist/cli/run.js +118 -7
- package/dist/context/klyro-md.d.ts +6 -0
- package/dist/context/klyro-md.js +21 -15
- package/dist/context/trust.d.ts +42 -0
- package/dist/context/trust.js +111 -0
- package/dist/events/catalog.d.ts +71 -0
- package/dist/index.js +4 -0
- package/dist/mcp/client.d.ts +53 -0
- package/dist/mcp/client.js +225 -0
- package/dist/mcp/config.d.ts +30 -0
- package/dist/mcp/config.js +82 -0
- package/dist/mcp/policy.d.ts +13 -0
- package/dist/mcp/policy.js +12 -0
- package/dist/mcp/registry.d.ts +50 -0
- package/dist/mcp/registry.js +172 -0
- package/dist/mcp/schema.d.ts +11 -0
- package/dist/mcp/schema.js +46 -0
- package/dist/policy/engine.js +3 -2
- package/dist/tools/agent/spawn-agent.d.ts +9 -0
- package/dist/tools/agent/spawn-agent.js +50 -0
- package/dist/tools/agent/task-get.d.ts +8 -0
- package/dist/tools/agent/task-get.js +40 -0
- package/dist/tools/agent/task-list.d.ts +4 -0
- package/dist/tools/agent/task-list.js +41 -0
- package/dist/tools/plan/todo-write.d.ts +1 -1
- package/dist/tools/registry.js +6 -0
- package/dist/tools/types.d.ts +12 -0
- package/package.json +1 -1
package/dist/cli/run.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { stdout, stderr } from 'node:process';
|
|
11
11
|
import { httpChatAdapter } from '../agent/provider-adapter.js';
|
|
12
12
|
import { anthropicAdapter } from '../agent/anthropic-adapter.js';
|
|
13
|
+
import { retryingAdapter } from '../agent/retry.js';
|
|
14
|
+
import { globalBus } from '../events/bus.js';
|
|
13
15
|
import { run } from '../agent/runtime.js';
|
|
14
16
|
import { builtinRegistry } from '../tools/registry.js';
|
|
15
17
|
import { builtinRules, clonePolicyConfig, PolicyEngine } from '../policy/engine.js';
|
|
@@ -22,6 +24,13 @@ function readEnv(name, fallback) {
|
|
|
22
24
|
return v && v.length > 0 ? v : fallback;
|
|
23
25
|
}
|
|
24
26
|
export async function runOnce(opts) {
|
|
27
|
+
// P0.5 — load <cwd>/.env first so KLYRO_* vars resolve without `export`.
|
|
28
|
+
// Never throws (missing file is a no-op); explicit env wins (no-clobber).
|
|
29
|
+
try {
|
|
30
|
+
const { loadDotenv } = await import('./dotenv.js');
|
|
31
|
+
loadDotenv(opts.cwd);
|
|
32
|
+
}
|
|
33
|
+
catch { /* ignore */ }
|
|
25
34
|
const output = opts.output ?? 'human';
|
|
26
35
|
if (opts.dryRun) {
|
|
27
36
|
return await dryRunReport(opts);
|
|
@@ -32,6 +41,27 @@ export async function runOnce(opts) {
|
|
|
32
41
|
clearReadHistory();
|
|
33
42
|
}
|
|
34
43
|
catch { /* ignore */ }
|
|
44
|
+
// P1.4 — validate --agent early so typos fail fast (exit 2) even without API keys.
|
|
45
|
+
if (opts.agent) {
|
|
46
|
+
const { BUILTIN_AGENTS: _known } = await import('../agent/orchestrator.js');
|
|
47
|
+
if (!_known.some((a) => a.id === opts.agent)) {
|
|
48
|
+
stderr.write(`klyro: unknown agent: ${opts.agent} (known: ${_known.map((a) => a.id).join(', ')})\n`);
|
|
49
|
+
return 2;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Box so retry telemetry (emitted before the session exists) picks up the
|
|
53
|
+
// real sessionId once create/resume assigns it below.
|
|
54
|
+
const sessionIdForRetry = { id: 'ephemeral' };
|
|
55
|
+
const onRetryEmit = (info) => {
|
|
56
|
+
globalBus.emit({
|
|
57
|
+
type: 'provider.retry',
|
|
58
|
+
ts: Date.now(),
|
|
59
|
+
sessionId: sessionIdForRetry.id,
|
|
60
|
+
attempt: info.attempt,
|
|
61
|
+
status: info.status,
|
|
62
|
+
...(info.retryAfterMs !== undefined ? { retryAfterMs: info.retryAfterMs } : {}),
|
|
63
|
+
});
|
|
64
|
+
};
|
|
35
65
|
let adapter = opts.adapter;
|
|
36
66
|
if (!adapter) {
|
|
37
67
|
const provider = opts.provider ?? 'openai';
|
|
@@ -42,19 +72,19 @@ export async function runOnce(opts) {
|
|
|
42
72
|
return 2;
|
|
43
73
|
}
|
|
44
74
|
if (provider === 'anthropic') {
|
|
45
|
-
adapter = anthropicAdapter({
|
|
75
|
+
adapter = retryingAdapter(anthropicAdapter({
|
|
46
76
|
baseURL: baseUrl,
|
|
47
77
|
apiKey,
|
|
48
78
|
timeoutMs: opts.timeoutMs ?? 60_000,
|
|
49
79
|
authHeader: opts.authHeader,
|
|
50
|
-
});
|
|
80
|
+
}), { onRetry: onRetryEmit });
|
|
51
81
|
}
|
|
52
82
|
else {
|
|
53
83
|
if (!baseUrl) {
|
|
54
84
|
stderr.write('klyro: KLYRO_BASE_URL is not set (or pass --base-url)\n');
|
|
55
85
|
return 2;
|
|
56
86
|
}
|
|
57
|
-
adapter = httpChatAdapter({ baseURL: baseUrl, apiKey, timeoutMs: opts.timeoutMs ?? 60_000 });
|
|
87
|
+
adapter = retryingAdapter(httpChatAdapter({ baseURL: baseUrl, apiKey, timeoutMs: opts.timeoutMs ?? 60_000 }), { onRetry: onRetryEmit });
|
|
58
88
|
}
|
|
59
89
|
}
|
|
60
90
|
const registry = builtinRegistry();
|
|
@@ -67,6 +97,23 @@ export async function runOnce(opts) {
|
|
|
67
97
|
catch {
|
|
68
98
|
/* ignore — engine defaults stand */
|
|
69
99
|
}
|
|
100
|
+
// Best-effort MCP tools: never fatal, never prompts. src/mcp/registry.ts
|
|
101
|
+
// lands from a sibling agent — the lazy import keeps runtime + builds green
|
|
102
|
+
// until then (import failure is caught below). @ts-ignore is used instead
|
|
103
|
+
// of @ts-expect-error so it stays inert after the sibling file lands.
|
|
104
|
+
let closeMcp;
|
|
105
|
+
try {
|
|
106
|
+
// @ts-ignore — sibling-owned module may not exist yet
|
|
107
|
+
const { loadAndRegisterMcp } = await import('../mcp/registry.js');
|
|
108
|
+
const mcp = await loadAndRegisterMcp({ cwd: opts.cwd, registry, policy });
|
|
109
|
+
for (const e of mcp.errors)
|
|
110
|
+
stderr.write(`klyro: mcp ${e.server}: ${e.message}\n`);
|
|
111
|
+
if (mcp.registered.length > 0 && output === 'human') {
|
|
112
|
+
stderr.write(`klyro: mcp tools: ${mcp.registered.join(', ')}\n`);
|
|
113
|
+
}
|
|
114
|
+
closeMcp = mcp.closeAll;
|
|
115
|
+
}
|
|
116
|
+
catch { /* ignore — MCP is optional */ }
|
|
70
117
|
const systemPrompt = await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt);
|
|
71
118
|
// Level 9 — session setup (create or resume)
|
|
72
119
|
const persistEnabled = opts.persist !== false;
|
|
@@ -110,6 +157,9 @@ export async function runOnce(opts) {
|
|
|
110
157
|
else if (opts.resumePath) {
|
|
111
158
|
initialTranscript = loadTranscript(opts.resumePath);
|
|
112
159
|
}
|
|
160
|
+
// Publish the real sessionId to retry telemetry (stays 'ephemeral' when
|
|
161
|
+
// persistence is disabled or the session was never created).
|
|
162
|
+
sessionIdForRetry.id = sessionId ?? 'ephemeral';
|
|
113
163
|
const ac = new AbortController();
|
|
114
164
|
const onSigint = () => {
|
|
115
165
|
stderr.write('\nklyro: SIGINT — aborting\n');
|
|
@@ -131,6 +181,49 @@ export async function runOnce(opts) {
|
|
|
131
181
|
requireVerify: opts.requireVerify,
|
|
132
182
|
};
|
|
133
183
|
let result;
|
|
184
|
+
// P1.4 — if --agent is requested, stand up a parent orchestrator so the
|
|
185
|
+
// model can call spawn_agent / task_list / task_get. The root run keeps
|
|
186
|
+
// depth 0; children are capped at maxDepth (default 1 per r-6-10.fix.md).
|
|
187
|
+
let agentBridge;
|
|
188
|
+
let parentContext;
|
|
189
|
+
if (opts.agent) {
|
|
190
|
+
const { AgentOrchestrator, BUILTIN_AGENTS } = await import('../agent/orchestrator.js');
|
|
191
|
+
const def = BUILTIN_AGENTS.find((a) => a.id === opts.agent); // validated above
|
|
192
|
+
const maxDepth = opts.maxDepth ?? 1;
|
|
193
|
+
const rootDeps = { adapter, registry, policy, approval: new DenyAllApprovalPrompt(), systemPrompt };
|
|
194
|
+
const orchestrator = new AgentOrchestrator({ sessionId: sessionId ?? 'ephemeral', deps: rootDeps });
|
|
195
|
+
const allowedTools = new Set(registry.list().map((t) => t.name));
|
|
196
|
+
parentContext = {
|
|
197
|
+
sessionId: sessionId ?? 'ephemeral',
|
|
198
|
+
depth: 0,
|
|
199
|
+
maxDepth,
|
|
200
|
+
allowedTools,
|
|
201
|
+
model: def.model ?? opts.model,
|
|
202
|
+
};
|
|
203
|
+
agentBridge = orchestrator.bridgeFor({
|
|
204
|
+
sessionId: sessionId ?? 'ephemeral',
|
|
205
|
+
cwd: opts.cwd,
|
|
206
|
+
depth: 0,
|
|
207
|
+
maxDepth,
|
|
208
|
+
allowedTools,
|
|
209
|
+
model: def.model ?? opts.model,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else if (opts.maxDepth !== undefined) {
|
|
213
|
+
const allowedTools = new Set(registry.list().map((t) => t.name));
|
|
214
|
+
const { AgentOrchestrator } = await import('../agent/orchestrator.js');
|
|
215
|
+
const rootDeps = { adapter, registry, policy, approval: new DenyAllApprovalPrompt(), systemPrompt };
|
|
216
|
+
const orchestrator = new AgentOrchestrator({ sessionId: sessionId ?? 'ephemeral', deps: rootDeps });
|
|
217
|
+
parentContext = { sessionId: sessionId ?? 'ephemeral', depth: 0, maxDepth: opts.maxDepth, allowedTools, model: opts.model };
|
|
218
|
+
agentBridge = orchestrator.bridgeFor({
|
|
219
|
+
sessionId: sessionId ?? 'ephemeral',
|
|
220
|
+
cwd: opts.cwd,
|
|
221
|
+
depth: 0,
|
|
222
|
+
maxDepth: opts.maxDepth,
|
|
223
|
+
allowedTools,
|
|
224
|
+
model: opts.model,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
134
227
|
try {
|
|
135
228
|
result = await run({
|
|
136
229
|
task: opts.task,
|
|
@@ -144,6 +237,8 @@ export async function runOnce(opts) {
|
|
|
144
237
|
initialTranscript,
|
|
145
238
|
verify: verifyOpts,
|
|
146
239
|
persist: store && sessionId ? { store, sessionId } : undefined,
|
|
240
|
+
...(agentBridge ? { agentBridge } : {}),
|
|
241
|
+
...(parentContext ? { parentContext } : {}),
|
|
147
242
|
onEvent: (ev) => {
|
|
148
243
|
if (output === 'json') {
|
|
149
244
|
stdout.write(JSON.stringify(ev) + '\n');
|
|
@@ -192,6 +287,10 @@ export async function runOnce(opts) {
|
|
|
192
287
|
}
|
|
193
288
|
finally {
|
|
194
289
|
doneSigint();
|
|
290
|
+
try {
|
|
291
|
+
await closeMcp?.();
|
|
292
|
+
}
|
|
293
|
+
catch { /* ignore */ }
|
|
195
294
|
}
|
|
196
295
|
if (store && sessionId) {
|
|
197
296
|
// Finalize session status
|
|
@@ -295,10 +394,22 @@ export async function makeRunSystemPrompt(cwd, base) {
|
|
|
295
394
|
const prefix = ctxBlock.formatted ? `\n\n<context>\n${ctxBlock.formatted}\n</context>` : '';
|
|
296
395
|
let klyroBlock = '';
|
|
297
396
|
try {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
397
|
+
// P0.3 trust gate: unapproved context files are excluded in headless
|
|
398
|
+
// mode (secure default-deny) and each exclusion is bus-visible.
|
|
399
|
+
const { loadKlyroMdFiles } = await import('../context/klyro-md.js');
|
|
400
|
+
const { ContextTrust } = await import('../context/trust.js');
|
|
401
|
+
const { globalBus } = await import('../events/bus.js');
|
|
402
|
+
const files = await loadKlyroMdFiles(cwd);
|
|
403
|
+
const { trusted, untrusted } = new ContextTrust().check(files);
|
|
404
|
+
for (const { file, reason } of untrusted) {
|
|
405
|
+
globalBus.emit({ type: 'context.trust_prompt', ts: Date.now(), sessionId: 'ephemeral', path: file.path, reason, trusted: false });
|
|
406
|
+
}
|
|
407
|
+
if (untrusted.length > 0) {
|
|
408
|
+
stderr.write(`klyro: trust gate excluded ${untrusted.length} unapproved context file(s): ${untrusted.map((u) => u.file.path).join(', ')}\n`);
|
|
409
|
+
}
|
|
410
|
+
const kept = trusted.map((f) => `# ${f.path}\n${f.content}`).join('\n\n---\n\n');
|
|
411
|
+
if (kept)
|
|
412
|
+
klyroBlock = `\n\n<KLYRO.md>\n${kept.slice(0, 4000)}\n</KLYRO.md>`;
|
|
302
413
|
}
|
|
303
414
|
catch { /* ignore */ }
|
|
304
415
|
return (ctx) => {
|
|
@@ -6,5 +6,11 @@
|
|
|
6
6
|
* secret files into context), each file is capped, and total output is
|
|
7
7
|
* capped so a giant monorepo doc can't blow the context budget.
|
|
8
8
|
*/
|
|
9
|
+
export interface KlyroMdFile {
|
|
10
|
+
path: string;
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
/** Per-file loader (used by the trust gate to approve/hash individual files). */
|
|
14
|
+
export declare function loadKlyroMdFiles(cwd: string): Promise<KlyroMdFile[]>;
|
|
9
15
|
export declare function loadKlyroMd(cwd: string): Promise<string>;
|
|
10
16
|
export declare function handleInit(cwd: string): Promise<string>;
|
package/dist/context/klyro-md.js
CHANGED
|
@@ -14,37 +14,43 @@ const MAX_TOTAL_CHARS = 8000;
|
|
|
14
14
|
function cap(s, n) {
|
|
15
15
|
return s.length > n ? s.slice(0, n) + `\n... [truncated ${s.length - n} chars]` : s;
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const push = (s) => {
|
|
21
|
-
if (total >= MAX_TOTAL_CHARS)
|
|
22
|
-
return;
|
|
23
|
-
const room = MAX_TOTAL_CHARS - total;
|
|
24
|
-
const chunk = s.length > room ? s.slice(0, room) + '\n... [truncated]' : s;
|
|
25
|
-
parts.push(chunk);
|
|
26
|
-
total += chunk.length;
|
|
27
|
-
};
|
|
28
|
-
// Global
|
|
17
|
+
/** Per-file loader (used by the trust gate to approve/hash individual files). */
|
|
18
|
+
export async function loadKlyroMdFiles(cwd) {
|
|
19
|
+
const files = [];
|
|
29
20
|
const home = os.homedir();
|
|
30
21
|
if (home) {
|
|
31
22
|
for (const p of [path.join(home, '.klyro', 'KLYRO.md'), path.join(home, '.klyro', 'KLYRO.local.md')]) {
|
|
32
23
|
try {
|
|
33
24
|
const t = await fs.readFile(p, 'utf-8');
|
|
34
|
-
push(
|
|
25
|
+
files.push({ path: p, content: cap(t, MAX_FILE_CHARS) });
|
|
35
26
|
}
|
|
36
27
|
catch { /* ignore */ }
|
|
37
28
|
}
|
|
38
29
|
}
|
|
39
|
-
// Root (imports resolved relative to each file, contained to cwd)
|
|
40
30
|
for (const name of ['KLYRO.md', 'KLYRO.local.md', 'AGENTS.md', '.cursorrules']) {
|
|
41
31
|
const p = path.join(cwd, name);
|
|
42
32
|
try {
|
|
43
33
|
const t = await fs.readFile(p, 'utf-8');
|
|
44
|
-
push(
|
|
34
|
+
files.push({ path: p, content: cap(await resolveImports(t, path.dirname(p), cwd), MAX_FILE_CHARS) });
|
|
45
35
|
}
|
|
46
36
|
catch { /* ignore */ }
|
|
47
37
|
}
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
export async function loadKlyroMd(cwd) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
let total = 0;
|
|
43
|
+
const push = (s) => {
|
|
44
|
+
if (total >= MAX_TOTAL_CHARS)
|
|
45
|
+
return;
|
|
46
|
+
const room = MAX_TOTAL_CHARS - total;
|
|
47
|
+
const chunk = s.length > room ? s.slice(0, room) + '\n... [truncated]' : s;
|
|
48
|
+
parts.push(chunk);
|
|
49
|
+
total += chunk.length;
|
|
50
|
+
};
|
|
51
|
+
for (const f of await loadKlyroMdFiles(cwd)) {
|
|
52
|
+
push(`# ${f.path}\n${f.content}`);
|
|
53
|
+
}
|
|
48
54
|
return parts.join('\n\n---\n\n');
|
|
49
55
|
}
|
|
50
56
|
async function resolveImports(text, base, root, depth = 0) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { KlyroEvent } from '../events/catalog.js';
|
|
2
|
+
import { type KlyroMdFile } from './klyro-md.js';
|
|
3
|
+
export interface TrustRecord {
|
|
4
|
+
sha256: string;
|
|
5
|
+
trustedAt: number;
|
|
6
|
+
}
|
|
7
|
+
export type TrustStore = Record<string, TrustRecord>;
|
|
8
|
+
export type TrustReason = 'unknown' | 'changed';
|
|
9
|
+
export declare function hashContent(content: string): string;
|
|
10
|
+
/** Pure evaluation: split files into trusted vs needs-approval. */
|
|
11
|
+
export declare function evaluateTrust(stored: TrustStore, files: KlyroMdFile[]): {
|
|
12
|
+
trusted: KlyroMdFile[];
|
|
13
|
+
untrusted: {
|
|
14
|
+
file: KlyroMdFile;
|
|
15
|
+
reason: TrustReason;
|
|
16
|
+
}[];
|
|
17
|
+
};
|
|
18
|
+
export declare function defaultTrustStorePath(): string;
|
|
19
|
+
export declare class ContextTrust {
|
|
20
|
+
private readonly storePath;
|
|
21
|
+
private store;
|
|
22
|
+
constructor(storePath?: string);
|
|
23
|
+
private load;
|
|
24
|
+
private save;
|
|
25
|
+
check(files: KlyroMdFile[]): ReturnType<typeof evaluateTrust>;
|
|
26
|
+
/** Record approval for the file's current content. */
|
|
27
|
+
approve(file: KlyroMdFile): void;
|
|
28
|
+
isTrusted(file: KlyroMdFile): boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface TrustedLoadOpts {
|
|
31
|
+
trust: ContextTrust;
|
|
32
|
+
/** Return true to include an untrusted file (and persist the approval). */
|
|
33
|
+
approve: (file: KlyroMdFile, reason: TrustReason) => Promise<boolean>;
|
|
34
|
+
sessionId?: string;
|
|
35
|
+
emit?: (ev: KlyroEvent) => void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Load KLYRO.md hierarchy with the trust gate applied. Untrusted files are
|
|
39
|
+
* offered to `approve`; declined files are excluded from the returned text.
|
|
40
|
+
* Every gate decision emits `context.trust_prompt` on the bus.
|
|
41
|
+
*/
|
|
42
|
+
export declare function loadTrustedKlyroMd(cwd: string, opts: TrustedLoadOpts): Promise<string>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P0.3 — context-file trust gate (r-11-17.md §5.1).
|
|
3
|
+
*
|
|
4
|
+
* Closes the shared HIGH flaw: `KLYRO.md` / `AGENTS.md` are auto-loaded into
|
|
5
|
+
* the prompt, so a malicious commit can smuggle instructions that read as
|
|
6
|
+
* authenticated system guidance. Every auto-loaded file is hashed; the first
|
|
7
|
+
* sighting (or any change) requires explicit approval before the content may
|
|
8
|
+
* enter context. Decisions persist in `~/.klyro/context-trust.json`.
|
|
9
|
+
*
|
|
10
|
+
* Headless runs approve nothing (secure default): untrusted files are
|
|
11
|
+
* excluded and the exclusion is visible on the event bus.
|
|
12
|
+
*/
|
|
13
|
+
import * as crypto from 'node:crypto';
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as os from 'node:os';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { loadKlyroMdFiles } from './klyro-md.js';
|
|
18
|
+
export function hashContent(content) {
|
|
19
|
+
return crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
20
|
+
}
|
|
21
|
+
/** Pure evaluation: split files into trusted vs needs-approval. */
|
|
22
|
+
export function evaluateTrust(stored, files) {
|
|
23
|
+
const trusted = [];
|
|
24
|
+
const untrusted = [];
|
|
25
|
+
for (const f of files) {
|
|
26
|
+
const rec = stored[f.path];
|
|
27
|
+
if (!rec) {
|
|
28
|
+
untrusted.push({ file: f, reason: 'unknown' });
|
|
29
|
+
}
|
|
30
|
+
else if (rec.sha256 !== hashContent(f.content)) {
|
|
31
|
+
untrusted.push({ file: f, reason: 'changed' });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
trusted.push(f);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { trusted, untrusted };
|
|
38
|
+
}
|
|
39
|
+
export function defaultTrustStorePath() {
|
|
40
|
+
return path.join(os.homedir() || process.cwd(), '.klyro', 'context-trust.json');
|
|
41
|
+
}
|
|
42
|
+
export class ContextTrust {
|
|
43
|
+
storePath;
|
|
44
|
+
store = {};
|
|
45
|
+
constructor(storePath = defaultTrustStorePath()) {
|
|
46
|
+
this.storePath = storePath;
|
|
47
|
+
this.load();
|
|
48
|
+
}
|
|
49
|
+
load() {
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs.readFileSync(this.storePath, 'utf-8');
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
54
|
+
this.store = parsed;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
this.store = {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
save() {
|
|
62
|
+
try {
|
|
63
|
+
fs.mkdirSync(path.dirname(this.storePath), { recursive: true });
|
|
64
|
+
fs.writeFileSync(this.storePath, JSON.stringify(this.store, null, 2), 'utf-8');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* best-effort — trust stays in memory for the session */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
check(files) {
|
|
71
|
+
return evaluateTrust(this.store, files);
|
|
72
|
+
}
|
|
73
|
+
/** Record approval for the file's current content. */
|
|
74
|
+
approve(file) {
|
|
75
|
+
this.store[file.path] = { sha256: hashContent(file.content), trustedAt: Date.now() };
|
|
76
|
+
this.save();
|
|
77
|
+
}
|
|
78
|
+
isTrusted(file) {
|
|
79
|
+
const rec = this.store[file.path];
|
|
80
|
+
return !!rec && rec.sha256 === hashContent(file.content);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Load KLYRO.md hierarchy with the trust gate applied. Untrusted files are
|
|
85
|
+
* offered to `approve`; declined files are excluded from the returned text.
|
|
86
|
+
* Every gate decision emits `context.trust_prompt` on the bus.
|
|
87
|
+
*/
|
|
88
|
+
export async function loadTrustedKlyroMd(cwd, opts) {
|
|
89
|
+
const sessionId = opts.sessionId ?? 'ephemeral';
|
|
90
|
+
const files = await loadKlyroMdFiles(cwd);
|
|
91
|
+
const { trusted, untrusted } = opts.trust.check(files);
|
|
92
|
+
const kept = [...trusted];
|
|
93
|
+
for (const { file, reason } of untrusted) {
|
|
94
|
+
let ok = false;
|
|
95
|
+
try {
|
|
96
|
+
ok = await opts.approve(file, reason);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
ok = false;
|
|
100
|
+
}
|
|
101
|
+
opts.emit?.({ type: 'context.trust_prompt', ts: Date.now(), sessionId, path: file.path, reason, trusted: ok });
|
|
102
|
+
if (ok) {
|
|
103
|
+
opts.trust.approve(file);
|
|
104
|
+
kept.push(file);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Preserve loader order.
|
|
108
|
+
const order = new Map(files.map((f, i) => [f.path, i]));
|
|
109
|
+
kept.sort((a, b) => (order.get(a.path) ?? 0) - (order.get(b.path) ?? 0));
|
|
110
|
+
return kept.map((f) => `# ${f.path}\n${f.content}`).join('\n\n---\n\n');
|
|
111
|
+
}
|
package/dist/events/catalog.d.ts
CHANGED
|
@@ -132,5 +132,76 @@ export type KlyroEvent = {
|
|
|
132
132
|
ts: number;
|
|
133
133
|
sessionId: string;
|
|
134
134
|
reason: string;
|
|
135
|
+
} | {
|
|
136
|
+
type: 'subtask.started';
|
|
137
|
+
ts: number;
|
|
138
|
+
sessionId: string;
|
|
139
|
+
taskId: string;
|
|
140
|
+
parentTaskId?: string;
|
|
141
|
+
agentName: string;
|
|
142
|
+
depth: number;
|
|
143
|
+
model?: string;
|
|
144
|
+
} | {
|
|
145
|
+
type: 'subtask.progress';
|
|
146
|
+
ts: number;
|
|
147
|
+
sessionId: string;
|
|
148
|
+
taskId: string;
|
|
149
|
+
note: string;
|
|
150
|
+
} | {
|
|
151
|
+
type: 'subtask.completed';
|
|
152
|
+
ts: number;
|
|
153
|
+
sessionId: string;
|
|
154
|
+
taskId: string;
|
|
155
|
+
status: 'succeeded';
|
|
156
|
+
durationMs: number;
|
|
157
|
+
steps: number;
|
|
158
|
+
toolCalls: number;
|
|
159
|
+
} | {
|
|
160
|
+
type: 'subtask.failed';
|
|
161
|
+
ts: number;
|
|
162
|
+
sessionId: string;
|
|
163
|
+
taskId: string;
|
|
164
|
+
status: 'failed' | 'cancelled' | 'timed_out' | 'blocked';
|
|
165
|
+
durationMs: number;
|
|
166
|
+
error?: {
|
|
167
|
+
code: string;
|
|
168
|
+
message: string;
|
|
169
|
+
};
|
|
170
|
+
} | {
|
|
171
|
+
type: 'subtask.tool_call';
|
|
172
|
+
ts: number;
|
|
173
|
+
sessionId: string;
|
|
174
|
+
taskId: string;
|
|
175
|
+
callId: string;
|
|
176
|
+
name: string;
|
|
177
|
+
} | {
|
|
178
|
+
type: 'subtask.tool_result';
|
|
179
|
+
ts: number;
|
|
180
|
+
sessionId: string;
|
|
181
|
+
taskId: string;
|
|
182
|
+
callId: string;
|
|
183
|
+
name: string;
|
|
184
|
+
isError: boolean;
|
|
185
|
+
latencyMs: number;
|
|
186
|
+
} | {
|
|
187
|
+
type: 'subtask.merged';
|
|
188
|
+
ts: number;
|
|
189
|
+
sessionId: string;
|
|
190
|
+
taskId: string;
|
|
191
|
+
changedFiles: string[];
|
|
192
|
+
} | {
|
|
193
|
+
type: 'provider.retry';
|
|
194
|
+
ts: number;
|
|
195
|
+
sessionId: string;
|
|
196
|
+
attempt: number;
|
|
197
|
+
status: string;
|
|
198
|
+
retryAfterMs?: number;
|
|
199
|
+
} | {
|
|
200
|
+
type: 'context.trust_prompt';
|
|
201
|
+
ts: number;
|
|
202
|
+
sessionId: string;
|
|
203
|
+
path: string;
|
|
204
|
+
reason: 'unknown' | 'changed';
|
|
205
|
+
trusted: boolean;
|
|
135
206
|
};
|
|
136
207
|
export type KlyroEventType = KlyroEvent['type'];
|
package/dist/index.js
CHANGED
|
@@ -274,6 +274,8 @@ async function main() {
|
|
|
274
274
|
.option('--max-repairs <n>', 'Max autonomous repair attempts (default 3)', (v) => parsePositiveInt('--max-repairs', v))
|
|
275
275
|
.option('--persist', 'Enable session persistence (Level 9, default: enabled)')
|
|
276
276
|
.option('--require-verify', 'Fail with exit 8 if no verification passed after edits (6.5)')
|
|
277
|
+
.option('--agent <name>', 'Run under an orchestrator context enabling spawn_agent/task_list/task_get (explorer|implementer|tester|reviewer)')
|
|
278
|
+
.option('--max-depth <n>', 'Max spawn depth for child agents (default 1)', (v) => parsePositiveInt('--max-depth', v))
|
|
277
279
|
.action(async (prompt, opts) => {
|
|
278
280
|
const model = opts.model ?? process.env.KLYRO_MODEL;
|
|
279
281
|
if (!model) {
|
|
@@ -307,6 +309,8 @@ async function main() {
|
|
|
307
309
|
maxRepairAttempts: opts.maxRepairs,
|
|
308
310
|
persist: opts.persist,
|
|
309
311
|
requireVerify: !!opts.requireVerify,
|
|
312
|
+
agent: opts.agent,
|
|
313
|
+
maxDepth: opts.maxDepth,
|
|
310
314
|
});
|
|
311
315
|
process.exit(code);
|
|
312
316
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { McpServerSpec } from './config.js';
|
|
2
|
+
export interface McpToolDef {
|
|
3
|
+
name: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
inputSchema?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface McpResource {
|
|
8
|
+
uri: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
mimeType?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface McpCallResult {
|
|
13
|
+
/** Normalized text payload (content blocks joined). */
|
|
14
|
+
text: string;
|
|
15
|
+
isError: boolean;
|
|
16
|
+
raw: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface McpClientLike {
|
|
19
|
+
listTools(): Promise<McpToolDef[]>;
|
|
20
|
+
callTool(name: string, args: unknown, signal?: AbortSignal): Promise<McpCallResult>;
|
|
21
|
+
listResources(): Promise<McpResource[]>;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare class McpError extends Error {
|
|
25
|
+
readonly code: string;
|
|
26
|
+
readonly details?: unknown | undefined;
|
|
27
|
+
constructor(message: string, code: string, details?: unknown | undefined);
|
|
28
|
+
}
|
|
29
|
+
export declare class McpClient implements McpClientLike {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
private readonly spec;
|
|
32
|
+
private child;
|
|
33
|
+
private nextId;
|
|
34
|
+
private readonly pending;
|
|
35
|
+
private stdoutBuf;
|
|
36
|
+
private stderrTail;
|
|
37
|
+
private dead;
|
|
38
|
+
private closed;
|
|
39
|
+
constructor(name: string, spec: McpServerSpec);
|
|
40
|
+
connect(): Promise<void>;
|
|
41
|
+
private connected;
|
|
42
|
+
listTools(): Promise<McpToolDef[]>;
|
|
43
|
+
callTool(name: string, args: unknown, signal?: AbortSignal): Promise<McpCallResult>;
|
|
44
|
+
listResources(): Promise<McpResource[]>;
|
|
45
|
+
readResource(uri: string, signal?: AbortSignal): Promise<string>;
|
|
46
|
+
close(): Promise<void>;
|
|
47
|
+
private timeout;
|
|
48
|
+
private assertLive;
|
|
49
|
+
private onStdout;
|
|
50
|
+
private notify;
|
|
51
|
+
private request;
|
|
52
|
+
private failAll;
|
|
53
|
+
}
|