@corenel/harness 0.1.0 → 0.3.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 (180) hide show
  1. package/dist/acp/agentMap.d.ts +34 -0
  2. package/dist/acp/agentMap.d.ts.map +1 -0
  3. package/dist/acp/agentMap.js +110 -0
  4. package/dist/acp/agentMap.js.map +1 -0
  5. package/dist/acp/index.d.ts +4 -0
  6. package/dist/acp/index.d.ts.map +1 -0
  7. package/dist/acp/index.js +6 -0
  8. package/dist/acp/index.js.map +1 -0
  9. package/dist/acp/policyMap.d.ts +55 -0
  10. package/dist/acp/policyMap.d.ts.map +1 -0
  11. package/dist/acp/policyMap.js +121 -0
  12. package/dist/acp/policyMap.js.map +1 -0
  13. package/dist/acp/types.d.ts +41 -0
  14. package/dist/acp/types.d.ts.map +1 -0
  15. package/dist/acp/types.js +14 -0
  16. package/dist/acp/types.js.map +1 -0
  17. package/dist/api/backend.d.ts +39 -0
  18. package/dist/api/backend.d.ts.map +1 -0
  19. package/dist/api/backend.js +126 -0
  20. package/dist/api/backend.js.map +1 -0
  21. package/dist/api/client.d.ts +95 -0
  22. package/dist/api/client.d.ts.map +1 -0
  23. package/dist/api/client.js +212 -0
  24. package/dist/api/client.js.map +1 -0
  25. package/dist/api/fileMirrorSource.d.ts +24 -0
  26. package/dist/api/fileMirrorSource.d.ts.map +1 -0
  27. package/dist/api/fileMirrorSource.js +59 -0
  28. package/dist/api/fileMirrorSource.js.map +1 -0
  29. package/dist/api/mirror.d.ts +102 -0
  30. package/dist/api/mirror.d.ts.map +1 -0
  31. package/dist/api/mirror.js +204 -0
  32. package/dist/api/mirror.js.map +1 -0
  33. package/dist/api/mirrorScheduler.d.ts +56 -0
  34. package/dist/api/mirrorScheduler.d.ts.map +1 -0
  35. package/dist/api/mirrorScheduler.js +167 -0
  36. package/dist/api/mirrorScheduler.js.map +1 -0
  37. package/dist/conversation.d.ts +0 -11
  38. package/dist/conversation.d.ts.map +1 -1
  39. package/dist/conversation.js +1 -24
  40. package/dist/conversation.js.map +1 -1
  41. package/dist/core/budget.d.ts +40 -0
  42. package/dist/core/budget.d.ts.map +1 -0
  43. package/dist/core/budget.js +62 -0
  44. package/dist/core/budget.js.map +1 -0
  45. package/dist/core/loop.d.ts +11 -0
  46. package/dist/core/loop.d.ts.map +1 -1
  47. package/dist/core/loop.js +89 -20
  48. package/dist/core/loop.js.map +1 -1
  49. package/dist/core/pricing.d.ts +8 -0
  50. package/dist/core/pricing.d.ts.map +1 -0
  51. package/dist/core/pricing.js +12 -0
  52. package/dist/core/pricing.js.map +1 -0
  53. package/dist/core/recovery.d.ts +60 -0
  54. package/dist/core/recovery.d.ts.map +1 -0
  55. package/dist/core/recovery.js +128 -0
  56. package/dist/core/recovery.js.map +1 -0
  57. package/dist/guardrail/builtins.js +6 -6
  58. package/dist/guardrail/builtins.js.map +1 -1
  59. package/dist/guardrail/permission-service.js +1 -1
  60. package/dist/guardrail/permission-service.js.map +1 -1
  61. package/dist/guardrail/serialize.d.ts.map +1 -1
  62. package/dist/guardrail/serialize.js +6 -0
  63. package/dist/guardrail/serialize.js.map +1 -1
  64. package/dist/guardrail/validate.d.ts.map +1 -1
  65. package/dist/guardrail/validate.js +13 -0
  66. package/dist/guardrail/validate.js.map +1 -1
  67. package/dist/install/lockfile.d.ts +79 -0
  68. package/dist/install/lockfile.d.ts.map +1 -0
  69. package/dist/install/lockfile.js +155 -0
  70. package/dist/install/lockfile.js.map +1 -0
  71. package/dist/install/store.d.ts +13 -0
  72. package/dist/install/store.d.ts.map +1 -0
  73. package/dist/install/store.js +38 -0
  74. package/dist/install/store.js.map +1 -0
  75. package/dist/log.d.ts.map +1 -1
  76. package/dist/log.js +6 -1
  77. package/dist/log.js.map +1 -1
  78. package/dist/prompts/host.d.ts +4 -0
  79. package/dist/prompts/host.d.ts.map +1 -1
  80. package/dist/prompts/host.js.map +1 -1
  81. package/dist/prompts/index.d.ts +6 -0
  82. package/dist/prompts/index.d.ts.map +1 -1
  83. package/dist/prompts/index.js +16 -6
  84. package/dist/prompts/index.js.map +1 -1
  85. package/dist/prompts/library/defaults.generated.d.ts.map +1 -1
  86. package/dist/prompts/library/defaults.generated.js +22 -4
  87. package/dist/prompts/library/defaults.generated.js.map +1 -1
  88. package/dist/prompts/toolsMarker.d.ts +13 -0
  89. package/dist/prompts/toolsMarker.d.ts.map +1 -0
  90. package/dist/prompts/toolsMarker.js +21 -0
  91. package/dist/prompts/toolsMarker.js.map +1 -0
  92. package/dist/providers/gateway.d.ts +19 -1
  93. package/dist/providers/gateway.d.ts.map +1 -1
  94. package/dist/providers/gateway.js +33 -4
  95. package/dist/providers/gateway.js.map +1 -1
  96. package/dist/services.d.ts +13 -0
  97. package/dist/services.d.ts.map +1 -1
  98. package/dist/services.js +2 -0
  99. package/dist/services.js.map +1 -1
  100. package/dist/sessions/apiLeaseStore.d.ts +10 -0
  101. package/dist/sessions/apiLeaseStore.d.ts.map +1 -0
  102. package/dist/sessions/apiLeaseStore.js +41 -0
  103. package/dist/sessions/apiLeaseStore.js.map +1 -0
  104. package/dist/sessions/fileRecallStore.d.ts +33 -0
  105. package/dist/sessions/fileRecallStore.d.ts.map +1 -0
  106. package/dist/sessions/fileRecallStore.js +112 -0
  107. package/dist/sessions/fileRecallStore.js.map +1 -0
  108. package/dist/sessions/fileSessionStore.d.ts +20 -0
  109. package/dist/sessions/fileSessionStore.d.ts.map +1 -0
  110. package/dist/sessions/fileSessionStore.js +151 -0
  111. package/dist/sessions/fileSessionStore.js.map +1 -0
  112. package/dist/sessions/head.d.ts +90 -0
  113. package/dist/sessions/head.d.ts.map +1 -0
  114. package/dist/sessions/head.js +82 -0
  115. package/dist/sessions/head.js.map +1 -0
  116. package/dist/sessions/inflight.d.ts +58 -0
  117. package/dist/sessions/inflight.d.ts.map +1 -0
  118. package/dist/sessions/inflight.js +0 -0
  119. package/dist/sessions/inflight.js.map +1 -0
  120. package/dist/sessions/jsonl.d.ts +23 -0
  121. package/dist/sessions/jsonl.d.ts.map +1 -0
  122. package/dist/sessions/jsonl.js +52 -0
  123. package/dist/sessions/jsonl.js.map +1 -0
  124. package/dist/sessions/lease.d.ts +59 -0
  125. package/dist/sessions/lease.d.ts.map +1 -0
  126. package/dist/sessions/lease.js +115 -0
  127. package/dist/sessions/lease.js.map +1 -0
  128. package/dist/sessions/leaseStore.d.ts +46 -0
  129. package/dist/sessions/leaseStore.d.ts.map +1 -0
  130. package/dist/sessions/leaseStore.js +58 -0
  131. package/dist/sessions/leaseStore.js.map +1 -0
  132. package/dist/sessions/testFs.d.ts +5 -0
  133. package/dist/sessions/testFs.d.ts.map +1 -0
  134. package/dist/sessions/testFs.js +85 -0
  135. package/dist/sessions/testFs.js.map +1 -0
  136. package/dist/sessions/timeline.d.ts +134 -0
  137. package/dist/sessions/timeline.d.ts.map +1 -0
  138. package/dist/sessions/timeline.js +127 -0
  139. package/dist/sessions/timeline.js.map +1 -0
  140. package/dist/sidecar/host.d.ts +284 -2
  141. package/dist/sidecar/host.d.ts.map +1 -1
  142. package/dist/sidecar/host.js +725 -1
  143. package/dist/sidecar/host.js.map +1 -1
  144. package/dist/sidecar/server.d.ts +217 -2
  145. package/dist/sidecar/server.d.ts.map +1 -1
  146. package/dist/sidecar/server.js +828 -3
  147. package/dist/sidecar/server.js.map +1 -1
  148. package/dist/skills.d.ts +8 -3
  149. package/dist/skills.d.ts.map +1 -1
  150. package/dist/skills.js +71 -35
  151. package/dist/skills.js.map +1 -1
  152. package/dist/snapshot/effects.d.ts +69 -0
  153. package/dist/snapshot/effects.d.ts.map +1 -0
  154. package/dist/snapshot/effects.js +67 -0
  155. package/dist/snapshot/effects.js.map +1 -0
  156. package/dist/snapshot/manifest.d.ts +73 -0
  157. package/dist/snapshot/manifest.d.ts.map +1 -0
  158. package/dist/snapshot/manifest.js +141 -0
  159. package/dist/snapshot/manifest.js.map +1 -0
  160. package/dist/snapshot/tree.d.ts +62 -0
  161. package/dist/snapshot/tree.d.ts.map +1 -0
  162. package/dist/snapshot/tree.js +147 -0
  163. package/dist/snapshot/tree.js.map +1 -0
  164. package/dist/tools/builtins.d.ts +1 -0
  165. package/dist/tools/builtins.d.ts.map +1 -1
  166. package/dist/tools/builtins.js +150 -5
  167. package/dist/tools/builtins.js.map +1 -1
  168. package/dist/tools/files.d.ts.map +1 -1
  169. package/dist/tools/files.js +13 -0
  170. package/dist/tools/files.js.map +1 -1
  171. package/dist/tools/http.d.ts.map +1 -1
  172. package/dist/tools/http.js.map +1 -1
  173. package/dist/tools/namespaces.d.ts +1 -1
  174. package/dist/tools/namespaces.d.ts.map +1 -1
  175. package/dist/tools/namespaces.js +24 -0
  176. package/dist/tools/namespaces.js.map +1 -1
  177. package/dist/tools/todo.d.ts.map +1 -1
  178. package/dist/tools/todo.js +12 -0
  179. package/dist/tools/todo.js.map +1 -1
  180. package/package.json +26 -34
@@ -0,0 +1,141 @@
1
+ export const SNAPSHOT_VERSION = 1;
2
+ /**
3
+ * Strip anything that looks like a secret VALUE from provider config.
4
+ *
5
+ * Defense in depth, not a substitute for callers behaving: provider settings are
6
+ * a loose bag, and a snapshot is a thing people mail around. A key that survives
7
+ * into a manifest is a key in someone's git history. Keys are matched by name
8
+ * because that is the only signal available in an untyped record.
9
+ */
10
+ const SECRET_WORD = /(^|_)(key|secret|token|password|passwd|credential|credentials|authorization|auth|bearer|apikey)($|_)/;
11
+ /**
12
+ * Normalize a key so camelCase reads like snake_case before matching.
13
+ *
14
+ * Without this, `apiKey` — the single most common spelling of the thing this
15
+ * function exists to catch — sailed straight through, because `key` was not
16
+ * preceded by a separator. Both boundaries are needed: lower-to-upper for
17
+ * `apiKey`, and the caps-run split for `APIKey`.
18
+ */
19
+ function normalizeKey(key) {
20
+ return key
21
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
22
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
23
+ .replace(/-/g, '_')
24
+ .toLowerCase();
25
+ }
26
+ function isSecretKey(key) {
27
+ return SECRET_WORD.test(normalizeKey(key));
28
+ }
29
+ export function redactProvider(provider) {
30
+ if (!provider)
31
+ return null;
32
+ const out = {};
33
+ for (const key of Object.keys(provider).sort()) {
34
+ if (isSecretKey(key))
35
+ continue;
36
+ const v = provider[key];
37
+ // Nested objects get the same treatment; anything else is copied as-is.
38
+ out[key] = (typeof v === 'object' && v !== null && !Array.isArray(v))
39
+ ? redactProvider(v)
40
+ : v;
41
+ }
42
+ return out;
43
+ }
44
+ export function buildManifest(input) {
45
+ return {
46
+ snapshotVersion: SNAPSHOT_VERSION,
47
+ sessionId: input.sessionId,
48
+ // Unbranched sessions are their own lineage root, which is the common case
49
+ // and must not require every caller to think about it.
50
+ lineageRoot: input.lineageRoot ?? input.sessionId,
51
+ parent: input.parent ?? null,
52
+ createdAt: input.createdAt,
53
+ baseImage: input.baseImage ?? null,
54
+ policy: input.policy ?? null,
55
+ lock: input.lock ?? null,
56
+ provider: redactProvider(input.provider),
57
+ // Names only, deduped and sorted so the manifest is stable.
58
+ env: [...new Set(input.env ?? [])].sort(),
59
+ tree: input.tree,
60
+ };
61
+ }
62
+ /** Canonical text form: fixed key order, sorted env, trailing newline. */
63
+ export function serializeManifest(m) {
64
+ const canonical = {
65
+ snapshotVersion: SNAPSHOT_VERSION,
66
+ sessionId: m.sessionId,
67
+ lineageRoot: m.lineageRoot || m.sessionId,
68
+ parent: m.parent,
69
+ createdAt: m.createdAt,
70
+ baseImage: m.baseImage,
71
+ policy: m.policy ?? null,
72
+ lock: m.lock ?? null,
73
+ provider: m.provider ?? null,
74
+ env: [...new Set(m.env)].sort(),
75
+ tree: m.tree,
76
+ };
77
+ return JSON.stringify(canonical, null, 2) + '\n';
78
+ }
79
+ function isRecord(v) {
80
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
81
+ }
82
+ /**
83
+ * Parse a manifest, or null when the text is not one.
84
+ *
85
+ * Strict about identity (`sessionId`, `tree`) because a snapshot that cannot say
86
+ * what it is or what it contains is unusable. A future `snapshotVersion` is
87
+ * rejected rather than read under rules that no longer apply — the same call the
88
+ * lockfile makes, for the same reason.
89
+ */
90
+ export function parseManifest(text) {
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(text);
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ if (!isRecord(parsed))
99
+ return null;
100
+ if (parsed.snapshotVersion !== SNAPSHOT_VERSION)
101
+ return null;
102
+ if (typeof parsed.sessionId !== 'string' || !parsed.sessionId)
103
+ return null;
104
+ if (typeof parsed.tree !== 'string' || !parsed.tree)
105
+ return null;
106
+ return {
107
+ snapshotVersion: SNAPSHOT_VERSION,
108
+ sessionId: parsed.sessionId,
109
+ // An older manifest has no lineage root; it was written before branching
110
+ // existed, so it IS its own root.
111
+ lineageRoot: typeof parsed.lineageRoot === 'string' && parsed.lineageRoot ? parsed.lineageRoot : parsed.sessionId,
112
+ parent: typeof parsed.parent === 'string' ? parsed.parent : null,
113
+ createdAt: typeof parsed.createdAt === 'number' ? parsed.createdAt : 0,
114
+ baseImage: typeof parsed.baseImage === 'string' ? parsed.baseImage : null,
115
+ policy: parsed.policy ?? null,
116
+ lock: (parsed.lock ?? null),
117
+ provider: isRecord(parsed.provider) ? parsed.provider : null,
118
+ env: Array.isArray(parsed.env) ? [...new Set(parsed.env.filter((e) => typeof e === 'string'))].sort() : [],
119
+ tree: parsed.tree,
120
+ };
121
+ }
122
+ /**
123
+ * Is this manifest safe to rehydrate from?
124
+ *
125
+ * A snapshot with no policy would restore UNGOVERNED. Rather than defaulting to
126
+ * something permissive — or to something strict the user did not choose — this
127
+ * refuses, and the caller decides whether to attach a policy explicitly. Silence
128
+ * here is how an audit trail ends up describing a run nobody authorized.
129
+ */
130
+ export function manifestProblems(m) {
131
+ const problems = [];
132
+ if (m.policy === null || m.policy === undefined) {
133
+ problems.push('no policy — restoring would run ungoverned (see spec s5.1)');
134
+ }
135
+ if (!m.tree)
136
+ problems.push('no tree hash — the snapshot does not say what it contains');
137
+ if (!m.sessionId)
138
+ problems.push('no session id');
139
+ return problems;
140
+ }
141
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../snapshot/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ /** path -> content hash. Sorted on serialize, so the same tree is the same text. */
2
+ export type Tree = Record<string, string>;
3
+ export interface TreeDelta {
4
+ /** Paths present now and absent from the parent. */
5
+ added: string[];
6
+ /** Paths in both, with different content. */
7
+ modified: string[];
8
+ /** Paths in the parent and gone now. A snapshot that cannot express deletion
9
+ * silently resurrects files on restore. */
10
+ removed: string[];
11
+ }
12
+ /** Canonical text form of a tree — sorted paths, trailing newline. */
13
+ export declare function serializeTree(tree: Tree): string;
14
+ export declare function parseTree(text: string): Tree | null;
15
+ /** What changed between two trees. Deterministic: every list is sorted. */
16
+ export declare function diffTrees(parent: Tree, current: Tree): TreeDelta;
17
+ /** True when nothing changed — worth checking before writing a snapshot nobody
18
+ * needs, and before shipping bytes across a wire. */
19
+ export declare function isEmptyDelta(d: TreeDelta): boolean;
20
+ /**
21
+ * The blob hashes a delta actually needs shipped.
22
+ *
23
+ * DEDUPED, which is the point of content addressing: two paths with identical
24
+ * content share one blob, so a copied file or a repeated boilerplate header costs
25
+ * nothing extra. Removals need no blob at all.
26
+ */
27
+ export declare function blobsForDelta(delta: TreeDelta, current: Tree): string[];
28
+ /**
29
+ * Apply a delta to a parent tree, producing the child tree.
30
+ *
31
+ * The inverse of diffTrees, and the restore path's core step. `hashes` supplies
32
+ * the content hash for every added/modified path — a delta alone does not carry
33
+ * them, because the tree it came from already did.
34
+ */
35
+ export declare function applyDelta(parent: Tree, delta: TreeDelta, hashes: Tree): Tree;
36
+ /**
37
+ * Collapse a lineage into one effective tree.
38
+ *
39
+ * `chain` runs oldest-first: a full snapshot's tree, then each descendant's
40
+ * delta. Restoring walks it forward rather than resolving parents recursively at
41
+ * read time, so a long chain costs one pass.
42
+ */
43
+ export declare function collapseChain(base: Tree, steps: readonly {
44
+ delta: TreeDelta;
45
+ hashes: Tree;
46
+ }[]): Tree;
47
+ /** Paths in a tree whose blobs are absent from `have` — what a restore must
48
+ * fetch before it can write anything. */
49
+ export declare function missingBlobs(tree: Tree, have: ReadonlySet<string>): string[];
50
+ /**
51
+ * Paths a snapshot must NOT capture.
52
+ *
53
+ * Session and run logs are Layer 3 and travel by reference (spec s4), so
54
+ * capturing them here would duplicate them and, worse, let a stale copy overwrite
55
+ * the live append-only log on restore — the exact clobber the single-writer rule
56
+ * exists to prevent. Blobs are the session's own offloaded outputs, referenced
57
+ * the same way.
58
+ */
59
+ export declare function isLayer3Path(path: string, stateDir: string): boolean;
60
+ /** Drop Layer 3 paths from a tree before diffing it. */
61
+ export declare function withoutLayer3(tree: Tree, stateDir: string): Tree;
62
+ //# sourceMappingURL=tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../snapshot/tree.ts"],"names":[],"mappings":"AAiBA,oFAAoF;AACpF,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE1C,MAAM,WAAW,SAAS;IACxB,oDAAoD;IACpD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;+CAC2C;IAC3C,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAKhD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CASnD;AAED,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,GAAG,SAAS,CAahE;AAED;qDACqD;AACrD,wBAAgB,YAAY,CAAC,CAAC,EAAE,SAAS,GAAG,OAAO,CAElD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,MAAM,EAAE,CAOvE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,GAAG,IAAI,CAW7E;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,IAAI,CAAA;CAAE,EAAE,GAAG,IAAI,CAEpG;AAED;yCACyC;AACzC,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAM5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMpE;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAMhE"}
@@ -0,0 +1,147 @@
1
+ /* Snapshot Layer 2 — the content-addressed file tree and the delta between two.
2
+ *
3
+ * A snapshot ships a DIFF, not a disk image. Spec s4: a raw microVM memory dump
4
+ * does not move between an Apple Silicon laptop and a Linux host, but a file diff
5
+ * does, and because the base is pinned by digest both ends provably start from
6
+ * identical ground. Typically megabytes, not gigabytes.
7
+ *
8
+ * This module is pure — no I/O, no crypto, no node. Hashing is the caller's
9
+ * (node:crypto server-side, SubtleCrypto in a browser), which is also what keeps
10
+ * one implementation serving both.
11
+ *
12
+ * It also gives `parent` a job. An earlier review flagged the manifest's lineage
13
+ * field as unused and asked whether to cut it; incremental snapshots are the use.
14
+ * The first snapshot of a session is full; each later one records only what
15
+ * changed since its parent, and a restore walks the chain back to a full one.
16
+ */
17
+ /** Canonical text form of a tree — sorted paths, trailing newline. */
18
+ export function serializeTree(tree) {
19
+ const keys = Object.keys(tree).sort();
20
+ const out = {};
21
+ for (const k of keys)
22
+ out[k] = tree[k];
23
+ return JSON.stringify(out, null, 2) + '\n';
24
+ }
25
+ export function parseTree(text) {
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(text);
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
34
+ return null;
35
+ const out = {};
36
+ for (const [k, v] of Object.entries(parsed)) {
37
+ if (typeof v === 'string' && v)
38
+ out[k] = v;
39
+ }
40
+ return out;
41
+ }
42
+ /** What changed between two trees. Deterministic: every list is sorted. */
43
+ export function diffTrees(parent, current) {
44
+ const added = [];
45
+ const modified = [];
46
+ const removed = [];
47
+ for (const [path, hash] of Object.entries(current)) {
48
+ const before = parent[path];
49
+ if (before === undefined)
50
+ added.push(path);
51
+ else if (before !== hash)
52
+ modified.push(path);
53
+ }
54
+ for (const path of Object.keys(parent)) {
55
+ if (current[path] === undefined)
56
+ removed.push(path);
57
+ }
58
+ return { added: added.sort(), modified: modified.sort(), removed: removed.sort() };
59
+ }
60
+ /** True when nothing changed — worth checking before writing a snapshot nobody
61
+ * needs, and before shipping bytes across a wire. */
62
+ export function isEmptyDelta(d) {
63
+ return d.added.length === 0 && d.modified.length === 0 && d.removed.length === 0;
64
+ }
65
+ /**
66
+ * The blob hashes a delta actually needs shipped.
67
+ *
68
+ * DEDUPED, which is the point of content addressing: two paths with identical
69
+ * content share one blob, so a copied file or a repeated boilerplate header costs
70
+ * nothing extra. Removals need no blob at all.
71
+ */
72
+ export function blobsForDelta(delta, current) {
73
+ const needed = new Set();
74
+ for (const path of [...delta.added, ...delta.modified]) {
75
+ const hash = current[path];
76
+ if (hash)
77
+ needed.add(hash);
78
+ }
79
+ return [...needed].sort();
80
+ }
81
+ /**
82
+ * Apply a delta to a parent tree, producing the child tree.
83
+ *
84
+ * The inverse of diffTrees, and the restore path's core step. `hashes` supplies
85
+ * the content hash for every added/modified path — a delta alone does not carry
86
+ * them, because the tree it came from already did.
87
+ */
88
+ export function applyDelta(parent, delta, hashes) {
89
+ const out = { ...parent };
90
+ for (const path of delta.removed)
91
+ delete out[path];
92
+ for (const path of [...delta.added, ...delta.modified]) {
93
+ const hash = hashes[path];
94
+ // A delta naming a path with no hash is corrupt; skipping it would silently
95
+ // produce a tree that does not match the snapshot's own tree hash, which the
96
+ // caller can then detect. Better a detectable mismatch than invented content.
97
+ if (hash)
98
+ out[path] = hash;
99
+ }
100
+ return out;
101
+ }
102
+ /**
103
+ * Collapse a lineage into one effective tree.
104
+ *
105
+ * `chain` runs oldest-first: a full snapshot's tree, then each descendant's
106
+ * delta. Restoring walks it forward rather than resolving parents recursively at
107
+ * read time, so a long chain costs one pass.
108
+ */
109
+ export function collapseChain(base, steps) {
110
+ return steps.reduce((acc, step) => applyDelta(acc, step.delta, step.hashes), { ...base });
111
+ }
112
+ /** Paths in a tree whose blobs are absent from `have` — what a restore must
113
+ * fetch before it can write anything. */
114
+ export function missingBlobs(tree, have) {
115
+ const missing = new Set();
116
+ for (const hash of Object.values(tree)) {
117
+ if (!have.has(hash))
118
+ missing.add(hash);
119
+ }
120
+ return [...missing].sort();
121
+ }
122
+ /**
123
+ * Paths a snapshot must NOT capture.
124
+ *
125
+ * Session and run logs are Layer 3 and travel by reference (spec s4), so
126
+ * capturing them here would duplicate them and, worse, let a stale copy overwrite
127
+ * the live append-only log on restore — the exact clobber the single-writer rule
128
+ * exists to prevent. Blobs are the session's own offloaded outputs, referenced
129
+ * the same way.
130
+ */
131
+ export function isLayer3Path(path, stateDir) {
132
+ const p = path.replace(/^\.\//, '');
133
+ return p.startsWith(`${stateDir}/sessions/`)
134
+ || p.startsWith(`${stateDir}/runs/`)
135
+ || p === `${stateDir}/blobs`
136
+ || p.startsWith(`${stateDir}/blobs/`);
137
+ }
138
+ /** Drop Layer 3 paths from a tree before diffing it. */
139
+ export function withoutLayer3(tree, stateDir) {
140
+ const out = {};
141
+ for (const [path, hash] of Object.entries(tree)) {
142
+ if (!isLayer3Path(path, stateDir))
143
+ out[path] = hash;
144
+ }
145
+ return out;
146
+ }
147
+ //# sourceMappingURL=tree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.js","sourceRoot":"","sources":["../../snapshot/tree.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,7 @@ export declare const recallResultTool: AgentTool;
8
8
  export declare const askUserTool: AgentTool;
9
9
  export declare const spawnAgentTool: AgentTool;
10
10
  export declare const spawnAgentsTool: AgentTool;
11
+ export declare const listToolsTool: AgentTool;
11
12
  export declare const compilePrompdTool: AgentTool;
12
13
  /** The kernel's runtime-agnostic built-in tools. Product/host tools (registry
13
14
  * search) are added by the registry — use `allTools()` for the complete set. */
@@ -1 +1 @@
1
- {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../../tools/builtins.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,mBAAmB,CAAC;AAahE,eAAO,MAAM,eAAe,EAAE,SA0C7B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,SAoB5B,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,SAa9B,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,SAWvB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,SAkBvB,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,SAgB9B,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,SAiDzB,CAAC;AAiBF,eAAO,MAAM,cAAc,EAAE,SA6B5B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,SA6C7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,SAgC/B,CAAC;AAEF;gFACgF;AAChF,eAAO,MAAM,YAAY,EAAE,SAAS,EAenC,CAAC;AAEF;;;+DAG+D;AAC/D,wBAAgB,QAAQ,IAAI,SAAS,EAAE,CAEtC"}
1
+ {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../../tools/builtins.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,mBAAmB,CAAC;AAwDhE,eAAO,MAAM,eAAe,EAAE,SA0F7B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,SAyB5B,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,SAa9B,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,SAWvB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,SAkBvB,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,SA6B9B,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,SAgEzB,CAAC;AAiBF,eAAO,MAAM,cAAc,EAAE,SA6B5B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,SA6C7B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,SAoB3B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,SAgC/B,CAAC;AAEF;gFACgF;AAChF,eAAO,MAAM,YAAY,EAAE,SAAS,EAgBnC,CAAC;AAEF;;;+DAG+D;AAC/D,wBAAgB,QAAQ,IAAI,SAAS,EAAE,CAEtC"}
@@ -8,9 +8,55 @@ function str(args, key) {
8
8
  const v = args[key];
9
9
  return typeof v === 'string' ? v : v == null ? '' : String(v);
10
10
  }
11
+ /* ---- propose_edit auto-correct ---- */
12
+ /** Max consecutive proposals that failed to compile and were bounced back to the
13
+ * model for a fix, before a stubbornly-broken proposal falls through to human
14
+ * review instead of looping. The streak lives in ctx.runState (per-run), keyed
15
+ * here, so it can't leak or race across runs. */
16
+ const MAX_AUTOFIX = 3;
17
+ const AUTOFIX_KEY = 'proposeEditAutofixStreak';
18
+ /** Read/advance the per-run auto-fix streak held in ctx.runState. */
19
+ function autofixStreak(ctx) {
20
+ const rs = ctx.runState;
21
+ const cur = rs && typeof rs[AUTOFIX_KEY] === 'number' ? rs[AUTOFIX_KEY] : 0;
22
+ return {
23
+ value: cur,
24
+ bump() { if (rs)
25
+ rs[AUTOFIX_KEY] = cur + 1; },
26
+ reset() { if (rs)
27
+ rs[AUTOFIX_KEY] = 0; },
28
+ };
29
+ }
30
+ /** Only .prmd (always) and .md WITH frontmatter are worth compiling to validate —
31
+ * plain markdown has nothing to fail. Needs a known editor path for sibling
32
+ * resolution. */
33
+ function shouldValidatePrmd(path, content) {
34
+ if (!path)
35
+ return false;
36
+ if (path.endsWith('.prmd'))
37
+ return true;
38
+ if (path.endsWith('.md'))
39
+ return content.trimStart().startsWith('---');
40
+ return false;
41
+ }
42
+ /** Compile the proposed content (resolving inherits:/{% include %} against the
43
+ * editor file's siblings) to validate it. Returns a formatted error list, or null
44
+ * when it compiles cleanly. Unfilled {{ params }} render empty (not an error), so a
45
+ * parameterized prompt still validates. */
46
+ async function prmdCompileError(content, ctx) {
47
+ try {
48
+ const r = await compilePrompt(content, {}, { files: ctx.files, path: ctx.editorFile });
49
+ return r.errors?.length ? r.errors.map((e) => `- ${e.message}`).join('\n') : null;
50
+ }
51
+ catch (e) {
52
+ return `- ${e instanceof Error ? e.message : String(e)}`;
53
+ }
54
+ }
11
55
  export const proposeEditTool = {
12
56
  name: 'propose_edit',
13
- description: 'Propose a replacement for the current editor file. The user reviews a diff and applies or rejects it — ' +
57
+ description: 'Propose a replacement for the CURRENTLY OPEN editor file. It takes no path and can only ever replace that '
58
+ + 'one file, so never use it to create a new file or to edit a different one — use create_file or write_file '
59
+ + 'for those. The user reviews a diff and applies or rejects it — ' +
14
60
  'this does NOT write the file directly. Provide the COMPLETE new file content (not a patch) plus a short ' +
15
61
  'explanation. Use the editor file shown in the system context as the basis. This call WAITS for the review ' +
16
62
  'and returns the outcome: applied, partially applied (with the rejected changes), or rejected — rely on ' +
@@ -20,11 +66,40 @@ export const proposeEditTool = {
20
66
  properties: {
21
67
  explanation: { type: 'string', description: 'One line describing the change.' },
22
68
  new_content: { type: 'string', description: 'The full proposed file content.' },
69
+ path: { type: 'string', description: 'The file being replaced. Must be the file currently open in the editor.' },
23
70
  },
24
- required: ['explanation', 'new_content'],
71
+ required: ['explanation', 'new_content', 'path'],
25
72
  },
26
73
  async run(args, ctx) {
27
74
  const explanation = str(args, 'explanation') || 'Proposed an edit';
75
+ // Fail closed with no open file. This tool carries NO target path -- it replaces
76
+ // whatever the editor has open -- so without one there is nothing to diff
77
+ // against. Proposing anyway showed the user an empty diff, and (worse) content
78
+ // meant for a NEW file got diffed against an unrelated open document, where
79
+ // accepting would have overwritten it. Point the model at the tools that take
80
+ // an explicit path instead.
81
+ if (!ctx.editorFile) {
82
+ return 'Cannot propose an edit: no file is open in the editor, and propose_edit only replaces the open file. '
83
+ + 'To create a new file use create_file, and to overwrite an existing one use write_file — both take an explicit path.';
84
+ }
85
+ // The target is explicit and must be the open file. Before `path` existed this
86
+ // tool meant "the open file" implicitly, so a proposal authored for some OTHER
87
+ // file was silently diffed against whatever was open -- and accepting wrote the
88
+ // wrong content into it. Stating the target makes that mismatch impossible
89
+ // rather than merely unlikely.
90
+ const target = str(args, 'path');
91
+ if (!target) {
92
+ return `Cannot propose an edit: propose_edit needs a path, and it must be the open file (${ctx.editorFile}). `
93
+ + 'To create a new file use create_file, or to overwrite a different one use write_file.';
94
+ }
95
+ // Compare on a normalized form so a Windows path or a leading ./ is not a false
96
+ // mismatch — but nothing looser than that: matching on the basename alone would
97
+ // accept a/welcome.prmd for b/welcome.prmd, which is the very confusion this guards.
98
+ const norm = (p) => p.trim().replace(/\\/g, '/').replace(/^\.\//, '');
99
+ if (norm(target) !== norm(ctx.editorFile)) {
100
+ return `Cannot propose an edit: the open file is ${ctx.editorFile}, but this proposal targets ${target}. `
101
+ + 'propose_edit can only replace the open file. Use create_file for a new file, or write_file to overwrite a different one.';
102
+ }
28
103
  // The diff + apply happen on the main thread (the panel renders the proposal
29
104
  // from this tool call). Block until the user decides so later turns can rely
30
105
  // on the outcome. Without a reviewer (sub-agents, DAG nodes, headless runs)
@@ -32,6 +107,21 @@ export const proposeEditTool = {
32
107
  if (!ctx.reviewEdit || !ctx.callId) {
33
108
  return `Proposed an edit: ${explanation}. Awaiting the user's review (apply/reject).`;
34
109
  }
110
+ // Auto-correct: validate the proposal BEFORE surfacing the diff. If it doesn't
111
+ // compile, bounce the errors back to the model to fix instead of showing the
112
+ // user a broken edit. Bounded — after MAX_AUTOFIX consecutive failures, fall
113
+ // through to human review so a stubbornly-broken proposal isn't hidden forever.
114
+ const newContent = str(args, 'new_content');
115
+ if (shouldValidatePrmd(ctx.editorFile, newContent)) {
116
+ const streak = autofixStreak(ctx);
117
+ const err = await prmdCompileError(newContent, ctx);
118
+ if (err && streak.value < MAX_AUTOFIX) {
119
+ streak.bump();
120
+ return ('This proposal does NOT compile, so it was not shown to the user. ' +
121
+ `Fix these errors and call propose_edit again with corrected content:\n${err}`);
122
+ }
123
+ streak.reset(); // surfacing now (clean, or giving up after the budget)
124
+ }
35
125
  const outcome = ctx.signal.aborted ? null : await Promise.race([
36
126
  ctx.reviewEdit(ctx.callId),
37
127
  new Promise((resolve) => ctx.signal.addEventListener('abort', () => resolve(null), { once: true })),
@@ -52,21 +142,25 @@ export const proposeEditTool = {
52
142
  export const saveMemoryTool = {
53
143
  name: 'save_memory',
54
144
  mutates: true,
55
- description: 'Persist a durable note to memory for later recall (facts, user preferences, decisions).',
145
+ description: 'Persist a durable note to memory for later recall (facts, user preferences, decisions). ' +
146
+ 'Choose scope: "workspace" for facts specific to the current project, "global" for facts ' +
147
+ 'about the user or their preferences that apply everywhere.',
56
148
  parameters: {
57
149
  type: 'object',
58
150
  properties: {
59
151
  text: { type: 'string', description: 'The note to remember.' },
152
+ scope: { type: 'string', enum: ['workspace', 'global'], description: 'Where to store it: workspace (this project) or global (everywhere).' },
60
153
  tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags.' },
61
154
  },
62
- required: ['text'],
155
+ required: ['text', 'scope'],
63
156
  },
64
157
  // Permission is gated centrally by the loop's policy/permission layer (this
65
158
  // tool is `mutates`), so no confirm call here.
66
159
  async run(args, ctx) {
67
160
  const text = str(args, 'text');
68
161
  const tags = Array.isArray(args.tags) ? args.tags.map(String) : [];
69
- const item = await ctx.memory.save(text, tags);
162
+ const scope = args.scope === 'global' ? 'global' : 'workspace';
163
+ const item = await ctx.memory.save(text, tags, scope);
70
164
  return `Saved memory ${item.id}.`;
71
165
  },
72
166
  };
@@ -117,6 +211,19 @@ export const kvSetTool = {
117
211
  export const recallResultTool = {
118
212
  name: 'recall_result',
119
213
  noOffload: true, // recalling a large result must never re-offload itself
214
+ /* DECLARED, like todo_write's and ask_user's. This one grants no new access at
215
+ * all: it reads back a tool result THIS SESSION ALREADY PRODUCED, which means
216
+ * the original call already passed the policy check that produced it. The
217
+ * store is session-scoped (`<root>/<sessionId>/blobs/<id>.json`) and the id
218
+ * shape is validated, so it cannot reach another session's data or escape the
219
+ * directory.
220
+ *
221
+ * The deciding argument is that offloading is a CONTEXT-MANAGEMENT detail,
222
+ * triggered by size. Gating recall means the same information costs an
223
+ * approval prompt depending on how large it happened to be - a performance
224
+ * heuristic leaking into governance, and an approval the user cannot act on
225
+ * meaningfully because they already approved the thing that produced it. */
226
+ permission: 'allow',
120
227
  description: 'Fetch the full content of a tool result that was offloaded to save context. ' +
121
228
  'You saw an "[offloaded tool result … recall_result({ id })]" reference — pass that id.',
122
229
  parameters: {
@@ -133,6 +240,21 @@ export const recallResultTool = {
133
240
  export const askUserTool = {
134
241
  name: 'ask_user',
135
242
  noOffload: true,
243
+ /* DECLARED, like todo_write's. Gating this behind an approval prompt is asking
244
+ * the user for permission to ask the user a question — two dialogs for one
245
+ * decision, the first of which carries no information.
246
+ *
247
+ * It changes nothing outside the run: no file, no network, no state. Its whole
248
+ * effect is to put a question in front of a person who is already there.
249
+ *
250
+ * Nothing depended on its previous `ask` tier. The case that looks like it
251
+ * might — an unattended run parking forever on a question nobody will answer —
252
+ * is handled by EXCLUSION rather than by permission: CREW_EXCLUDED_TOOLS drops
253
+ * ask_user from every crew agent (see @corenel/crew's capabilities.ts), and the
254
+ * daemon's executor honours the same set. On a surface where it IS offered,
255
+ * someone is by definition attending. And with no `ask` handler wired, `run`
256
+ * already returns "unavailable in this environment" rather than hanging. */
257
+ permission: 'allow',
136
258
  description: 'Ask the user one or more questions when you need a decision you cannot make from context — to choose ' +
137
259
  'between approaches, get a missing requirement, or confirm direction. Prefer this over guessing. Give each ' +
138
260
  'question a short `header` (a chip label), the `question` text, and 2–4 distinct `options` (each with a ' +
@@ -274,6 +396,28 @@ export const spawnAgentsTool = {
274
396
  return results.join('\n\n');
275
397
  },
276
398
  };
399
+ export const listToolsTool = {
400
+ name: 'list_tools',
401
+ noOffload: true,
402
+ description: 'List the tools available to you this run — their names, one-line descriptions, and top-level parameters. ' +
403
+ 'Use this to discover what you can call (including any MCP or connected-host tools) before deciding how to act.',
404
+ parameters: { type: 'object', properties: {} },
405
+ async run(_args, ctx) {
406
+ if (!ctx.listTools)
407
+ return 'Tool discovery is not available here.';
408
+ const tools = ctx.listTools();
409
+ if (!tools.length)
410
+ return 'No tools are available this run.';
411
+ const lines = tools.map((t) => {
412
+ const props = t.parameters && typeof t.parameters === 'object'
413
+ ? Object.keys(t.parameters.properties ?? {})
414
+ : [];
415
+ const params = props.length ? ` — params: ${props.join(', ')}` : '';
416
+ return `- \`${t.name}\`: ${t.description}${params}`;
417
+ });
418
+ return `Available tools this run (${tools.length}):\n${lines.join('\n')}`;
419
+ },
420
+ };
277
421
  export const compilePrompdTool = {
278
422
  name: 'compile_prompd',
279
423
  noOffload: true, // the compiled output is what the user asked to see — keep inline
@@ -313,6 +457,7 @@ export const defaultTools = [
313
457
  askUserTool,
314
458
  spawnAgentTool,
315
459
  spawnAgentsTool,
460
+ listToolsTool,
316
461
  proposeEditTool,
317
462
  saveMemoryTool,
318
463
  recallMemoryTool,