klyro 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/agent/anthropic-adapter.d.ts +13 -5
  2. package/dist/agent/anthropic-adapter.js +19 -2
  3. package/dist/agent/capabilities.js +7 -1
  4. package/dist/agent/orchestrator.d.ts +18 -1
  5. package/dist/agent/orchestrator.js +51 -4
  6. package/dist/agent/retry.js +52 -10
  7. package/dist/agent/runtime.d.ts +34 -0
  8. package/dist/agent/runtime.js +164 -14
  9. package/dist/agent/stream-budget.d.ts +36 -0
  10. package/dist/agent/stream-budget.js +121 -0
  11. package/dist/checkpoints/store.d.ts +9 -0
  12. package/dist/checkpoints/store.js +26 -0
  13. package/dist/cli/commit.d.ts +31 -0
  14. package/dist/cli/commit.js +142 -0
  15. package/dist/cli/config.d.ts +45 -0
  16. package/dist/cli/config.js +82 -0
  17. package/dist/cli/doctor.d.ts +1 -0
  18. package/dist/cli/doctor.js +71 -6
  19. package/dist/cli/hooks.d.ts +47 -0
  20. package/dist/cli/hooks.js +181 -0
  21. package/dist/cli/repl.js +41 -1
  22. package/dist/cli/run.d.ts +6 -0
  23. package/dist/cli/run.js +76 -3
  24. package/dist/events/catalog.d.ts +9 -0
  25. package/dist/events/catalog.js +9 -0
  26. package/dist/index.js +89 -5
  27. package/dist/mcp/client.js +1 -1
  28. package/dist/mcp/registry.d.ts +0 -18
  29. package/dist/mcp/registry.js +49 -2
  30. package/dist/policy/engine.d.ts +16 -0
  31. package/dist/policy/engine.js +74 -1
  32. package/dist/policy/path-guard.d.ts +24 -0
  33. package/dist/policy/path-guard.js +46 -0
  34. package/dist/providers/model-info.d.ts +6 -0
  35. package/dist/providers/model-info.js +8 -0
  36. package/dist/tools/fs/apply-patch.js +6 -1
  37. package/dist/tools/fs/edit-file.js +4 -1
  38. package/dist/tools/fs/multi-edit.js +4 -1
  39. package/dist/tools/fs/write-file.js +16 -6
  40. package/dist/tools/plan/todo-write.js +1 -1
  41. package/dist/tools/shell/shell-exec.d.ts +28 -0
  42. package/dist/tools/shell/shell-exec.js +87 -1
  43. package/dist/trace/writer.d.ts +7 -0
  44. package/dist/trace/writer.js +7 -0
  45. package/dist/verification/classify.js +4 -3
  46. package/dist/verification/engine.d.ts +8 -0
  47. package/dist/verification/engine.js +25 -0
  48. package/dist/verification/registry.js +16 -5
  49. package/dist/verification/scoped.js +36 -5
  50. package/package.json +1 -1
@@ -4,6 +4,7 @@
4
4
  import * as fs from 'node:fs';
5
5
  import * as path from 'node:path';
6
6
  import { spawn } from 'node:child_process';
7
+ import { filteredVerifyEnv } from './registry.js';
7
8
  import { detect } from './detect.js';
8
9
  function signatureOf(failure, fallbackExit) {
9
10
  return {
@@ -48,7 +49,7 @@ export function classifyFailure(current, baseline, flakyRerunOk) {
48
49
  }
49
50
  export async function rerunOnce(cwd, command, timeoutMs = 45_000) {
50
51
  return new Promise((resolve) => {
51
- const child = spawn(command, { cwd, shell: true, env: process.env });
52
+ const child = spawn(command, { cwd, shell: true, env: filteredVerifyEnv() });
52
53
  let done = false;
53
54
  const t = setTimeout(() => { if (!done) {
54
55
  done = true;
@@ -92,7 +93,7 @@ export async function gatherRepairContext(cwd, failure) {
92
93
  }
93
94
  function execCapturePipe(cwd, cmd) {
94
95
  return new Promise((resolve) => {
95
- const child = spawn(cmd, { cwd, shell: true, env: process.env });
96
+ const child = spawn(cmd, { cwd, shell: true, env: filteredVerifyEnv() });
96
97
  const chunks = [];
97
98
  child.stdout.on('data', (b) => { chunks.push(b); });
98
99
  child.stderr.on('data', (b) => { chunks.push(b); });
@@ -102,7 +103,7 @@ function execCapturePipe(cwd, cmd) {
102
103
  }
103
104
  function execCaptureArgv(cwd, file, args) {
104
105
  return new Promise((resolve) => {
105
- const child = spawn('git', [...args, file], { cwd, shell: false, env: process.env });
106
+ const child = spawn('git', [...args, file], { cwd, shell: false, env: filteredVerifyEnv() });
106
107
  const chunks = [];
107
108
  child.stdout.on('data', (b) => { chunks.push(b); });
108
109
  child.stderr.on('data', (b) => { chunks.push(b); });
@@ -8,10 +8,18 @@
8
8
  * structured Failure (and the model has had up to N attempts to fix).
9
9
  */
10
10
  import { type Failure } from './detect.js';
11
+ /** Verification mode. Canonical definition — agent/runtime.ts converges to
12
+ * this type automatically once this export exists (see its conditional
13
+ * VerifyMode). Backward-compatible: pure type, no runtime change. */
14
+ export type VerifyMode = 'strict' | 'advisory' | 'off';
11
15
  export interface VerifyOptions {
12
16
  cwd: string;
13
17
  command: string;
14
18
  timeoutMs?: number;
19
+ /** Owning session for `verification.*` bus events. Optional for backward
20
+ * compatibility — siblings pass `sessionId` at verify call sites; when
21
+ * absent the emitted events fall back to 'ephemeral'. */
22
+ sessionId?: string;
15
23
  }
16
24
  export interface VerifyResult {
17
25
  ok: boolean;
@@ -11,6 +11,7 @@ import { spawn } from 'node:child_process';
11
11
  import { detect, summarize } from './detect.js';
12
12
  import { filteredVerifyEnv } from './registry.js';
13
13
  import { redact } from '../policy/secret-redactor.js';
14
+ import { globalBus } from '../events/bus.js';
14
15
  const MAX_VERIFY_BYTES = 256 * 1024;
15
16
  function appendCapped(current, chunk) {
16
17
  if (current.length >= MAX_VERIFY_BYTES)
@@ -48,9 +49,30 @@ const DANGEROUS_VERIFY_PATTERNS = [
48
49
  { pattern: /\|\|\s*rm\s+-rf/, reason: 'chained rm -rf' },
49
50
  ];
50
51
  export async function verify(opts) {
52
+ const sid = opts.sessionId ?? 'ephemeral';
53
+ const emitStarted = () => {
54
+ try {
55
+ globalBus.emit({ type: 'verification.started', ts: Date.now(), sessionId: sid, command: opts.command });
56
+ }
57
+ catch { /* bus never throws */ }
58
+ };
59
+ const emitSucceeded = () => {
60
+ try {
61
+ globalBus.emit({ type: 'verification.succeeded', ts: Date.now(), sessionId: sid, command: opts.command });
62
+ }
63
+ catch { /* bus never throws */ }
64
+ };
65
+ const emitFailed = (reason) => {
66
+ try {
67
+ globalBus.emit({ type: 'verification.failed', ts: Date.now(), sessionId: sid, command: opts.command, reason });
68
+ }
69
+ catch { /* bus never throws */ }
70
+ };
71
+ emitStarted();
51
72
  // SEC-004: reject dangerous patterns before spawning
52
73
  for (const { pattern, reason } of DANGEROUS_VERIFY_PATTERNS) {
53
74
  if (pattern.test(opts.command)) {
75
+ emitFailed(`Command blocked: ${reason}`);
54
76
  return {
55
77
  ok: false,
56
78
  exitCode: -1,
@@ -75,6 +97,7 @@ export async function verify(opts) {
75
97
  const so = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
76
98
  const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
77
99
  const raw = se + '\n' + so;
100
+ emitFailed('[verify timeout]');
78
101
  resolve({
79
102
  ok: false,
80
103
  exitCode: -1,
@@ -96,10 +119,12 @@ export async function verify(opts) {
96
119
  const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
97
120
  const exit = typeof code === 'number' ? code : -1;
98
121
  if (exit === 0) {
122
+ emitSucceeded();
99
123
  resolve({ ok: true, exitCode: 0, stdout: so, stderr: se });
100
124
  return;
101
125
  }
102
126
  const failure = detect(so, se, exit);
127
+ emitFailed(failure.type);
103
128
  resolve({ ok: false, exitCode: exit, stdout: so, stderr: se, failure });
104
129
  });
105
130
  });
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import * as fs from 'node:fs';
7
7
  import * as path from 'node:path';
8
+ import { filteredEnv as siblingFilteredEnv } from '../tools/shell/shell-exec.js';
8
9
  function exists(p) {
9
10
  try {
10
11
  return fs.existsSync(p);
@@ -115,10 +116,13 @@ export function primaryVerifyCommand(cwd) {
115
116
  }
116
117
  // S2 — filtered env for ALL spawned verify commands.
117
118
  //
118
- // Sibling `tools/shell/shell-exec.ts` owns an equivalent `filteredEnv` but
119
- // does not export it, so this mirrors that filter (allowlisted prefixes +
120
- // exact keys, secret-ish names stripped, preload vectors deleted) instead of
121
- // importing sibling internals.
119
+ // Delegates to the sibling-owned `filteredEnv` from
120
+ // `tools/shell/shell-exec.ts` (EXACT export name — verified via grep) for
121
+ // the allowlist base, then applies this module's stricter secret strip
122
+ // (sibling only strips SECRET/TOKEN + named keys; verify children also
123
+ // strip KEY-shaped names like NODE_API_KEY) plus the Windows shell vars a
124
+ // spawned shell needs (SystemRoot/COMSPEC/...) and preload-vector deletes.
125
+ // `filteredVerifyEnv` already exists — reuse it, don't duplicate it.
122
126
  const VERIFY_ENV_PREFIXES = ['PATH', 'HOME', 'USER', 'SHELL', 'TERM', 'LANG', 'NODE_', 'NPM_', 'PNPM_', 'YARN_'];
123
127
  // Exact keys: sibling's PWD/TMPDIR/TEMP plus the Windows OS vars a spawned
124
128
  // shell needs to start (COMSPEC/SystemRoot/PATHEXT/...) — none carry secrets.
@@ -130,7 +134,14 @@ const VERIFY_ENV_EXACT = new Set([
130
134
  const VERIFY_ENV_SECRET_RE = /SECRET|TOKEN|KEY/i;
131
135
  /** Filtered env for verify children: allowlist + secret strip + preload-vector delete. */
132
136
  export function filteredVerifyEnv() {
133
- const out = {};
137
+ // Sibling-owned base (allowlisted prefixes + secret strip).
138
+ const out = { ...siblingFilteredEnv() };
139
+ // Harden beyond the sibling: strip KEY-shaped names the sibling passes
140
+ // through (e.g. NODE_API_KEY) before overlaying our own allowlist.
141
+ for (const k of Object.keys(out)) {
142
+ if (VERIFY_ENV_SECRET_RE.test(k))
143
+ delete out[k];
144
+ }
134
145
  for (const [k, v] of Object.entries(process.env)) {
135
146
  if (v === undefined)
136
147
  continue;
@@ -78,14 +78,45 @@ export function buildScopedCommand(cwd, baseCommand, relatedTests) {
78
78
  if (baseCommand.includes('pytest')) {
79
79
  return `pytest ${safe.map((f) => `"${f}"`).join(' ')}`;
80
80
  }
81
- if (baseCommand.includes('go test')) {
82
- // go test ./... with file filter — fallback to full
83
- return null;
84
- }
85
81
  if (baseCommand.includes('cargo test')) {
86
- // cargo test --test <name>
82
+ // Cargo keeps the full suite: `cargo test` selects by test-target name
83
+ // (`--test <name>`), lib/bin unit-test filters, or `-- <filter>` args —
84
+ // none of which map 1:1 onto file paths. Interpolating a file path as a
85
+ // filter would silently run the WRONG subset (substring match against
86
+ // test names), so fall back to the full suite rather than lie about
87
+ // coverage. (Checked before the `go test` branch: "cargo test" contains
88
+ // the substring "go test".)
87
89
  return null;
88
90
  }
91
+ // NOTE: `baseCommand.includes('go test')` must stay AFTER the cargo branch
92
+ // above — "cargo test" contains "go test" as a substring.
93
+ if (baseCommand.includes('go test')) {
94
+ // Go has no file-scoped test selection: `go test` takes package
95
+ // patterns, not file paths (passing `foo_test.go` directly errors with
96
+ // "no Go files in ..."). Derive `go test ./<pkgdirs...>` from the
97
+ // directories of the related test files instead: dedupe, cap 10, and
98
+ // reuse the same containment-validated `safe` list as every other
99
+ // branch (untrusted paths never reach the shell).
100
+ const dirs = [];
101
+ const seen = new Set();
102
+ for (const f of safe) {
103
+ const d = path.dirname(f);
104
+ const pkg = d === '.' ? '.' : `./${d.split(path.sep).join('/')}`;
105
+ if (!seen.has(pkg)) {
106
+ seen.add(pkg);
107
+ dirs.push(pkg);
108
+ }
109
+ if (dirs.length >= 10)
110
+ break;
111
+ }
112
+ if (dirs.length === 0)
113
+ return null;
114
+ // Preserve a './...'-style base by replacing it; otherwise append.
115
+ if (baseCommand.includes('./...')) {
116
+ return baseCommand.replace('./...', dirs.join(' '));
117
+ }
118
+ return `${baseCommand} ${dirs.join(' ')}`;
119
+ }
89
120
  return null;
90
121
  }
91
122
  const MAX_SCOPED_BYTES = 256 * 1024;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Klyro — autonomous coding harness CLI that streams from any OpenAI-compatible or Anthropic LLM endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",