klyro 1.0.0 → 1.0.1
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 +36 -0
- package/dist/agent/anthropic-adapter.js +68 -16
- package/dist/agent/capabilities.d.ts +23 -0
- package/dist/agent/capabilities.js +46 -5
- package/dist/agent/child-worker.d.ts +104 -0
- package/dist/agent/child-worker.js +250 -0
- package/dist/agent/orchestrator.d.ts +106 -5
- package/dist/agent/orchestrator.js +378 -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 +2 -2
- package/dist/agent/runtime.d.ts +50 -8
- package/dist/agent/runtime.js +196 -33
- package/dist/agent/worktree-manager.d.ts +74 -0
- package/dist/agent/worktree-manager.js +189 -0
- package/dist/checkpoints/store.js +30 -5
- package/dist/cli/auth.js +16 -1
- package/dist/cli/config.d.ts +9 -3
- package/dist/cli/config.js +64 -3
- package/dist/cli/eval.d.ts +6 -1
- package/dist/cli/eval.js +9 -0
- package/dist/cli/repl.js +155 -28
- package/dist/cli/run.d.ts +7 -11
- package/dist/cli/run.js +68 -17
- 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 +28 -0
- package/dist/index.js +89 -4
- 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 -1
- package/dist/mcp/registry.js +78 -6
- 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 +1 -0
- package/dist/policy/engine.js +88 -8
- package/dist/policy/secret-redactor.js +4 -0
- package/dist/providers/model-info.d.ts +17 -0
- package/dist/providers/model-info.js +35 -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 +71 -0
- package/dist/tools/fs/edit-file.js +65 -0
- package/dist/tools/fs/multi-edit.d.ts +4 -0
- package/dist/tools/fs/multi-edit.js +66 -0
- package/dist/tools/fs/write-file.js +67 -0
- 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 +1 -0
- package/dist/tools/shell/shell-exec.js +83 -11
- 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 +13 -0
- package/dist/trace/writer.js +55 -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 +23 -12
- package/dist/verification/engine.js +3 -1
- package/dist/verification/registry.d.ts +2 -0
- package/dist/verification/registry.js +33 -0
- package/dist/verification/scoped.js +28 -6
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -15,6 +15,7 @@ import { anthropicAdapter } from '../agent/anthropic-adapter.js';
|
|
|
15
15
|
import { retryingAdapter } from '../agent/retry.js';
|
|
16
16
|
import { globalBus } from '../events/bus.js';
|
|
17
17
|
import { run } from '../agent/runtime.js';
|
|
18
|
+
import { resolveSystemPrompt } from '../agent/runtime.js';
|
|
18
19
|
import { builtinRegistry } from '../tools/registry.js';
|
|
19
20
|
import { builtinRules, clonePolicyConfig, PolicyEngine } from '../policy/engine.js';
|
|
20
21
|
import { buildLevel6Context } from '../context/level6.js';
|
|
@@ -28,7 +29,10 @@ import { MouseFilter, MOUSE_ENABLE, MOUSE_DISABLE, createReadWrapper } from '../
|
|
|
28
29
|
import { inferProviderFromBaseURL } from '../agent/registry.js';
|
|
29
30
|
import { getDefaultSessionStore } from '../persistence/session.js';
|
|
30
31
|
import { buildSystemPrompt, parseImageInput } from '../context/system-prompt.js';
|
|
32
|
+
import { memoryBlock } from '../context/memory.js';
|
|
31
33
|
import { estimateCost } from '../providers/model-info.js';
|
|
34
|
+
import { ContextTrust } from '../context/trust.js';
|
|
35
|
+
import { redact } from '../policy/secret-redactor.js';
|
|
32
36
|
export async function startRepl(opts = {}) {
|
|
33
37
|
// P0.5 — load <cwd>/.env first so provider resolution below sees KLYRO_*
|
|
34
38
|
// vars without `export`. Never throws; explicit env wins (no-clobber).
|
|
@@ -90,21 +94,66 @@ export async function startRepl(opts = {}) {
|
|
|
90
94
|
catch {
|
|
91
95
|
/* ignore — engine defaults stand */
|
|
92
96
|
}
|
|
93
|
-
// Best-effort MCP tools: never fatal, never prompts.
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
97
|
+
// Best-effort MCP tools: never fatal, never prompts. Servers live for the
|
|
98
|
+
// process lifetime, so no close wiring here (unlike runOnce's finally).
|
|
99
|
+
// NOTE: the actual loadAndRegisterMcp call lives below (after the approval
|
|
100
|
+
// object exists) so project-server approval can route through it.
|
|
101
|
+
// Local inline sanitizer for the approval prompt (duplicates the shared
|
|
102
|
+
// sanitizeForPrompt in sibling-owned policy/approval.ts — intentionally
|
|
103
|
+
// not imported; kept local to avoid coupling the REPL to that module).
|
|
104
|
+
function sanitizeForPrompt(s) {
|
|
105
|
+
return s.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]|\x1b\[[0-9;]*[A-Za-z]/g, '').slice(0, 120);
|
|
106
|
+
}
|
|
107
|
+
async function loadMcpTools() {
|
|
108
|
+
try {
|
|
109
|
+
const { loadAndRegisterMcp } = await import('../mcp/registry.js');
|
|
110
|
+
const mcp = await loadAndRegisterMcp({
|
|
111
|
+
cwd,
|
|
112
|
+
registry,
|
|
113
|
+
policy,
|
|
114
|
+
// CONTRACT (sibling D): project-server approval hook. Registries that
|
|
115
|
+
// predate the contract ignore the unknown opt (checked via try/catch
|
|
116
|
+
// below with a skip-with-warning fallback).
|
|
117
|
+
approveProjectServer: async (info) => {
|
|
118
|
+
// Show the disabled state alongside source before asking, so a
|
|
119
|
+
// disabled server's approval can't be mistaken for activation.
|
|
120
|
+
let disabledNote = '';
|
|
121
|
+
try {
|
|
122
|
+
const { loadMcpServers } = await import('../mcp/config.js');
|
|
123
|
+
const cfg = loadMcpServers(cwd);
|
|
124
|
+
const spec = cfg.servers[info.name];
|
|
125
|
+
if (spec?.disabled)
|
|
126
|
+
disabledNote = ' (disabled in config)';
|
|
127
|
+
}
|
|
128
|
+
catch { /* ignore — prompt without the note */ }
|
|
129
|
+
const safeName = sanitizeForPrompt(info.name);
|
|
130
|
+
const safeSource = sanitizeForPrompt(info.source);
|
|
131
|
+
try {
|
|
132
|
+
const choice = await approval.ask({
|
|
133
|
+
toolName: 'mcp_project_server',
|
|
134
|
+
reason: `MCP project server "${safeName}"${disabledNote} from ${safeSource} — approve to register its tools`,
|
|
135
|
+
summary: safeName,
|
|
136
|
+
});
|
|
137
|
+
return choice === 'allow' || choice === 'always' || choice === 'always-persist';
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
for (const e of mcp.errors)
|
|
145
|
+
process.stderr.write(`klyro: mcp ${e.server}: ${e.message}\n`);
|
|
146
|
+
if (mcp.registered.length > 0) {
|
|
147
|
+
process.stderr.write(`klyro: mcp tools: ${mcp.registered.join(', ')}\n`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
try {
|
|
152
|
+
process.stderr.write('klyro: mcp project-server approval skipped (registry unavailable)\n');
|
|
153
|
+
}
|
|
154
|
+
catch { /* ignore */ }
|
|
105
155
|
}
|
|
106
156
|
}
|
|
107
|
-
catch { /* ignore — MCP is optional */ }
|
|
108
157
|
const providerKind = inferProviderFromBaseURL(baseUrl);
|
|
109
158
|
// Local Ollama exposes OpenAI-compat but hostname could contain "anthropic"
|
|
110
159
|
// via proxy — don't try anthropic adapter with empty key (would 401).
|
|
@@ -138,14 +187,16 @@ export async function startRepl(opts = {}) {
|
|
|
138
187
|
let adapter = buildAdapter(currentProvider, currentBaseUrl, currentApiKey);
|
|
139
188
|
const ctxBlock = await buildLevel6Context({ cwd });
|
|
140
189
|
let ctxPrefix = ctxBlock.formatted ? `\n\n<context>\n${ctxBlock.formatted}\n</context>` : '';
|
|
141
|
-
// 4.4 KLYRO.md hierarchy (mutable — /reload refreshes)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
190
|
+
// 4.4 KLYRO.md hierarchy (mutable — /reload refreshes). Content is gated
|
|
191
|
+
// through the context trust store (see loadKlyroBlockTrust below) — the
|
|
192
|
+
// initial value stays empty until the gated load runs after approval setup.
|
|
193
|
+
let klyroBlock = '';
|
|
194
|
+
// 2.3 layered system prompt (split shape: stable base + volatile telemetry suffix)
|
|
145
195
|
const systemPromptFn = (_ctx) => {
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
196
|
+
// 8.4 — inject session memory so memory_write actually takes effect.
|
|
197
|
+
const memBlock = memoryBlock(cwd);
|
|
198
|
+
const base = buildSystemPrompt({ cwd, model, extraSystem: opts.systemPrompt, appendSystem: ctxPrefix + klyroBlock + memBlock });
|
|
199
|
+
return _ctx.telemetry ? { system: base, suffix: _ctx.telemetry } : { system: base };
|
|
149
200
|
};
|
|
150
201
|
let ac = new AbortController();
|
|
151
202
|
// When the TUI is mounted, use the inline Ink prompt. Otherwise
|
|
@@ -176,6 +227,45 @@ export async function startRepl(opts = {}) {
|
|
|
176
227
|
});
|
|
177
228
|
},
|
|
178
229
|
});
|
|
230
|
+
// Context-file trust gate (context/trust.js): KLYRO.md / AGENTS.md content
|
|
231
|
+
// only enters the prompt after the trust store approves it. Untrusted files
|
|
232
|
+
// prompt via the repl approval object (allow/always/always-persist → true,
|
|
233
|
+
// deny → false); every decision emits context.trust_prompt on globalBus.
|
|
234
|
+
const trustStore = new ContextTrust();
|
|
235
|
+
async function loadKlyroBlockTrust() {
|
|
236
|
+
try {
|
|
237
|
+
const { loadTrustedKlyroMd } = await import('../context/trust.js');
|
|
238
|
+
const text = await loadTrustedKlyroMd(cwd, {
|
|
239
|
+
trust: trustStore,
|
|
240
|
+
approve: async (file, reason) => {
|
|
241
|
+
try {
|
|
242
|
+
const choice = await approval.ask({
|
|
243
|
+
toolName: 'context_trust',
|
|
244
|
+
reason: `untrusted context file (${reason}): ${file.path}`,
|
|
245
|
+
summary: file.path,
|
|
246
|
+
});
|
|
247
|
+
return choice === 'allow' || choice === 'always' || choice === 'always-persist';
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
sessionId: typeof tuiSessionId === 'string' ? tuiSessionId : 'ephemeral',
|
|
254
|
+
emit: (ev) => globalBus.emit(ev),
|
|
255
|
+
});
|
|
256
|
+
return text ? `\n\n<KLYRO.md>\n${text.slice(0, 4000)}\n</KLYRO.md>` : '';
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return '';
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// MCP servers need the approval object above — load them now.
|
|
263
|
+
await loadMcpTools();
|
|
264
|
+
// Non-TUI runs can prompt on stdin immediately; the TUI defers the gated
|
|
265
|
+
// load to onMounted (the modal can't render before mount — see below).
|
|
266
|
+
if (!useTui) {
|
|
267
|
+
klyroBlock = await loadKlyroBlockTrust().catch(() => '');
|
|
268
|
+
}
|
|
179
269
|
let inflight = null;
|
|
180
270
|
let lastStatus = null;
|
|
181
271
|
const pendingQueue = [];
|
|
@@ -479,14 +569,41 @@ export async function startRepl(opts = {}) {
|
|
|
479
569
|
return s.length > cap ? s.slice(0, cap) + `\n... [truncated ${s.length - cap} chars]` : s;
|
|
480
570
|
}
|
|
481
571
|
async function execShell(command, timeoutMs = 120_000) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
572
|
+
// Internal shell executions bypass the agent runtime's event path, so
|
|
573
|
+
// audit them here: a redacted tool.call before, the matching tool.result
|
|
574
|
+
// after. One helper covers every slash handler (/commit, /push, …).
|
|
575
|
+
const callId = `internal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
576
|
+
const sessionId = typeof tuiSessionId === 'string' ? tuiSessionId : 'ephemeral';
|
|
577
|
+
const startedAt = Date.now();
|
|
578
|
+
globalBus.emit({
|
|
579
|
+
type: 'tool.call', ts: startedAt, sessionId, callId,
|
|
580
|
+
name: 'shell_exec', input: { internal: true, command: redact(command).slice(0, 2000) },
|
|
581
|
+
});
|
|
582
|
+
try {
|
|
583
|
+
const r = await registry.execute('shell_exec', { command, timeoutMs }, { cwd, env: process.env, nonInteractive: true });
|
|
584
|
+
if (!r.ok)
|
|
585
|
+
throw new Error(r.error.message ?? 'shell failed');
|
|
586
|
+
const v = r.value;
|
|
587
|
+
globalBus.emit({
|
|
588
|
+
type: 'tool.result', ts: Date.now(), sessionId, callId,
|
|
589
|
+
name: 'shell_exec', output: { internal: true, exitCode: v.exitCode },
|
|
590
|
+
isError: false, latencyMs: Date.now() - startedAt,
|
|
591
|
+
});
|
|
592
|
+
return v;
|
|
593
|
+
}
|
|
594
|
+
catch (err) {
|
|
595
|
+
globalBus.emit({
|
|
596
|
+
type: 'tool.result', ts: Date.now(), sessionId, callId,
|
|
597
|
+
name: 'shell_exec', output: { internal: true, error: err instanceof Error ? err.message : String(err) },
|
|
598
|
+
isError: true, latencyMs: Date.now() - startedAt,
|
|
599
|
+
});
|
|
600
|
+
throw err;
|
|
601
|
+
}
|
|
486
602
|
}
|
|
487
603
|
/** Read-only LLM answer (no tools) — powers /ask, /explain, /compact. */
|
|
488
604
|
async function answerReadOnly(question, context) {
|
|
489
|
-
|
|
605
|
+
// Legacy chat path (no modes): resolve the split prompt back to one string.
|
|
606
|
+
const sys = resolveSystemPrompt(systemPromptFn, { cwd, telemetry: '' }).system + '\n\nAnswer read-only: do not call tools, do not edit files.';
|
|
490
607
|
const attached = await buildAttachedBlock();
|
|
491
608
|
const userText = (context ? `${question}\n\n<context>\n${context.slice(0, 6000)}\n</context>` : question) + attached;
|
|
492
609
|
const req = {
|
|
@@ -564,6 +681,14 @@ export async function startRepl(opts = {}) {
|
|
|
564
681
|
hooks.append(ev.item);
|
|
565
682
|
}
|
|
566
683
|
pendingQueue.length = 0;
|
|
684
|
+
// Deferred trust-gated KLYRO.md load: the approval modal can only
|
|
685
|
+
// render once the App is mounted. Prompts sent before this resolves
|
|
686
|
+
// run without the KLYRO.md block (noted in /reload output).
|
|
687
|
+
if (useTui && !klyroBlock) {
|
|
688
|
+
void loadKlyroBlockTrust()
|
|
689
|
+
.then((b) => { klyroBlock = b; })
|
|
690
|
+
.catch(() => undefined);
|
|
691
|
+
}
|
|
567
692
|
},
|
|
568
693
|
}), { patchConsole: false });
|
|
569
694
|
// Install SIGINT handler only after app exists (avoids TDZ) and use once
|
|
@@ -618,7 +743,8 @@ export async function startRepl(opts = {}) {
|
|
|
618
743
|
try {
|
|
619
744
|
const simpleReq = {
|
|
620
745
|
model,
|
|
621
|
-
|
|
746
|
+
// Legacy chat path (no modes): plain string system, untouched by the split.
|
|
747
|
+
system: resolveSystemPrompt(systemPromptFn, { cwd, telemetry: '' }).system,
|
|
622
748
|
messages: [{ role: 'user', content: [{ kind: 'text', text: taskText }] }],
|
|
623
749
|
tools: [],
|
|
624
750
|
signal: ac.signal,
|
|
@@ -2380,8 +2506,9 @@ export async function startRepl(opts = {}) {
|
|
|
2380
2506
|
}
|
|
2381
2507
|
const ctx = await buildLevel6Context({ cwd });
|
|
2382
2508
|
ctxPrefix = ctx.formatted ? `\n\n<context>\n${ctx.formatted}\n</context>` : '';
|
|
2383
|
-
|
|
2384
|
-
|
|
2509
|
+
// Trust-gated like the initial load (the modal is mounted here,
|
|
2510
|
+
// so untrusted files prompt instead of being silently dropped).
|
|
2511
|
+
klyroBlock = await loadKlyroBlockTrust().catch(() => '');
|
|
2385
2512
|
queuedAppend({
|
|
2386
2513
|
id: `reload-${Date.now()}`,
|
|
2387
2514
|
kind: 'text',
|
package/dist/cli/run.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* even on a long task. Tool calls and policy decisions go to stderr so
|
|
8
8
|
* they don't pollute the captured `finalText` if the user pipes.
|
|
9
9
|
*/
|
|
10
|
+
import type { VerifyMode } from '../agent/runtime.js';
|
|
11
|
+
import type { SystemPromptFn } from '../agent/runtime.js';
|
|
10
12
|
import type { Message } from '../agent/message.js';
|
|
11
13
|
export interface RunCliOptions {
|
|
12
14
|
task: string;
|
|
@@ -15,10 +17,7 @@ export interface RunCliOptions {
|
|
|
15
17
|
maxSteps?: number;
|
|
16
18
|
maxTokens?: number;
|
|
17
19
|
temperature?: number;
|
|
18
|
-
systemPrompt?:
|
|
19
|
-
cwd: string;
|
|
20
|
-
telemetry?: string;
|
|
21
|
-
}) => string;
|
|
20
|
+
systemPrompt?: SystemPromptFn;
|
|
22
21
|
baseUrl?: string;
|
|
23
22
|
apiKey?: string;
|
|
24
23
|
timeoutMs?: number;
|
|
@@ -60,6 +59,9 @@ export interface RunCliOptions {
|
|
|
60
59
|
maxRepairAttempts?: number;
|
|
61
60
|
verifyTimeoutMs?: number;
|
|
62
61
|
requireVerify?: boolean;
|
|
62
|
+
/** Verification mode: strict (default) runs the full repair pipeline;
|
|
63
|
+
* advisory reports once without repairs; off skips verification. */
|
|
64
|
+
verifyMode?: VerifyMode;
|
|
63
65
|
/** Level 9 — persistence */
|
|
64
66
|
persist?: boolean;
|
|
65
67
|
sessionId?: string;
|
|
@@ -70,11 +72,5 @@ export interface RunCliOptions {
|
|
|
70
72
|
}
|
|
71
73
|
export declare function runOnce(opts: RunCliOptions): Promise<number>;
|
|
72
74
|
/** Wrap a system-prompt fn to inject Level-6 context (project map etc.) + KLYRO.md (4.4). */
|
|
73
|
-
export declare function makeRunSystemPrompt(cwd: string, base:
|
|
74
|
-
cwd: string;
|
|
75
|
-
telemetry?: string;
|
|
76
|
-
}) => string): Promise<(ctx: {
|
|
77
|
-
cwd: string;
|
|
78
|
-
telemetry?: string;
|
|
79
|
-
}) => string>;
|
|
75
|
+
export declare function makeRunSystemPrompt(cwd: string, base: SystemPromptFn): Promise<SystemPromptFn>;
|
|
80
76
|
export declare function loadTranscript(path: string): Message[];
|
package/dist/cli/run.js
CHANGED
|
@@ -17,6 +17,7 @@ import { builtinRegistry } from '../tools/registry.js';
|
|
|
17
17
|
import { builtinRules, clonePolicyConfig, PolicyEngine } from '../policy/engine.js';
|
|
18
18
|
import { DenyAllApprovalPrompt } from '../policy/approval.js';
|
|
19
19
|
import { buildLevel6Context } from '../context/level6.js';
|
|
20
|
+
import { memoryBlock } from '../context/memory.js';
|
|
20
21
|
import { resolveSessionId } from '../persistence/session.js';
|
|
21
22
|
import * as fs from 'node:fs';
|
|
22
23
|
function readEnv(name, fallback) {
|
|
@@ -97,15 +98,29 @@ export async function runOnce(opts) {
|
|
|
97
98
|
catch {
|
|
98
99
|
/* ignore — engine defaults stand */
|
|
99
100
|
}
|
|
100
|
-
// Best-effort MCP tools: never fatal, never prompts
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
101
|
+
// Best-effort MCP tools: never fatal, never prompts (headless cannot
|
|
102
|
+
// approve). Project-sourced servers auto-connect ONLY when their exact
|
|
103
|
+
// spec hash is already in the McpTrust store (e.g. approved in a prior
|
|
104
|
+
// REPL session); unknown specs are skipped with a warning.
|
|
104
105
|
let closeMcp;
|
|
105
106
|
try {
|
|
106
|
-
// @ts-ignore — sibling-owned module may not exist yet
|
|
107
107
|
const { loadAndRegisterMcp } = await import('../mcp/registry.js');
|
|
108
|
-
const
|
|
108
|
+
const { McpTrust, hashSpec } = await import('../mcp/trust.js');
|
|
109
|
+
const { loadMcpServers } = await import('../mcp/config.js');
|
|
110
|
+
const mcpTrust = new McpTrust();
|
|
111
|
+
const mcpSpecs = loadMcpServers(opts.cwd).servers;
|
|
112
|
+
const mcp = await loadAndRegisterMcp({
|
|
113
|
+
cwd: opts.cwd,
|
|
114
|
+
registry,
|
|
115
|
+
policy,
|
|
116
|
+
approveProjectServer: async ({ name }) => {
|
|
117
|
+
const spec = mcpSpecs[name];
|
|
118
|
+
if (spec && mcpTrust.isTrusted(name, hashSpec(spec)))
|
|
119
|
+
return true;
|
|
120
|
+
stderr.write(`klyro: mcp project server "${name}" not in trust store (skipped — headless cannot prompt)\n`);
|
|
121
|
+
return false;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
109
124
|
for (const e of mcp.errors)
|
|
110
125
|
stderr.write(`klyro: mcp ${e.server}: ${e.message}\n`);
|
|
111
126
|
if (mcp.registered.length > 0 && output === 'human') {
|
|
@@ -179,6 +194,7 @@ export async function runOnce(opts) {
|
|
|
179
194
|
maxRepairAttempts: opts.maxRepairAttempts ?? 3,
|
|
180
195
|
timeoutMs: opts.verifyTimeoutMs,
|
|
181
196
|
requireVerify: opts.requireVerify,
|
|
197
|
+
...(opts.verifyMode ? { mode: opts.verifyMode } : {}),
|
|
182
198
|
};
|
|
183
199
|
let result;
|
|
184
200
|
// P1.4 — if --agent is requested, stand up a parent orchestrator so the
|
|
@@ -297,6 +313,7 @@ export async function runOnce(opts) {
|
|
|
297
313
|
const statusMap = {
|
|
298
314
|
complete: 'complete',
|
|
299
315
|
max_steps: 'max_steps',
|
|
316
|
+
limit: 'max_steps',
|
|
300
317
|
aborted: 'aborted',
|
|
301
318
|
no_final: 'aborted',
|
|
302
319
|
verify_failed: 'verify_failed',
|
|
@@ -320,7 +337,21 @@ export async function runOnce(opts) {
|
|
|
320
337
|
stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps }) + '\n');
|
|
321
338
|
else
|
|
322
339
|
stderr.write(`klyro: hit max steps (${result.steps}); consider raising --max-steps\n`);
|
|
323
|
-
return
|
|
340
|
+
return 7;
|
|
341
|
+
}
|
|
342
|
+
if (result.status === 'limit') {
|
|
343
|
+
if (output === 'json')
|
|
344
|
+
stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps, text: result.finalText }) + '\n');
|
|
345
|
+
else
|
|
346
|
+
stderr.write(`klyro: stopped early: ${result.finalText || result.status} (after ${result.steps} steps)\n`);
|
|
347
|
+
return 7;
|
|
348
|
+
}
|
|
349
|
+
if (result.status === 'stuck') {
|
|
350
|
+
if (output === 'json')
|
|
351
|
+
stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps }) + '\n');
|
|
352
|
+
else
|
|
353
|
+
stderr.write(`klyro: stuck — repeated the same action with no progress; aborting after ${result.steps} steps\n`);
|
|
354
|
+
return 7;
|
|
324
355
|
}
|
|
325
356
|
if (result.status === 'aborted') {
|
|
326
357
|
return 130;
|
|
@@ -328,14 +359,14 @@ export async function runOnce(opts) {
|
|
|
328
359
|
if (result.status === 'no_final') {
|
|
329
360
|
if (output !== 'json')
|
|
330
361
|
stderr.write('klyro: provider error — no final answer\n');
|
|
331
|
-
return
|
|
362
|
+
return 5;
|
|
332
363
|
}
|
|
333
364
|
if (result.status === 'verify_failed') {
|
|
334
365
|
if (output === 'json')
|
|
335
366
|
stdout.write(JSON.stringify({ kind: 'final', status: 'verify_failed', failureType: result.verification?.failureType }) + '\n');
|
|
336
367
|
else
|
|
337
368
|
stderr.write(`klyro: verification failed after ${result.verification?.attempts ?? 3} repairs — see output above\n`);
|
|
338
|
-
return
|
|
369
|
+
return 8;
|
|
339
370
|
}
|
|
340
371
|
// 6.5 — --require-verify: if edits were made but verification never passed, exit 8
|
|
341
372
|
if (opts.requireVerify && result.verification && !result.verification.ok) {
|
|
@@ -361,7 +392,9 @@ async function dryRunReport(opts) {
|
|
|
361
392
|
// Assemble the REAL prompt (Level-6 context + KLYRO.md), not the bare base —
|
|
362
393
|
// otherwise dry-run shows a different prompt than production runs use.
|
|
363
394
|
const systemPromptFn = await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt);
|
|
364
|
-
const
|
|
395
|
+
const { resolveSystemPrompt } = await import('../agent/runtime.js');
|
|
396
|
+
const { system, suffix } = resolveSystemPrompt(systemPromptFn, { cwd: opts.cwd });
|
|
397
|
+
const systemPrompt = suffix ? `${system}\n\n${suffix}` : system;
|
|
365
398
|
const registry = builtinRegistry();
|
|
366
399
|
const rules = builtinRules();
|
|
367
400
|
const report = {
|
|
@@ -386,35 +419,53 @@ function defaultRunSystemPrompt(_ctx) {
|
|
|
386
419
|
'Solve it by calling tools as needed. When done, produce a short final text answer.',
|
|
387
420
|
'Do not invent file paths. Do not call tools outside the working directory.',
|
|
388
421
|
].join(' ');
|
|
389
|
-
return _ctx.telemetry ?
|
|
422
|
+
return _ctx.telemetry ? { system: base, suffix: _ctx.telemetry } : { system: base };
|
|
390
423
|
}
|
|
391
424
|
/** Wrap a system-prompt fn to inject Level-6 context (project map etc.) + KLYRO.md (4.4). */
|
|
392
425
|
export async function makeRunSystemPrompt(cwd, base) {
|
|
393
426
|
const ctxBlock = await buildLevel6Context({ cwd });
|
|
394
427
|
const prefix = ctxBlock.formatted ? `\n\n<context>\n${ctxBlock.formatted}\n</context>` : '';
|
|
428
|
+
// Session memory: .klyro/memory/session-notes.md injected so memory_write
|
|
429
|
+
// actually takes effect (redacted at write time — see context/memory.ts).
|
|
430
|
+
const memBlock = memoryBlock(cwd);
|
|
395
431
|
let klyroBlock = '';
|
|
396
432
|
try {
|
|
397
|
-
// P0.3 trust gate
|
|
398
|
-
//
|
|
433
|
+
// P0.3 trust gate with headless approve-if-store-known: approve(file) is
|
|
434
|
+
// trust.isTrusted(file), so REPL-persisted approvals are honored with no
|
|
435
|
+
// prompting; unknown/changed files stay excluded and each exclusion is
|
|
436
|
+
// bus-visible.
|
|
399
437
|
const { loadKlyroMdFiles } = await import('../context/klyro-md.js');
|
|
400
438
|
const { ContextTrust } = await import('../context/trust.js');
|
|
401
439
|
const { globalBus } = await import('../events/bus.js');
|
|
440
|
+
const trust = new ContextTrust();
|
|
441
|
+
// Headless approve-if-store-known (no prompts): approve(file) is
|
|
442
|
+
// trust.isTrusted(file), so REPL-persisted approvals are honored;
|
|
443
|
+
// unknown/changed files stay excluded.
|
|
444
|
+
const approve = (file) => trust.isTrusted(file);
|
|
402
445
|
const files = await loadKlyroMdFiles(cwd);
|
|
403
|
-
const { trusted, untrusted } =
|
|
446
|
+
const { trusted, untrusted } = trust.check(files);
|
|
404
447
|
for (const { file, reason } of untrusted) {
|
|
405
448
|
globalBus.emit({ type: 'context.trust_prompt', ts: Date.now(), sessionId: 'ephemeral', path: file.path, reason, trusted: false });
|
|
406
449
|
}
|
|
407
450
|
if (untrusted.length > 0) {
|
|
408
451
|
stderr.write(`klyro: trust gate excluded ${untrusted.length} unapproved context file(s): ${untrusted.map((u) => u.file.path).join(', ')}\n`);
|
|
409
452
|
}
|
|
410
|
-
const
|
|
453
|
+
const keptFiles = trusted.filter(approve);
|
|
454
|
+
const kept = keptFiles.map((f) => `# ${f.path}\n${f.content}`).join('\n\n---\n\n');
|
|
411
455
|
if (kept)
|
|
412
456
|
klyroBlock = `\n\n<KLYRO.md>\n${kept.slice(0, 4000)}\n</KLYRO.md>`;
|
|
413
457
|
}
|
|
414
458
|
catch { /* ignore */ }
|
|
415
459
|
return (ctx) => {
|
|
416
|
-
const
|
|
417
|
-
|
|
460
|
+
const r = base(ctx);
|
|
461
|
+
if (typeof r === 'string') {
|
|
462
|
+
// Legacy string fn: keep the exact legacy concatenation behavior.
|
|
463
|
+
const t = ctx.telemetry ? '\n\n' + ctx.telemetry : '';
|
|
464
|
+
return r + prefix + klyroBlock + memBlock + t;
|
|
465
|
+
}
|
|
466
|
+
// Split shape: Level-6/KLYRO.md join the stable prefix; telemetry stays
|
|
467
|
+
// the volatile suffix for cache-friendly adapters.
|
|
468
|
+
return { system: r.system + prefix + klyroBlock + memBlock, ...(r.suffix !== undefined ? { suffix: r.suffix } : {}) };
|
|
418
469
|
};
|
|
419
470
|
}
|
|
420
471
|
export function loadTranscript(path) {
|
package/dist/cli/update.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* klyro update — check registry for newer version, cached 24h.
|
|
3
3
|
* Env KLYRO_NO_UPDATE_CHECK=1 disables.
|
|
4
|
+
*
|
|
5
|
+
* Integrity: before recommending `npm i`, we verify the tarball's SRI hash
|
|
6
|
+
* (sha512) against the registry's recorded `dist.integrity`. An install is
|
|
7
|
+
* only recommended when the download hash matches, so a tampered CDN or
|
|
8
|
+
* MITM registry response can't push a malicious binary to the operator.
|
|
4
9
|
*/
|
|
5
10
|
export declare function checkForUpdate(current: string): Promise<string | null>;
|
|
6
11
|
export declare function runUpdate(): Promise<number>;
|
package/dist/cli/update.js
CHANGED
|
@@ -1,15 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* klyro update — check registry for newer version, cached 24h.
|
|
3
3
|
* Env KLYRO_NO_UPDATE_CHECK=1 disables.
|
|
4
|
+
*
|
|
5
|
+
* Integrity: before recommending `npm i`, we verify the tarball's SRI hash
|
|
6
|
+
* (sha512) against the registry's recorded `dist.integrity`. An install is
|
|
7
|
+
* only recommended when the download hash matches, so a tampered CDN or
|
|
8
|
+
* MITM registry response can't push a malicious binary to the operator.
|
|
4
9
|
*/
|
|
5
10
|
import * as fs from 'node:fs/promises';
|
|
6
11
|
import * as path from 'node:path';
|
|
7
12
|
import * as os from 'node:os';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
8
14
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
15
|
+
const REGISTRY_BASE = 'https://registry.npmjs.org/klyro';
|
|
16
|
+
const FETCH_TIMEOUT_MS = 3000;
|
|
17
|
+
const TARBALL_TIMEOUT_MS = 10000;
|
|
9
18
|
function cachePath() {
|
|
19
|
+
// KLYRO_UPDATE_CACHE lets tests isolate the on-disk cache (and power users
|
|
20
|
+
// relocate it); default is the home cache like the rest of Klyro.
|
|
21
|
+
const override = process.env.KLYRO_UPDATE_CACHE;
|
|
22
|
+
if (override)
|
|
23
|
+
return override;
|
|
10
24
|
const home = os.homedir() || process.cwd();
|
|
11
25
|
return path.join(home, '.klyro', 'update-cache.json');
|
|
12
26
|
}
|
|
27
|
+
/** SRI string may carry multiple hashes parsable with `pick`; we accept sha512 or sha256. */
|
|
28
|
+
function parseSRI(integrity) {
|
|
29
|
+
if (!integrity)
|
|
30
|
+
return null;
|
|
31
|
+
for (const part of integrity.split(/\s+/)) {
|
|
32
|
+
const m = /^(sha256|sha512)-([A-Za-z0-9+/=]+)$/.exec(part);
|
|
33
|
+
if (m)
|
|
34
|
+
return { algo: m[1], digest: m[2] };
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/** Fetch with a timeout; rejects on non-2xx. */
|
|
39
|
+
async function fetchWithTimeout(url, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
40
|
+
const ctrl = new AbortController();
|
|
41
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(url, { signal: ctrl.signal });
|
|
44
|
+
if (!res.ok)
|
|
45
|
+
throw new Error(`HTTP ${res.status}`);
|
|
46
|
+
return res;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
clearTimeout(t);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Download the tarball and confirm its hash equals the registry's SRI digest. */
|
|
53
|
+
async function verifyTarballIntegrity(dist) {
|
|
54
|
+
const sri = parseSRI(dist.integrity);
|
|
55
|
+
const tarball = dist.tarball;
|
|
56
|
+
if (!sri || !tarball)
|
|
57
|
+
return false;
|
|
58
|
+
const res = await fetchWithTimeout(tarball, TARBALL_TIMEOUT_MS);
|
|
59
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
60
|
+
const actual = createHash(sri.algo).update(buf).digest('base64');
|
|
61
|
+
return actual === sri.digest;
|
|
62
|
+
}
|
|
13
63
|
export async function checkForUpdate(current) {
|
|
14
64
|
if (process.env.KLYRO_NO_UPDATE_CHECK === '1')
|
|
15
65
|
return null;
|
|
@@ -25,18 +75,20 @@ export async function checkForUpdate(current) {
|
|
|
25
75
|
// ignore
|
|
26
76
|
}
|
|
27
77
|
try {
|
|
28
|
-
const
|
|
29
|
-
const t = setTimeout(() => ctrl.abort(), 3000);
|
|
30
|
-
const res = await fetch('https://registry.npmjs.org/klyro/latest', { signal: ctrl.signal });
|
|
31
|
-
clearTimeout(t);
|
|
32
|
-
if (!res.ok)
|
|
33
|
-
return null;
|
|
78
|
+
const res = await fetchWithTimeout(`${REGISTRY_BASE}/latest`);
|
|
34
79
|
const json = (await res.json());
|
|
35
80
|
const latest = json.version ?? '';
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
81
|
+
if (latest && latest !== current) {
|
|
82
|
+
// Verify the tarball's integrity before caching/recommending this version.
|
|
83
|
+
const verRes = await fetchWithTimeout(`${REGISTRY_BASE}/${encodeURIComponent(latest)}`);
|
|
84
|
+
const verJson = (await verRes.json());
|
|
85
|
+
const ok = await verifyTarballIntegrity(verJson.dist ?? {});
|
|
86
|
+
if (!ok)
|
|
87
|
+
return null; // integrity mismatch — treat as no update, never recommend it
|
|
88
|
+
await fs.mkdir(path.dirname(cache), { recursive: true });
|
|
89
|
+
await fs.writeFile(cache, JSON.stringify({ at: Date.now(), latest }), 'utf-8');
|
|
39
90
|
return latest;
|
|
91
|
+
}
|
|
40
92
|
}
|
|
41
93
|
catch {
|
|
42
94
|
// network failure — silent
|
|
@@ -55,7 +107,7 @@ export async function runUpdate() {
|
|
|
55
107
|
const cur = pkg.version ?? '0.0.0';
|
|
56
108
|
const latest = await checkForUpdate(cur);
|
|
57
109
|
if (latest) {
|
|
58
|
-
process.stdout.write(`Update available: ${cur} → ${latest}\n npm i -g klyro@latest\n`);
|
|
110
|
+
process.stdout.write(`Update available: ${cur} → ${latest} (integrity verified)\n npm i -g klyro@latest\n`);
|
|
59
111
|
}
|
|
60
112
|
else {
|
|
61
113
|
process.stdout.write(`klyro ${cur} is latest\n`);
|
|
@@ -3,6 +3,8 @@ export interface ImportGraph {
|
|
|
3
3
|
edges: Map<string, Set<string>>;
|
|
4
4
|
mtime: number;
|
|
5
5
|
}
|
|
6
|
+
/** Invalidate the import-graph cache (tests + callers that mutate the tree). */
|
|
7
|
+
export declare function clearImportGraphCache(): void;
|
|
6
8
|
export declare function buildImportGraph(cwd: string): Promise<ImportGraph>;
|
|
7
9
|
export declare function importsOf(cwd: string, file: string): Promise<string[]>;
|
|
8
10
|
export declare function importersOf(cwd: string, file: string): Promise<string[]>;
|