cohorte 2.10.1 → 3.0.0-dev.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.
Files changed (213) hide show
  1. package/README.md +7 -0
  2. package/assets/manifest.json +193 -0
  3. package/assets/migrations/config/README.md +12 -0
  4. package/assets/migrations/state/0001_init.sql +154 -0
  5. package/assets/prompts/README.md +11 -0
  6. package/assets/prompts/agents/fixer.md +7 -0
  7. package/assets/prompts/agents/implementer.md +12 -0
  8. package/assets/prompts/agents/reviewer.md +8 -0
  9. package/assets/prompts/agents/security-reviewer.md +7 -0
  10. package/assets/prompts/phases/build.md +5 -0
  11. package/assets/prompts/phases/review.md +5 -0
  12. package/assets/prompts/phases/test.md +5 -0
  13. package/assets/prompts/system.md +11 -0
  14. package/assets/schemas/agent-output.schema.json +225 -0
  15. package/assets/schemas/auth-status.schema.json +87 -0
  16. package/assets/schemas/command-result.schema.json +192 -0
  17. package/assets/schemas/commands.schema.json +952 -0
  18. package/assets/schemas/config.schema.json +2259 -0
  19. package/assets/schemas/doctor-report.schema.json +104 -0
  20. package/assets/schemas/events.schema.json +8514 -0
  21. package/assets/schemas/fake-script.schema.json +549 -0
  22. package/assets/schemas/inspect.schema.json +3133 -0
  23. package/assets/schemas/manifest.schema.json +79 -0
  24. package/assets/schemas/ownership.schema.json +52 -0
  25. package/assets/schemas/policy-verdict.schema.json +282 -0
  26. package/assets/schemas/project-model.schema.json +783 -0
  27. package/assets/schemas/project-status.schema.json +217 -0
  28. package/assets/schemas/reconcile-plan.schema.json +154 -0
  29. package/assets/schemas/run-diff.schema.json +177 -0
  30. package/assets/schemas/run-snapshot-manifest.schema.json +2104 -0
  31. package/assets/schemas/run-state.schema.json +1755 -0
  32. package/assets/schemas/runtime-capabilities.schema.json +1545 -0
  33. package/assets/schemas/sandbox-capabilities.schema.json +116 -0
  34. package/assets/schemas/skill.schema.json +87 -0
  35. package/assets/schemas/spec.schema.json +202 -0
  36. package/assets/schemas/tool-catalogue.schema.json +655 -0
  37. package/assets/schemas/trust-record.schema.json +38 -0
  38. package/assets/skills/README.md +9 -0
  39. package/assets/skills/inspect-surface.md +4 -0
  40. package/dist/agent-host.mjs +2187 -0
  41. package/dist/agent-host.mjs.map +1 -0
  42. package/dist/bundle-manifest.json +104 -0
  43. package/dist/chunks/__host-DgGPyco9.mjs +18 -0
  44. package/dist/chunks/__host-DgGPyco9.mjs.map +1 -0
  45. package/dist/chunks/approve-Cv135UqI.mjs +19 -0
  46. package/dist/chunks/approve-Cv135UqI.mjs.map +1 -0
  47. package/dist/chunks/auth-BT5oVp76.mjs +132 -0
  48. package/dist/chunks/auth-BT5oVp76.mjs.map +1 -0
  49. package/dist/chunks/auth-CYOh60KG.mjs +93 -0
  50. package/dist/chunks/auth-CYOh60KG.mjs.map +1 -0
  51. package/dist/chunks/brainstorm-BwKA6yZP.mjs +7 -0
  52. package/dist/chunks/brainstorm-BwKA6yZP.mjs.map +1 -0
  53. package/dist/chunks/cancel-KV_cpsqS.mjs +15 -0
  54. package/dist/chunks/cancel-KV_cpsqS.mjs.map +1 -0
  55. package/dist/chunks/canonical-C8UxeEmB.mjs +57 -0
  56. package/dist/chunks/canonical-C8UxeEmB.mjs.map +1 -0
  57. package/dist/chunks/catalogue-BvwSrM_j.mjs +1549 -0
  58. package/dist/chunks/catalogue-BvwSrM_j.mjs.map +1 -0
  59. package/dist/chunks/compose-Dvxunpto.mjs +7307 -0
  60. package/dist/chunks/compose-Dvxunpto.mjs.map +1 -0
  61. package/dist/chunks/config-DefHA2Lw.mjs +96 -0
  62. package/dist/chunks/config-DefHA2Lw.mjs.map +1 -0
  63. package/dist/chunks/contract-BkMjU1kR.mjs +295 -0
  64. package/dist/chunks/contract-BkMjU1kR.mjs.map +1 -0
  65. package/dist/chunks/contract-bQ9FJl4N.mjs +123 -0
  66. package/dist/chunks/contract-bQ9FJl4N.mjs.map +1 -0
  67. package/dist/chunks/deny-ClTxSvHJ.mjs +15 -0
  68. package/dist/chunks/deny-ClTxSvHJ.mjs.map +1 -0
  69. package/dist/chunks/diff-Qw0II0sj.mjs +101 -0
  70. package/dist/chunks/diff-Qw0II0sj.mjs.map +1 -0
  71. package/dist/chunks/discover-eS01-me1.mjs +21 -0
  72. package/dist/chunks/discover-eS01-me1.mjs.map +1 -0
  73. package/dist/chunks/doctor-B-H2UK5e.mjs +222 -0
  74. package/dist/chunks/doctor-B-H2UK5e.mjs.map +1 -0
  75. package/dist/chunks/fix-D4Tj8RVA.mjs +18 -0
  76. package/dist/chunks/fix-D4Tj8RVA.mjs.map +1 -0
  77. package/dist/chunks/gc-BBXlBF5w.mjs +161 -0
  78. package/dist/chunks/gc-BBXlBF5w.mjs.map +1 -0
  79. package/dist/chunks/init-BUnzIkeE.mjs +63 -0
  80. package/dist/chunks/init-BUnzIkeE.mjs.map +1 -0
  81. package/dist/chunks/inspect-Buby4cQD.mjs +22 -0
  82. package/dist/chunks/inspect-Buby4cQD.mjs.map +1 -0
  83. package/dist/chunks/logs-Giq5sUau.mjs +25 -0
  84. package/dist/chunks/logs-Giq5sUau.mjs.map +1 -0
  85. package/dist/chunks/migrate-DybihKzZ.mjs +33 -0
  86. package/dist/chunks/migrate-DybihKzZ.mjs.map +1 -0
  87. package/dist/chunks/models-Btp6X2YZ.mjs +24 -0
  88. package/dist/chunks/models-Btp6X2YZ.mjs.map +1 -0
  89. package/dist/chunks/pause-BASBfKLO.mjs +16 -0
  90. package/dist/chunks/pause-BASBfKLO.mjs.map +1 -0
  91. package/dist/chunks/policy-DUIgzF3W.mjs +139 -0
  92. package/dist/chunks/policy-DUIgzF3W.mjs.map +1 -0
  93. package/dist/chunks/providers-Dx78imKj.mjs +19 -0
  94. package/dist/chunks/providers-Dx78imKj.mjs.map +1 -0
  95. package/dist/chunks/reconcile-Bhy3IIYo.mjs +50 -0
  96. package/dist/chunks/reconcile-Bhy3IIYo.mjs.map +1 -0
  97. package/dist/chunks/render-DyZr5Lgi.mjs +53 -0
  98. package/dist/chunks/render-DyZr5Lgi.mjs.map +1 -0
  99. package/dist/chunks/resume-GJQRb2Eb.mjs +15 -0
  100. package/dist/chunks/resume-GJQRb2Eb.mjs.map +1 -0
  101. package/dist/chunks/retry-CLrUem1H.mjs +15 -0
  102. package/dist/chunks/retry-CLrUem1H.mjs.map +1 -0
  103. package/dist/chunks/review-BDdjb6kB.mjs +17 -0
  104. package/dist/chunks/review-BDdjb6kB.mjs.map +1 -0
  105. package/dist/chunks/run-CgTWYlcd.mjs +82 -0
  106. package/dist/chunks/run-CgTWYlcd.mjs.map +1 -0
  107. package/dist/chunks/run-tool-C3rAv4XF.mjs +26 -0
  108. package/dist/chunks/run-tool-C3rAv4XF.mjs.map +1 -0
  109. package/dist/chunks/scan-8Gy_B6kI.mjs +122 -0
  110. package/dist/chunks/scan-8Gy_B6kI.mjs.map +1 -0
  111. package/dist/chunks/schema-C9QbUW4H.mjs +578 -0
  112. package/dist/chunks/schema-C9QbUW4H.mjs.map +1 -0
  113. package/dist/chunks/send-BdnJ85Nz.mjs +19 -0
  114. package/dist/chunks/send-BdnJ85Nz.mjs.map +1 -0
  115. package/dist/chunks/ship-DTuLGoaJ.mjs +19 -0
  116. package/dist/chunks/ship-DTuLGoaJ.mjs.map +1 -0
  117. package/dist/chunks/shutdown-BM92Iyv2.mjs +15 -0
  118. package/dist/chunks/shutdown-BM92Iyv2.mjs.map +1 -0
  119. package/dist/chunks/skip-DZG5kpcH.mjs +18 -0
  120. package/dist/chunks/skip-DZG5kpcH.mjs.map +1 -0
  121. package/dist/chunks/spec-Bq0P3jVh.mjs +20 -0
  122. package/dist/chunks/spec-Bq0P3jVh.mjs.map +1 -0
  123. package/dist/chunks/src-CId6kMsY.mjs +1689 -0
  124. package/dist/chunks/src-CId6kMsY.mjs.map +1 -0
  125. package/dist/chunks/src-CfAOde7C.mjs +5403 -0
  126. package/dist/chunks/src-CfAOde7C.mjs.map +1 -0
  127. package/dist/chunks/src-CmFjh4M0.mjs +928 -0
  128. package/dist/chunks/src-CmFjh4M0.mjs.map +1 -0
  129. package/dist/chunks/src-Cu3TxNob.mjs +783 -0
  130. package/dist/chunks/src-Cu3TxNob.mjs.map +1 -0
  131. package/dist/chunks/src-DcU4IXMe.mjs +117 -0
  132. package/dist/chunks/src-DcU4IXMe.mjs.map +1 -0
  133. package/dist/chunks/src-s7YRvk0t.mjs +2116 -0
  134. package/dist/chunks/src-s7YRvk0t.mjs.map +1 -0
  135. package/dist/chunks/status-SMjiOMYy.mjs +29 -0
  136. package/dist/chunks/status-SMjiOMYy.mjs.map +1 -0
  137. package/dist/chunks/tail-BuhT-2PH.mjs +25 -0
  138. package/dist/chunks/tail-BuhT-2PH.mjs.map +1 -0
  139. package/dist/chunks/update-BXIppsbi.mjs +16 -0
  140. package/dist/chunks/update-BXIppsbi.mjs.map +1 -0
  141. package/dist/cli.mjs +175 -0
  142. package/dist/cli.mjs.map +1 -0
  143. package/package.json +17 -32
  144. package/CHANGELOG.md +0 -1461
  145. package/bin/cli.js +0 -817
  146. package/bin/report.js +0 -141
  147. package/core/adapter/render.js +0 -415
  148. package/core/agents/implementer.template.md +0 -142
  149. package/core/agents/profile-reader.md +0 -50
  150. package/core/agents/release.md +0 -58
  151. package/core/agents/review.md +0 -179
  152. package/core/commands/cohorte-align-ds.md +0 -33
  153. package/core/commands/cohorte-audit.md +0 -58
  154. package/core/commands/cohorte-brainstorm.md +0 -70
  155. package/core/commands/cohorte-build.md +0 -219
  156. package/core/commands/cohorte-doctor.md +0 -194
  157. package/core/commands/cohorte-fix.md +0 -80
  158. package/core/commands/cohorte-fleet.md +0 -103
  159. package/core/commands/cohorte-init-pipeline.md +0 -31
  160. package/core/commands/cohorte-intake.md +0 -92
  161. package/core/commands/cohorte-patch.md +0 -118
  162. package/core/commands/cohorte-refactor.md +0 -59
  163. package/core/commands/cohorte-retro.md +0 -85
  164. package/core/commands/cohorte-review.md +0 -270
  165. package/core/commands/cohorte-ship.md +0 -122
  166. package/core/commands/cohorte-spec.md +0 -101
  167. package/core/commands/cohorte-update-pipeline.md +0 -165
  168. package/core/hooks/gate.py +0 -588
  169. package/core/runtimes/claude.json +0 -74
  170. package/core/runtimes/codex.json +0 -85
  171. package/core/runtimes/cursor.json +0 -76
  172. package/core/runtimes/gemini.json +0 -76
  173. package/core/runtimes/opencode.json +0 -73
  174. package/core/templates/agent-handoff.md +0 -34
  175. package/core/templates/brainstorm-return.md +0 -36
  176. package/core/templates/decisions.template.md +0 -42
  177. package/core/templates/design-brief.md +0 -44
  178. package/core/templates/patch.template.md +0 -86
  179. package/core/templates/pr-body.md +0 -25
  180. package/core/templates/review-feedback.md +0 -38
  181. package/core/templates/spec.template.md +0 -87
  182. package/core/templates/steps/init-pipeline/01-detect-stack.md +0 -44
  183. package/core/templates/steps/init-pipeline/02-interview-gaps.md +0 -55
  184. package/core/templates/steps/init-pipeline/03-draft-profile.md +0 -10
  185. package/core/templates/steps/init-pipeline/04-write-render.md +0 -145
  186. package/core/templates/steps/init-pipeline/05-report.md +0 -12
  187. package/core/workflows/audit.js +0 -240
  188. package/core/workflows/loop.js +0 -641
  189. package/core/workflows/refactor.js +0 -276
  190. package/core/workflows/review.js +0 -456
  191. package/install.ps1 +0 -103
  192. package/install.sh +0 -114
  193. package/lib/doctor.js +0 -534
  194. package/lib/runtime.js +0 -139
  195. package/lib/versions.js +0 -149
  196. package/lib/yaml.js +0 -129
  197. package/profile/PIPELINE.template.md +0 -259
  198. package/profile/SCHEMA.md +0 -735
  199. package/profile/cohorte.config.template.yaml +0 -43
  200. package/scripts/assert-gate-hook.mjs +0 -75
  201. package/scripts/kanban-move.sh +0 -313
  202. package/scripts/metrics/collect.mjs +0 -531
  203. package/scripts/metrics/prices.json +0 -42
  204. package/scripts/new-feature.sh.template +0 -98
  205. package/scripts/preflight.sh +0 -124
  206. package/scripts/remove-feature.sh.template +0 -56
  207. package/scripts/test-adapter.mjs +0 -437
  208. package/scripts/test-gate.mjs +0 -443
  209. package/scripts/test-kanban.mjs +0 -304
  210. package/scripts/test-lib.mjs +0 -356
  211. package/scripts/test-metrics.mjs +0 -156
  212. package/scripts/test-workflows.mjs +0 -758
  213. package/scripts/validate-core.mjs +0 -325
@@ -0,0 +1,1689 @@
1
+ import { n as sha256Hex } from "./canonical-C8UxeEmB.mjs";
2
+ import { B as ErrorInfo, O as ToolCallId, S as IsoInstant, T as Sha256, V as JsonValueSchema, d as ModelRef, h as ApprovalId, l as AuthMode, m as AgentId, n as QuotaInfo, p as ThinkingLevel, r as TokenUsage, w as RunId } from "./src-Cu3TxNob.mjs";
3
+ import { c as CommandRule } from "./schema-C9QbUW4H.mjs";
4
+ import { Type } from "typebox";
5
+ import { readFile } from "node:fs/promises";
6
+ import { delimiter, join } from "node:path";
7
+ import { execFile, spawn } from "node:child_process";
8
+ import { promisify } from "node:util";
9
+ import { accessSync, constants, realpathSync } from "node:fs";
10
+ import { StringDecoder } from "node:string_decoder";
11
+ //#region ../../packages/security/src/exec/capabilities.ts
12
+ /**
13
+ * The fixed L0-process report (every OS, the built-in `none` backend). `memory` stays `unavailable`: the ulimit
14
+ * wrapper never attempts `-v` (toolchain.md §8: it fails outright on macOS, and this unit applies only `-t/-f/-n/-u`
15
+ * on every platform alike, per DESIGN 2.6.6's own list of flags). `missing`/`notes` report what `probeSandbox`
16
+ * observed about L1 binaries on this machine; the level stays `L0-process` regardless, since no backend besides
17
+ * `none` is implemented here.
18
+ *
19
+ * `cpuTime` and `processes` say `enforced` because the wrapper FAILS CLOSED (ulimit.ts): a limit the kernel
20
+ * refuses aborts the run instead of letting the program start without it, so the word is never a claim about a
21
+ * limit that was silently dropped.
22
+ */
23
+ function l0Capabilities(missing = [], notes = []) {
24
+ return {
25
+ level: "L0-process",
26
+ backend: "none",
27
+ filesystem: "advisory",
28
+ network: "unenforced",
29
+ processEscape: "possible",
30
+ envFiltering: "enforced",
31
+ timeout: "enforced",
32
+ outputCap: "enforced",
33
+ cpuTime: "enforced",
34
+ memory: "unavailable",
35
+ processes: "enforced",
36
+ killTree: "process-group-with-sweep",
37
+ missing: [...missing],
38
+ notes: [...notes]
39
+ };
40
+ }
41
+ function isExecutable(path) {
42
+ try {
43
+ accessSync(path, constants.X_OK);
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+ /**
50
+ * Is `name` an executable on the host's PATH? Scanned directly rather than through `which`, which is not part of
51
+ * coreutils and is simply absent from many minimal Linux images and containers: there, a spawned `which` fails,
52
+ * `bwrap` is reported missing although it is installed, and `doctor` — the report ADR-0003 makes load-bearing for
53
+ * the `native` / `best-effort` decision — states a false negative. A `stat` per PATH entry also costs no fork.
54
+ *
55
+ * This is the one place the executor reads `process.env`, and it reads the HOST's PATH to answer a question about
56
+ * the host. Nothing here ever reaches a child: the environment of a spawned command is built from `ExecRequest.env`
57
+ * alone (S-20, executor.ts).
58
+ */
59
+ function isOnPath(name) {
60
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) if (dir !== "" && isExecutable(join(dir, name))) return true;
61
+ return false;
62
+ }
63
+ const settledDetection = /* @__PURE__ */ new Map();
64
+ /**
65
+ * Informational only (DESIGN 2.6.6 `missing` example: `["bwrap"]`): this unit never wraps a command in Seatbelt or
66
+ * bubblewrap (that is U4.07's job), so detecting them changes nothing about the reported `level`/`backend` — it
67
+ * only tells `doctor` what is absent.
68
+ *
69
+ * SYNCHRONOUS, because `Executor.capabilities()` is: DESIGN 2.6.6 marks `SandboxCapabilities` `[S]` and
70
+ * `capabilities()` returns it without awaiting anything. Priming an asynchronous detection at construction was not
71
+ * enough — a `capabilities()` called in the same tick still read an unsettled memo and answered `missing: []` while
72
+ * `probeSandbox()` answered `['bwrap']`, i.e. `doctor` contradicted itself depending on when it was called. A
73
+ * handful of `stat`s is all the answer needs.
74
+ *
75
+ * MEMOISED per platform: DESIGN 2.6.6 says `probe()` is "cached per Cohorte version + OS build", and the answer is
76
+ * a property of the machine, not of the command; without the memo it would be recomputed on every `run()`.
77
+ */
78
+ function detectMissingL1Binaries(platform) {
79
+ const settled = settledDetection.get(platform);
80
+ if (settled !== void 0) return settled;
81
+ const missing = [];
82
+ if (platform === "darwin" && !isExecutable("/usr/bin/sandbox-exec")) missing.push("sandbox-exec");
83
+ if (platform === "linux" && !isOnPath("bwrap")) missing.push("bwrap");
84
+ settledDetection.set(platform, missing);
85
+ return missing;
86
+ }
87
+ /**
88
+ * Tries every non-`none` backend in order and returns the FIRST one whose probe reports `L1-os`, together with its
89
+ * report; falls back to the honest L0 report (with L1-binary detection folded into `missing`) when none is usable —
90
+ * which, until U4.07 lands, is always, since no L1 `SandboxBackend` is constructed by this unit.
91
+ *
92
+ * Returning the backend and its report together is what keeps `ExecResult.guarantees` honest: the executor wraps
93
+ * the command with exactly the backend that earned the report, so it can never claim an isolation that the argv it
94
+ * spawned does not carry.
95
+ */
96
+ async function resolveSandbox(backends, platform) {
97
+ for (const backend of backends) {
98
+ if (backend.id === "none") continue;
99
+ try {
100
+ const probed = await backend.probe();
101
+ if (probed.level === "L1-os") return {
102
+ backend,
103
+ capabilities: probed
104
+ };
105
+ } catch {}
106
+ }
107
+ return { capabilities: l0Capabilities(detectMissingL1Binaries(platform)) };
108
+ }
109
+ /** The escape self-test that gates the word `enforced` on this platform (DESIGN 2.6.6, ADR-0003 §2b). */
110
+ function escapeSelfTest(platform) {
111
+ if (platform === "darwin") return "escape self-test S-28";
112
+ if (platform === "linux") return "escape self-test S-29";
113
+ return "the platform escape self-test";
114
+ }
115
+ /**
116
+ * One sentence per axis on which `guarantees` falls short of `require: 'native'`; empty exactly when it satisfies
117
+ * it. DESIGN 2.6.6 requires the refusal to NAME the failing self-test rather than hand the caller a bare enum, so
118
+ * these strings are what the executor appends to `guarantees.notes` on a `sandbox-denied` result and what
119
+ * `sandboxUnavailable()` (exec/index.ts) turns into the catalogued error's message.
120
+ *
121
+ * `level: 'L1-os'` alone is never enough: a backend whose escape self-test has not passed here reports `partial` on
122
+ * the axes that matter, and `native` is satisfied only by `enforced` (ADR-0003 §2b).
123
+ */
124
+ function nativeShortfalls(guarantees, platform = process.platform) {
125
+ if (guarantees.level !== "L1-os") return [`sandbox: no OS sandbox backend is active${guarantees.missing.length > 0 ? ` (missing: ${guarantees.missing.join(", ")})` : ""} — level is '${guarantees.level}', backend '${guarantees.backend}', so filesystem isolation is '${guarantees.filesystem}' and network '${guarantees.network}'`];
126
+ const test = escapeSelfTest(platform);
127
+ const shortfalls = [];
128
+ if (guarantees.filesystem !== "enforced") shortfalls.push(`sandbox: filesystem is '${guarantees.filesystem}' (${test} has not passed on this machine)`);
129
+ if (guarantees.network !== "enforced-off") shortfalls.push(`sandbox: network is '${guarantees.network}' (${test} has not passed on this machine)`);
130
+ if (guarantees.processEscape !== "denied") shortfalls.push(`sandbox: processEscape is '${guarantees.processEscape}' (${test} has not passed on this machine)`);
131
+ return shortfalls;
132
+ }
133
+ /** `resolveSandbox`'s report alone, for callers that only want to know what is guaranteed (`probeSandbox`). */
134
+ async function computeCapabilities(backends, platform) {
135
+ return (await resolveSandbox(backends, platform)).capabilities;
136
+ }
137
+ //#endregion
138
+ //#region ../../packages/security/src/exec/identity.ts
139
+ const execFileAsync = promisify(execFile);
140
+ /**
141
+ * One `ps` fork, four columns. `lstart=` is the last one on purpose: it is the only field with embedded spaces
142
+ * (`Tue Sep 15 09:09:30 2026`), so everything after the third column is its value. A row that cannot produce all
143
+ * four is DROPPED rather than tracked without an identity — a pid with no start time is a bare pid, and this file
144
+ * does not kill those.
145
+ */
146
+ async function processTable() {
147
+ const { stdout } = await execFileAsync("ps", ["-Ao", "pid=,ppid=,pgid=,lstart="]);
148
+ const rows = [];
149
+ for (const line of stdout.split("\n")) {
150
+ const fields = line.trim().split(/\s+/);
151
+ if (fields.length < 4) continue;
152
+ const [pidText, ppidText, pgidText] = fields;
153
+ const pid = Number(pidText);
154
+ const ppid = Number(ppidText);
155
+ const pgid = Number(pgidText);
156
+ const start = fields.slice(3).join(" ");
157
+ if (Number.isInteger(pid) && Number.isInteger(ppid) && Number.isInteger(pgid) && start !== "") rows.push({
158
+ pid,
159
+ ppid,
160
+ pgid,
161
+ start
162
+ });
163
+ }
164
+ return rows;
165
+ }
166
+ /** Every row transitively parented by `rootPid`, whatever its CURRENT pgid (so a `setsid()` escapee still shows). */
167
+ function descendantsOf(rootPid, table) {
168
+ const byParent = /* @__PURE__ */ new Map();
169
+ for (const row of table) {
170
+ const siblings = byParent.get(row.ppid);
171
+ if (siblings) siblings.push(row);
172
+ else byParent.set(row.ppid, [row]);
173
+ }
174
+ const found = [];
175
+ const queue = [...byParent.get(rootPid) ?? []];
176
+ for (let next = queue.pop(); next !== void 0; next = queue.pop()) {
177
+ found.push(next);
178
+ queue.push(...byParent.get(next.pid) ?? []);
179
+ }
180
+ return found;
181
+ }
182
+ /**
183
+ * Is `pgid` a real process group we may address? On POSIX `kill(-0, …)` signals the CALLER's own process group —
184
+ * the run host itself — and `kill(-1, …)` every process the user may signal. `ExecResult.pgid` is `0` on every
185
+ * early return (nothing was spawned), so a dependant that feeds a result back into a sweep must find a closed door
186
+ * here rather than one missing guard between it and a TERM of the run host.
187
+ */
188
+ function isAddressableGroup(pgid) {
189
+ return Number.isInteger(pgid) && pgid > 1;
190
+ }
191
+ function isAlive(pid) {
192
+ try {
193
+ process.kill(pid, 0);
194
+ return true;
195
+ } catch {
196
+ return false;
197
+ }
198
+ }
199
+ /** `true` when the signal was actually delivered; `false` when the target was gone or refused us. */
200
+ function trySignal(pid, signal) {
201
+ try {
202
+ process.kill(pid, signal);
203
+ return true;
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+ /** TERM the pids that are still alive, wait `graceMs`, KILL the survivors. Returns how many were signalled. */
209
+ async function terminatePids(pids, wait, graceMs) {
210
+ const alive = pids.filter((pid) => isAlive(pid));
211
+ if (alive.length === 0) return 0;
212
+ for (const pid of alive) trySignal(pid, "SIGTERM");
213
+ await wait(graceMs);
214
+ for (const pid of alive) if (isAlive(pid)) trySignal(pid, "SIGKILL");
215
+ return alive.length;
216
+ }
217
+ /**
218
+ * Polls the process table until `until` settles, folding every descendant ever seen under `rootPid` into `sink`
219
+ * AS `pid -> start time`. The start time is what makes the record an identity instead of a number: by the end of a
220
+ * long command most of these pids are gone, and the sweep must be able to tell the process it tracked from whatever
221
+ * the OS has since given that number to. It is kept as FIRST SEEN and never overwritten — a differing later reading
222
+ * means the number was recycled, which is exactly what the sweep needs to notice.
223
+ *
224
+ * Run concurrently with the leader's own lifetime (DESIGN 2.6.6 "post-run SWEEP ... also callable on demand by the
225
+ * Resumer"); its own errors (no `ps` on the machine, a transient failure) are swallowed — sweeping is advisory,
226
+ * never a reason to fail the call it is watching.
227
+ */
228
+ async function trackDescendants(rootPid, sink, until, wait, interval) {
229
+ let settled = false;
230
+ const stop = () => {
231
+ settled = true;
232
+ };
233
+ until.then(stop, stop);
234
+ const poll = async () => {
235
+ try {
236
+ for (const row of descendantsOf(rootPid, await processTable())) if (!sink.has(row.pid)) sink.set(row.pid, row.start);
237
+ } catch {}
238
+ };
239
+ let period = interval.initialMs;
240
+ let polledMs = 0;
241
+ for (;;) {
242
+ await poll();
243
+ if (settled) return;
244
+ try {
245
+ await wait(period);
246
+ } catch {
247
+ return;
248
+ }
249
+ polledMs += period;
250
+ if (polledMs >= interval.rampAfterMs) period = Math.min(interval.maxMs, period * 2);
251
+ if (settled) {
252
+ await poll();
253
+ return;
254
+ }
255
+ }
256
+ }
257
+ /**
258
+ * TERM every tracked descendant that is STILL THAT DESCENDANT, wait `graceMs`, then KILL the survivors (`leaderPid`
259
+ * itself is excluded: the caller already handled the group leader through its own `-pgid` kill).
260
+ *
261
+ * Identity first, always. One process-table snapshot is taken before anything is signalled, and a tracked pid takes
262
+ * part only when its CURRENT start time still equals the one recorded when it was first seen. A `pnpm test` that
263
+ * forks hundreds of short-lived children leaves a set whose pids are mostly dead by the end; without this check the
264
+ * sweep would TERM+KILL whatever unrelated process of the same user the OS had meanwhile given one of those numbers
265
+ * to — a bare-pid kill, which DESIGN 4.4 step 5 forbids here exactly as it does across a restart — and would count
266
+ * it as an escapee on top. A pid that is gone, or whose start time has moved, is neither signalled nor counted.
267
+ *
268
+ * Of the survivors that ARE ours, the KILL is total — every tracked descendant, whatever its group. The COUNT is
269
+ * not: DESIGN 2.6.6 defines `ExecResult.escapees` as "processes that LEFT the group". A tracked pid whose current
270
+ * pgid still equals `leaderPid` is an ordinary grandchild outliving its parent — routine, and counting it would
271
+ * raise a false escapee on DESIGN 4.4's resume diagnostics and on any `escapees > 0` alerting; a pid that
272
+ * `setsid()`ed away has its own pgid and IS one (S-22). When the snapshot cannot be taken at all, nothing is
273
+ * signalled and nothing is claimed: an unverifiable pid is not a target.
274
+ */
275
+ async function sweepTracked(seen, leaderPid, wait, graceMs) {
276
+ if (seen.size === 0) return 0;
277
+ let table;
278
+ try {
279
+ table = await processTable();
280
+ } catch {
281
+ return 0;
282
+ }
283
+ const current = new Map(table.map((row) => [row.pid, row]));
284
+ let escapees = 0;
285
+ const targets = [];
286
+ for (const [pid, start] of seen) {
287
+ if (pid === leaderPid) continue;
288
+ const row = current.get(pid);
289
+ if (row === void 0 || row.start !== start) continue;
290
+ targets.push(pid);
291
+ if (row.pgid !== leaderPid) escapees += 1;
292
+ }
293
+ await terminatePids(targets, wait, graceMs);
294
+ return escapees;
295
+ }
296
+ /**
297
+ * The post-exit safety net, and a BEST-EFFORT net, not an identity-verified kill — be precise about what it is:
298
+ * once the leader has been reaped (Node emits `'exit'` AFTER `waitpid`) its pid is free for reuse, and this
299
+ * function keys on that same bare pgid NUMBER. It is narrower than `kill(-pgid)` in one respect only — it signals
300
+ * members one pid at a time, sparing a recycled leader (`row.pid !== pgid`) — and it does NOT verify a start token,
301
+ * so a group that has taken over the recycled number would be signalled with its members. The window is the few
302
+ * milliseconds between the leader's reap and this call, and pids are allocated sequentially, so a recycle inside it
303
+ * is not realistic. The identity-verified path is `sweepGroupByToken`, which is what the Resumer (DESIGN 4.4
304
+ * step 5, "never kills on a bare pid") uses across restarts, where the window is unbounded and the guarantee must
305
+ * be real. Returns how many were signalled.
306
+ */
307
+ async function killGroupMembers(pgid, wait, graceMs) {
308
+ if (!isAddressableGroup(pgid)) return 0;
309
+ let members;
310
+ try {
311
+ members = (await processTable()).filter((row) => row.pgid === pgid && row.pid !== pgid).map((row) => row.pid);
312
+ } catch {
313
+ return 0;
314
+ }
315
+ return terminatePids(members, wait, graceMs);
316
+ }
317
+ /**
318
+ * A string that identifies WHEN `pid` started, not just its number — the "start token" of DESIGN 4.4 step 5.
319
+ * `undefined` when the pid is gone or the platform's process table cannot be read.
320
+ *
321
+ * The token must be EXEC-STABLE: it is minted at the `'spawn'` event, while the leader's process image is still
322
+ * `/bin/sh -c '<ulimit script>'`, and every later reading of it — which is all the Resumer ever has — sees the image
323
+ * the wrapper `exec`ed into. Anything image-dependent in the hash (a command line, an argv) therefore cannot round
324
+ * trip, and `sweepGroupByToken` would never verify a group this executor recorded. Only start TIME qualifies.
325
+ *
326
+ * Resolution differs per platform, and it decides how narrow the pid-reuse window is. Linux reads `starttime` from
327
+ * `/proc/<pid>/stat`, in clock ticks since boot: two processes can share a pid only if one started long after the
328
+ * other died, so the token is effectively unique. macOS has no such counter; `ps -o lstart=` has ONE-SECOND
329
+ * resolution, so the residual risk there is a pid recycled by a process that started in the SAME SECOND. That is
330
+ * why nothing but a kill-tree cleanup is ever driven from this token, and why the program's identity is carried
331
+ * separately, by `PidRegistry.record({ label: req.file })` (contract/exec.ts).
332
+ */
333
+ async function processStartToken(pid, platform = process.platform) {
334
+ try {
335
+ if (platform === "linux") {
336
+ const stat = await readFile(`/proc/${pid}/stat`, "utf8");
337
+ const starttime = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19];
338
+ return starttime === void 0 || starttime === "" ? void 0 : `linux:${starttime}`;
339
+ }
340
+ const { stdout } = await execFileAsync("ps", [
341
+ "-o",
342
+ "lstart=",
343
+ "-p",
344
+ String(pid)
345
+ ]);
346
+ const identity = stdout.trim();
347
+ return identity === "" ? void 0 : `lstart:${sha256Hex(identity)}`;
348
+ } catch {
349
+ return;
350
+ }
351
+ }
352
+ /**
353
+ * Best-effort TERM -> grace -> KILL of the whole process group, addressed as `-pgid`. Never throws: a dead group
354
+ * is not an error. Only ever called while the group LEADER is known alive (an escalation during the run): once the
355
+ * leader has been reaped its pid can be recycled, and the post-exit cleanup uses `killGroupMembers` instead.
356
+ */
357
+ async function killGroup(pgid, wait, graceMs) {
358
+ if (!isAddressableGroup(pgid)) return;
359
+ trySignal(-pgid, "SIGTERM");
360
+ await wait(graceMs);
361
+ trySignal(-pgid, "SIGKILL");
362
+ }
363
+ //#endregion
364
+ //#region ../../packages/security/src/exec/ulimit.ts
365
+ const ULIMIT_REFUSED_EXIT = 126;
366
+ const ULIMIT_WRAPPER_SCRIPT = `[ -n "$1" ] && { ulimit -t "$1" 2>/dev/null || exit ${ULIMIT_REFUSED_EXIT}; }; [ -n "$2" ] && { ulimit -f "$2" 2>/dev/null || exit ${ULIMIT_REFUSED_EXIT}; }; [ -n "$3" ] && { ulimit -n "$3" 2>/dev/null || exit ${ULIMIT_REFUSED_EXIT}; }; [ -n "$4" ] && { ulimit -u "$4" 2>/dev/null || exit ${ULIMIT_REFUSED_EXIT}; }; shift 4; exec /usr/bin/env -u PWD -u SHLVL "\$0" "\$@"`;
367
+ /** Absolute, not resolved through PATH: the wrapper never depends on what an agent-controlled PATH would find. */
368
+ const ULIMIT_SHELL = "/bin/sh";
369
+ /**
370
+ * Can the wrapper carry this program at all? `false` for a path `/usr/bin/env` would read as a variable assignment
371
+ * (see the header): the executor turns that into `outcome: 'error'` and nothing is spawned. The check is on the
372
+ * path the wrapper's `env` actually receives as its first operand — that is `SandboxBackend.wrap()`'s output, not
373
+ * necessarily `ExecRequest.file`: when a backend prepends its own helper, the original program has become an
374
+ * ARGUMENT of that helper, which `env` never inspects.
375
+ */
376
+ function isWrappableProgramPath(file) {
377
+ return !file.includes("=");
378
+ }
379
+ /** Why a run was refused before any spawn, in the words the result carries in `guarantees.notes`. */
380
+ function unwrappableProgramNote(file) {
381
+ return `exec: refused to run '${file}' under the requested rlimits: a program path containing '=' cannot be carried through the rlimit wrapper — '/usr/bin/env' would read it as a variable assignment and exec the next argument instead. Nothing was spawned. (The same program runs when no rlimit is requested: there is no wrapper then.)`;
382
+ }
383
+ const toIntArg = (value) => value === void 0 ? "" : String(Math.max(0, Math.trunc(value)));
384
+ /**
385
+ * `ulimit -f` uses 1 KiB blocks on macOS and 512-byte blocks on Linux (the POSIX shells expose different units).
386
+ * At least one block once a POSITIVE byte limit was asked for —
387
+ * but `fileSizeBytes: 0` is a request in its own right ("this command may create no file at all"), and rounding it
388
+ * up to one block would hand the caller 1024 writable bytes while `guarantees` still claims the limit applied.
389
+ */
390
+ const toBlockArg = (bytes) => {
391
+ if (bytes === void 0) return "";
392
+ if (bytes <= 0) return "0";
393
+ const blockSize = process.platform === "linux" ? 512 : 1024;
394
+ return String(Math.max(1, Math.ceil(bytes / blockSize)));
395
+ };
396
+ /**
397
+ * Wraps `file`/`args` so the process starts under `ulimit -t/-f/-n/-u` before `exec` hands control to it. `limits`
398
+ * become POSITIONAL ARGUMENTS of the constant script (never text baked into it); an omitted limit is passed as an
399
+ * empty string and the script's own `[ -n "$k" ]` guard skips it. A limit that IS asked for and that the kernel
400
+ * refuses aborts with `ULIMIT_REFUSED_EXIT_CODE` instead of running unlimited. `ulimit -v` (address space) is not
401
+ * attempted: toolchain.md §8 measured it failing outright on macOS ("cannot modify limit: Invalid argument"), so
402
+ * `memoryBytes` is not enforced by this wrapper on any platform (SandboxCapabilities.memory stays `unavailable`).
403
+ */
404
+ function requestsAnyRlimit(limits) {
405
+ return limits.cpuSeconds !== void 0 || limits.fileSizeBytes !== void 0 || limits.openFiles !== void 0 || limits.processes !== void 0;
406
+ }
407
+ function wrapWithUlimit(file, args, limits) {
408
+ return {
409
+ file: ULIMIT_SHELL,
410
+ args: [
411
+ "-c",
412
+ ULIMIT_WRAPPER_SCRIPT,
413
+ file,
414
+ toIntArg(limits.cpuSeconds),
415
+ toBlockArg(limits.fileSizeBytes),
416
+ toIntArg(limits.openFiles),
417
+ toIntArg(limits.processes),
418
+ ...args
419
+ ]
420
+ };
421
+ }
422
+ //#endregion
423
+ //#region ../../packages/security/src/exec/executor.ts
424
+ /** Verified (toolchain.md §8: "detached: true + negative-pid kill... put a timeout on stream draining as well"). */
425
+ const GRACE_MS = 300;
426
+ const STREAM_DRAIN_TIMEOUT_MS = 500;
427
+ /** 40 ms while the leader is young (an escapee must be seen before it is reaped), backing off after one second. */
428
+ const TRACK_INTERVAL = {
429
+ initialMs: 40,
430
+ maxMs: 500,
431
+ rampAfterMs: 1e3
432
+ };
433
+ /** The built-in `none` `SandboxBackend`: wraps nothing, always reports the honest L0 level. */
434
+ function createNoneBackend() {
435
+ return {
436
+ id: "none",
437
+ probe: () => Promise.resolve(l0Capabilities()),
438
+ wrap: (file, args) => ({
439
+ file,
440
+ args: [...args]
441
+ })
442
+ };
443
+ }
444
+ /** The identity wrapper used whenever no L1 backend earned the reported guarantees. */
445
+ const NONE_BACKEND = createNoneBackend();
446
+ function backendsFor(backend) {
447
+ return backend.id === "none" ? [] : [backend];
448
+ }
449
+ /**
450
+ * DESIGN 2.6.6 / ADR-0003 §2b: "`sandbox.require: native` is satisfied only by `enforced`: with a `partial`
451
+ * backend the run refuses to start". `level: 'L1-os'` alone is not enough — a backend whose escape self-test
452
+ * (S-28 on macOS, S-29 on Linux) has not passed here reports `partial` on the axes that matter.
453
+ */
454
+ function satisfiesNative(guarantees) {
455
+ return guarantees.level === "L1-os" && guarantees.filesystem === "enforced" && guarantees.network === "enforced-off" && guarantees.processEscape === "denied";
456
+ }
457
+ /**
458
+ * DESIGN 2.6.6 L0 row: "Filesystem and network isolation are advisory." The plan makes that explicit for the roots
459
+ * a caller asked to be protected — `fs.readOnly` (the slot's dependency directories, DESIGN 5.7) and `fs.denyRead`
460
+ * — by RECORDING them on the result rather than silently ignoring them: at L0 nothing enforces them, and U4.07 is
461
+ * where these same roots become real. A request that asks for neither adds no note, so `ExecResult.guarantees`
462
+ * still equals `probeSandbox()` for it.
463
+ */
464
+ function advisoryFsNotes(req, guarantees) {
465
+ if (guarantees.filesystem === "enforced") return [];
466
+ const parts = [];
467
+ if (req.fs.readOnly.length > 0) parts.push(`readOnly: ${req.fs.readOnly.join(", ")}`);
468
+ if (req.fs.denyRead.length > 0) parts.push(`denyRead: ${req.fs.denyRead.join(", ")}`);
469
+ if (parts.length === 0) return [];
470
+ return [`filesystem is '${guarantees.filesystem}' at this level: the requested roots are recorded but NOT enforced (${parts.join("; ")}) — an OS sandbox backend is what enforces them`];
471
+ }
472
+ /** `guarantees` plus the notes that describe THIS request; never the report cached for `capabilities()`. */
473
+ function withNotes(guarantees, notes) {
474
+ return notes.length === 0 ? guarantees : {
475
+ ...guarantees,
476
+ notes: [...guarantees.notes, ...notes]
477
+ };
478
+ }
479
+ /** `req.cwd` must still refer to exactly what it claims to (defence against a TOCTOU swap between resolve and spawn). */
480
+ function cwdVerifies(cwd) {
481
+ try {
482
+ return realpathSync.native(cwd) === cwd;
483
+ } catch {
484
+ return false;
485
+ }
486
+ }
487
+ function createExecutor(options) {
488
+ const backend = options.backend ?? NONE_BACKEND;
489
+ const platform = process.platform;
490
+ let lastGuarantees;
491
+ return {
492
+ capabilities() {
493
+ return lastGuarantees ?? l0Capabilities(detectMissingL1Binaries(platform));
494
+ },
495
+ run: async (req, signal) => {
496
+ const run = await runOnce(options, backend, req, signal);
497
+ lastGuarantees = run.observed;
498
+ return run.result;
499
+ }
500
+ };
501
+ }
502
+ /**
503
+ * `pgid: 0` is the "nothing was spawned" sentinel of every early return, NOT a process group: on POSIX `kill(-0)`
504
+ * addresses the caller's own group, so `sweepGroupByToken`, `killGroup` and `killGroupMembers` all refuse a pgid
505
+ * of 0 or 1 outright (identity.ts). A dependant must read it as "no group", never as one to sweep.
506
+ */
507
+ function emptyResult(outcome, guarantees, redactor, startedMonoMs, clock) {
508
+ return {
509
+ exitCode: null,
510
+ outcome,
511
+ tail: redactor.sealText("").text,
512
+ outputSha256: sha256Hex(""),
513
+ outputBytes: 0,
514
+ truncated: false,
515
+ durationMs: clock.monotonicMs() - startedMonoMs,
516
+ pgid: 0,
517
+ startToken: "",
518
+ escapees: 0,
519
+ guarantees
520
+ };
521
+ }
522
+ async function runOnce(options, backend, req, signal) {
523
+ const platform = process.platform;
524
+ const startedMonoMs = options.clock.monotonicMs();
525
+ const resolved = await resolveSandbox(backendsFor(backend), platform);
526
+ const observed = resolved.capabilities;
527
+ const guarantees = withNotes(observed, advisoryFsNotes(req, observed));
528
+ const early = (outcome, notes = []) => ({
529
+ result: emptyResult(outcome, withNotes(guarantees, notes), options.redactor, startedMonoMs, options.clock),
530
+ observed
531
+ });
532
+ if (req.require === "native" && !satisfiesNative(observed)) return early("sandbox-denied", nativeShortfalls(observed, platform));
533
+ if (signal.aborted) return early("killed");
534
+ if (!cwdVerifies(req.cwd)) return early("error");
535
+ const outer = (resolved.backend ?? NONE_BACKEND).wrap(req.file, req.args, req);
536
+ const wrapping = requestsAnyRlimit(req.limits);
537
+ if (wrapping && !isWrappableProgramPath(outer.file)) return early("error", [unwrappableProgramNote(outer.file)]);
538
+ const launch = wrapping ? wrapWithUlimit(outer.file, outer.args, req.limits) : {
539
+ file: outer.file,
540
+ args: [...outer.args]
541
+ };
542
+ const child = spawn(launch.file, launch.args, {
543
+ cwd: req.cwd,
544
+ env: { ...req.env },
545
+ detached: true,
546
+ stdio: [
547
+ "ignore",
548
+ "pipe",
549
+ "pipe"
550
+ ]
551
+ });
552
+ const leaderPid = child.pid;
553
+ let outcome = "ok";
554
+ let truncated = false;
555
+ let totalBytes = 0;
556
+ let rawTail = "";
557
+ const chunks = [];
558
+ const decoders = {
559
+ stdout: new StringDecoder("utf8"),
560
+ stderr: new StringDecoder("utf8")
561
+ };
562
+ const sealChunk = (stream, bytes) => {
563
+ const text = decoders[stream].write(bytes);
564
+ rawTail = (rawTail + text).slice(-65536);
565
+ req.onChunk?.({
566
+ stream,
567
+ bytes: bytes.length,
568
+ text: options.redactor.sealText(text).text
569
+ });
570
+ };
571
+ let escalation;
572
+ const escalate = (why) => {
573
+ if (escalation !== void 0 || leaderPid === void 0) return;
574
+ outcome = why;
575
+ escalation = killGroup(leaderPid, (ms) => options.clock.sleep(ms), GRACE_MS);
576
+ };
577
+ const onData = (stream) => (chunk) => {
578
+ if (truncated) return;
579
+ let bytes = chunk;
580
+ if (totalBytes + bytes.length > req.maxOutputBytes) {
581
+ const room = Math.max(0, req.maxOutputBytes - totalBytes);
582
+ bytes = bytes.subarray(0, room);
583
+ truncated = true;
584
+ }
585
+ if (bytes.length > 0) {
586
+ totalBytes += bytes.length;
587
+ chunks.push(Buffer.from(bytes));
588
+ sealChunk(stream, bytes);
589
+ }
590
+ if (truncated) escalate("output-capped");
591
+ };
592
+ child.stdout?.on("data", onData("stdout"));
593
+ child.stderr?.on("data", onData("stderr"));
594
+ let exitCode = null;
595
+ let exitSignal;
596
+ const exited = new Promise((resolve) => {
597
+ child.once("exit", (code, sig) => {
598
+ exitCode = code;
599
+ exitSignal = sig ?? void 0;
600
+ resolve();
601
+ });
602
+ });
603
+ if (await new Promise((resolve) => {
604
+ child.once("spawn", () => resolve("spawned"));
605
+ child.once("error", () => resolve("error"));
606
+ }) === "error" || leaderPid === void 0) return early("error");
607
+ child.on("error", () => {});
608
+ const startToken = await processStartToken(leaderPid, platform) ?? `unknown:${leaderPid}`;
609
+ options.pids.record({
610
+ pgid: leaderPid,
611
+ startToken,
612
+ label: req.file
613
+ });
614
+ const escapeesSeen = /* @__PURE__ */ new Map();
615
+ const trackingTask = trackDescendants(leaderPid, escapeesSeen, exited, (ms) => options.clock.sleep(ms), TRACK_INTERVAL);
616
+ let finished = false;
617
+ const timeoutAbort = new AbortController();
618
+ const timeoutTask = options.clock.sleep(req.timeoutMs, timeoutAbort.signal).then(() => {
619
+ if (!finished) escalate("timed-out");
620
+ }).catch(() => {});
621
+ const onExternalAbort = () => escalate("killed");
622
+ if (!signal.aborted) signal.addEventListener("abort", onExternalAbort, { once: true });
623
+ await exited;
624
+ finished = true;
625
+ timeoutAbort.abort();
626
+ signal.removeEventListener("abort", onExternalAbort);
627
+ await timeoutTask;
628
+ await trackingTask;
629
+ if (escalation) await escalation;
630
+ await killGroupMembers(leaderPid, (ms) => options.clock.sleep(ms), GRACE_MS);
631
+ await drainStreams(child, (ms) => options.clock.sleep(ms).catch(() => {}));
632
+ const flushed = decoders.stdout.end() + decoders.stderr.end();
633
+ if (flushed !== "") rawTail = (rawTail + flushed).slice(-65536);
634
+ const escapees = await sweepTracked(escapeesSeen, leaderPid, (ms) => options.clock.sleep(ms), GRACE_MS);
635
+ options.pids.remove(leaderPid);
636
+ if (outcome === "ok" && exitCode === 126 && totalBytes === 0 && requestsAnyRlimit(req.limits)) outcome = "error";
637
+ const output = Buffer.concat(chunks);
638
+ const sealedTail = options.redactor.sealText(rawTail).text;
639
+ const durationMs = options.clock.monotonicMs() - startedMonoMs;
640
+ return {
641
+ result: {
642
+ exitCode,
643
+ outcome,
644
+ tail: sealedTail,
645
+ outputSha256: sha256Hex(output),
646
+ outputBytes: totalBytes,
647
+ truncated,
648
+ durationMs,
649
+ pgid: leaderPid,
650
+ startToken,
651
+ escapees,
652
+ guarantees,
653
+ ...exitSignal !== void 0 ? { signal: exitSignal } : {}
654
+ },
655
+ observed
656
+ };
657
+ }
658
+ async function drainStreams(child, wait) {
659
+ const closed = Promise.all([child.stdout, child.stderr].map((stream) => new Promise((resolve) => {
660
+ if (!stream || stream.destroyed) {
661
+ resolve();
662
+ return;
663
+ }
664
+ stream.once("close", () => resolve());
665
+ })));
666
+ let timedOut = false;
667
+ await Promise.race([closed.then(() => {}), wait(STREAM_DRAIN_TIMEOUT_MS).then(() => {
668
+ timedOut = true;
669
+ })]);
670
+ if (timedOut) {
671
+ child.stdout?.destroy();
672
+ child.stderr?.destroy();
673
+ }
674
+ }
675
+ //#endregion
676
+ //#region ../../packages/security/src/exec/index.ts
677
+ /**
678
+ * `cohorte doctor --json`'s "sandbox" section (spec 9 "garanties réellement actives"). Until U4.07 constructs a
679
+ * real Seatbelt/bubblewrap `SandboxBackend`, this always reports the honest L0 level: the L1 binaries this machine
680
+ * happens to have installed are detected only for `missing`/`notes`, never used to widen the claimed guarantees.
681
+ */
682
+ function probeSandbox(options = {}) {
683
+ return computeCapabilities(options.backends ?? [], options.platform ?? process.platform);
684
+ }
685
+ //#endregion
686
+ //#region ../../packages/security/src/redact/seal.ts
687
+ function sealText(text, redactions) {
688
+ return {
689
+ text,
690
+ redactions
691
+ };
692
+ }
693
+ function sealJson(value, redactions) {
694
+ return {
695
+ value,
696
+ redactions
697
+ };
698
+ }
699
+ //#endregion
700
+ //#region ../../packages/security/src/redact/index.ts
701
+ const replacement = "[REDACTED]";
702
+ const escapePointerToken = (token) => token.replaceAll("~", "~0").replaceAll("/", "~1");
703
+ function scrub(text, path, secrets, redactions) {
704
+ let result = text;
705
+ for (const { value, id } of secrets) if (result.includes(value)) {
706
+ result = result.split(value).join(replacement);
707
+ redactions.push({
708
+ path,
709
+ reason: "secret-value",
710
+ detector: `registered:${id}`
711
+ });
712
+ }
713
+ return result;
714
+ }
715
+ function createRedactor(options = {}) {
716
+ const secrets = [];
717
+ for (const [id, value] of Object.entries(options.secrets ?? {})) if (value.length >= 8) secrets.push({
718
+ value,
719
+ id
720
+ });
721
+ secrets.sort((a, b) => b.value.length - a.value.length);
722
+ const walk = (value, path, redactions) => {
723
+ if (typeof value === "string") return scrub(value, path, secrets, redactions);
724
+ if (Array.isArray(value)) return value.map((item, index) => walk(item, `${path}/${index}`, redactions));
725
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, walk(item, `${path}/${escapePointerToken(key)}`, redactions)]));
726
+ return value;
727
+ };
728
+ return {
729
+ registerSecret(value, id) {
730
+ if (value.length < 8) throw new RangeError("registerSecret: a secret value shorter than 8 characters is rejected");
731
+ secrets.push({
732
+ value,
733
+ id
734
+ });
735
+ secrets.sort((a, b) => b.value.length - a.value.length);
736
+ },
737
+ sealText(text) {
738
+ const redactions = [];
739
+ return sealText(scrub(text, "", secrets, redactions), redactions);
740
+ },
741
+ sealJson(value) {
742
+ const redactions = [];
743
+ return sealJson(walk(value, "", redactions), redactions);
744
+ }
745
+ };
746
+ }
747
+ //#endregion
748
+ //#region ../../packages/security/src/contract/builtin.ts
749
+ const frozen = (values) => Object.freeze(values);
750
+ /** Worktree-relative, any depth: `.git` (file OR directory), Cohorte's own directory, Pi's project directory. */
751
+ const PROTECTED_REPO_GLOBS = frozen([
752
+ "**/.git",
753
+ "**/.git/**",
754
+ "**/.cohorte/**",
755
+ "**/.pi/**"
756
+ ]);
757
+ frozen([
758
+ ".cohorte/keys",
759
+ ".cohorte/versions",
760
+ ".cohorte/pi-agent",
761
+ ".cohorte/brains",
762
+ ".cohorte/trust",
763
+ ".cohorte/config.yaml",
764
+ ".pi/agent",
765
+ ".ssh",
766
+ ".aws",
767
+ ".gnupg",
768
+ ".config/gh",
769
+ ".config/gcloud"
770
+ ]);
771
+ frozen([
772
+ ".pi/agent",
773
+ ".cohorte/keys",
774
+ ".cohorte/versions",
775
+ ".cohorte/pi-agent",
776
+ ".cohorte/brains",
777
+ ".cohorte/trust",
778
+ ".ssh",
779
+ ".aws",
780
+ ".gnupg",
781
+ ".config/gh",
782
+ ".config/gcloud"
783
+ ]);
784
+ /** The default `denyRead` / `denyWrite` of every AgentGrant (DESIGN 2.6.1). Deny sets always win. */
785
+ const DEFAULT_DENY_GLOBS = frozen([
786
+ "**/.env*",
787
+ "**/*.pem",
788
+ "**/*.key",
789
+ "**/id_rsa*",
790
+ "**/id_ed25519*",
791
+ "**/.git",
792
+ "**/.git/**",
793
+ "**/.cohorte/**",
794
+ "**/.pi/**",
795
+ "**/.npmrc",
796
+ "**/.netrc"
797
+ ]);
798
+ /**
799
+ * Programs that run OTHER programs or reach the network: denied with `overridable: false` (DESIGN 2.6.4 step 2).
800
+ * `pi` and `cohorte` are in it: Pi has an `auth` subcommand that prints the OAuth token. A project that truly needs one
801
+ * declares an exact-argv rule under `policy.dangerousCommands`, and every use is an `ask`.
802
+ */
803
+ const TRAMPOLINE_PROGRAMS = frozen([
804
+ "sh",
805
+ "bash",
806
+ "zsh",
807
+ "dash",
808
+ "fish",
809
+ "ksh",
810
+ "csh",
811
+ "env",
812
+ "xargs",
813
+ "sudo",
814
+ "su",
815
+ "doas",
816
+ "eval",
817
+ "exec",
818
+ "nohup",
819
+ "time",
820
+ "watch",
821
+ "npx",
822
+ "pnpx",
823
+ "bunx",
824
+ "corepack",
825
+ "ssh",
826
+ "scp",
827
+ "curl",
828
+ "wget",
829
+ "nc",
830
+ "perl",
831
+ "ruby",
832
+ "osascript",
833
+ "pi",
834
+ "cohorte"
835
+ ]);
836
+ /** `python*`: python, python3, python3.12, pythonw ... */
837
+ const TRAMPOLINE_PREFIXES = frozen(["python"]);
838
+ const TRAMPOLINES = new Set(TRAMPOLINE_PROGRAMS);
839
+ /** `name` is the alias-normalised BARE program name; the comparison is exact-case (the name was resolved on disk first). */
840
+ function isTrampoline(name) {
841
+ return TRAMPOLINES.has(name) || TRAMPOLINE_PREFIXES.some((prefix) => name.startsWith(prefix));
842
+ }
843
+ /** Agents have no commit capability (D9): commits and merges are Cohorte's. Built-in, non-overridable. */
844
+ const AGENT_GIT_DENIED_SUBCOMMANDS = frozen([
845
+ "commit",
846
+ "push",
847
+ "merge",
848
+ "rebase",
849
+ "reset",
850
+ "checkout",
851
+ "switch",
852
+ "worktree",
853
+ "config",
854
+ "update-ref",
855
+ "filter-branch",
856
+ "gc"
857
+ ]);
858
+ frozen([
859
+ "PATH",
860
+ "HOME",
861
+ "LANG",
862
+ "LC_ALL",
863
+ "TERM",
864
+ "CI",
865
+ "TMPDIR",
866
+ "NO_COLOR",
867
+ "GIT_CONFIG_GLOBAL",
868
+ "GIT_CONFIG_NOSYSTEM",
869
+ "GIT_TERMINAL_PROMPT"
870
+ ]);
871
+ Object.freeze({
872
+ TERM: "dumb",
873
+ CI: "1",
874
+ NO_COLOR: "1",
875
+ GIT_CONFIG_GLOBAL: "/dev/null",
876
+ GIT_CONFIG_NOSYSTEM: "1",
877
+ GIT_TERMINAL_PROMPT: "0"
878
+ });
879
+ Object.freeze({ darwin: Object.freeze(["__CF_USER_TEXT_ENCODING"]) });
880
+ //#endregion
881
+ //#region ../../packages/security/src/contract/decisions.ts
882
+ /**
883
+ * Spec 9, exactly. The pure engine returns allow | deny | ask. allow-once / allow-for-run are APPROVAL RESOLUTIONS:
884
+ * rows in `approvals`, found by grantKey at stage 6, reported in the verdict with the approvalId so the audit trail
885
+ * says WHY. An `ask` nobody can answer becomes `deny`: an unanswerable ask never silently runs.
886
+ */
887
+ const POLICY_DECISIONS = [
888
+ "allow",
889
+ "deny",
890
+ "ask",
891
+ "allow-once",
892
+ "allow-for-run"
893
+ ];
894
+ const GATE_STAGE_NAMES = [
895
+ "liveness",
896
+ "schema",
897
+ "capability",
898
+ "path",
899
+ "command",
900
+ "network",
901
+ "budget",
902
+ "approval"
903
+ ];
904
+ const closed = { additionalProperties: false };
905
+ const oneOf$1 = (values) => Type.Unsafe({
906
+ type: "string",
907
+ enum: [...values]
908
+ });
909
+ const count$4 = () => Type.Integer({ minimum: 0 });
910
+ const canonicalPath = () => Type.String({ minLength: 1 });
911
+ const replay = () => oneOf$1(["idempotent", "at-most-once"]);
912
+ const globSet = () => Type.Object({
913
+ include: Type.Array(Type.String()),
914
+ exclude: Type.Array(Type.String())
915
+ }, closed);
916
+ const ResolvedPathSchema = Type.Object({
917
+ canonical: canonicalPath(),
918
+ relative: Type.String(),
919
+ root: canonicalPath(),
920
+ exists: Type.Boolean(),
921
+ identity: Type.Optional(Type.Object({
922
+ dev: Type.Number(),
923
+ ino: Type.Number(),
924
+ nlink: Type.Number()
925
+ }, closed)),
926
+ viaSymlink: Type.Boolean()
927
+ }, closed);
928
+ const NormalizedCallSchema = Type.Object({
929
+ tool: Type.String(),
930
+ paths: Type.Array(Type.Object({
931
+ arg: Type.String(),
932
+ resolved: ResolvedPathSchema,
933
+ intent: oneOf$1([
934
+ "read",
935
+ "write",
936
+ "create",
937
+ "list",
938
+ "exec-cwd"
939
+ ])
940
+ }, closed)),
941
+ command: Type.Optional(Type.Object({
942
+ file: canonicalPath(),
943
+ args: Type.Array(Type.String()),
944
+ cwd: canonicalPath(),
945
+ ruleId: Type.String(),
946
+ replay: replay(),
947
+ network: Type.Boolean(),
948
+ timeoutMs: count$4()
949
+ }, closed)),
950
+ input: JsonValueSchema,
951
+ grantKeyMaterial: JsonValueSchema
952
+ }, closed);
953
+ const ask = () => Type.Object({
954
+ stage: oneOf$1(GATE_STAGE_NAMES),
955
+ ruleId: Type.String(),
956
+ reason: Type.String()
957
+ }, closed);
958
+ Type.Unsafe(Type.Object({
959
+ decision: oneOf$1(POLICY_DECISIONS),
960
+ stage: oneOf$1(GATE_STAGE_NAMES),
961
+ ruleId: Type.String({ minLength: 1 }),
962
+ reason: Type.String(),
963
+ modelFacingReason: Type.String(),
964
+ overridable: Type.Boolean(),
965
+ securityViolation: Type.Boolean(),
966
+ asks: Type.Array(ask()),
967
+ evaluatedRules: Type.Array(Type.String()),
968
+ normalized: Type.Union([NormalizedCallSchema, Type.Null()]),
969
+ approvalId: Type.Optional(ApprovalId),
970
+ grantId: Type.Optional(Type.String())
971
+ }, closed));
972
+ Type.Unsafe(Type.Object({
973
+ agentId: AgentId,
974
+ role: Type.String({ minLength: 1 }),
975
+ digest: Sha256,
976
+ tools: Type.Array(Type.String()),
977
+ roots: Type.Object({
978
+ workspace: Type.Union([canonicalPath(), Type.Null()]),
979
+ readOnly: Type.Array(canonicalPath())
980
+ }, closed),
981
+ read: globSet(),
982
+ write: globSet(),
983
+ denyRead: globSet(),
984
+ denyWrite: globSet(),
985
+ commands: Type.Object({
986
+ default: Type.Literal("deny"),
987
+ rules: Type.Array(CommandRule)
988
+ }, closed),
989
+ secrets: Type.Array(Type.Object({
990
+ id: Type.String(),
991
+ exposeAs: Type.Literal("env"),
992
+ name: Type.String()
993
+ }, closed)),
994
+ temporary: Type.Array(Type.Object({
995
+ grantId: Type.String(),
996
+ approvalId: ApprovalId,
997
+ grantKey: Type.String(),
998
+ expires: oneOf$1(["call", "run"])
999
+ }, closed)),
1000
+ limits: Type.Object({
1001
+ maxToolCalls: count$4(),
1002
+ maxCallsPerMinute: count$4(),
1003
+ perTool: Type.Record(Type.String(), Type.Object({
1004
+ maxCalls: Type.Optional(count$4()),
1005
+ timeoutMs: count$4(),
1006
+ maxOutputBytes: count$4()
1007
+ }, closed))
1008
+ }, closed)
1009
+ }, closed));
1010
+ //#endregion
1011
+ //#region ../../packages/security/src/contract/exec.ts
1012
+ const oneOf = (values) => Type.Unsafe({
1013
+ type: "string",
1014
+ enum: [...values]
1015
+ });
1016
+ /** [S]. Annotated so that Biome never infers it (docs/v3/requests/U0.02.md R1). */
1017
+ const SandboxCapabilities = Type.Unsafe(Type.Object({
1018
+ level: oneOf(["L0-process", "L1-os"]),
1019
+ backend: oneOf([
1020
+ "none",
1021
+ "seatbelt",
1022
+ "bubblewrap"
1023
+ ]),
1024
+ filesystem: oneOf([
1025
+ "enforced",
1026
+ "partial",
1027
+ "advisory"
1028
+ ]),
1029
+ network: oneOf([
1030
+ "enforced-off",
1031
+ "partial",
1032
+ "unenforced"
1033
+ ]),
1034
+ processEscape: oneOf([
1035
+ "denied",
1036
+ "partial",
1037
+ "possible"
1038
+ ]),
1039
+ envFiltering: Type.Literal("enforced"),
1040
+ timeout: Type.Literal("enforced"),
1041
+ outputCap: Type.Literal("enforced"),
1042
+ cpuTime: oneOf(["enforced", "unavailable"]),
1043
+ memory: oneOf([
1044
+ "enforced",
1045
+ "node-only",
1046
+ "unavailable"
1047
+ ]),
1048
+ processes: oneOf(["enforced", "unavailable"]),
1049
+ killTree: oneOf(["pid-namespace", "process-group-with-sweep"]),
1050
+ missing: Type.Array(Type.String()),
1051
+ notes: Type.Array(Type.String())
1052
+ }, { additionalProperties: false }));
1053
+ //#endregion
1054
+ //#region ../../packages/runtime-contract/src/capabilities.ts
1055
+ const strict$5 = { additionalProperties: false };
1056
+ const Cap = Type.Union([
1057
+ Type.Object({ value: Type.Literal("yes") }, strict$5),
1058
+ Type.Object({
1059
+ value: Type.Literal("no"),
1060
+ why: Type.String()
1061
+ }, strict$5),
1062
+ Type.Object({
1063
+ value: Type.Literal("partial"),
1064
+ why: Type.String()
1065
+ }, strict$5)
1066
+ ]);
1067
+ const RuntimeCapabilities = Type.Object({
1068
+ contractVersion: Type.Literal("1"),
1069
+ /** the only legal value (C1); present so conformance can assert it */
1070
+ toolExecution: Type.Literal("host-delegated"),
1071
+ streaming: Cap,
1072
+ thinkingStream: Cap,
1073
+ send: Type.Object({
1074
+ steer: Cap,
1075
+ followUp: Cap
1076
+ }, strict$5),
1077
+ cancelCooperative: Cap,
1078
+ cancelHard: Cap,
1079
+ pause: Type.Object({
1080
+ toolBoundary: Cap,
1081
+ modelBoundary: Cap
1082
+ }, strict$5),
1083
+ continuationFromTranscript: Cap,
1084
+ processIsolation: Cap,
1085
+ envFiltering: Cap,
1086
+ brainSandbox: Cap,
1087
+ resourceLimits: Cap,
1088
+ budgetEnforcement: Type.Object({
1089
+ turns: Cap,
1090
+ modelRequests: Cap,
1091
+ tokens: Cap,
1092
+ context: Cap,
1093
+ wallClock: Cap,
1094
+ outputTokensPerRequest: Cap
1095
+ }, strict$5),
1096
+ /** 'no' = none possible (compaction and engine retries off) */
1097
+ hiddenModelCalls: Cap,
1098
+ usageReporting: Cap,
1099
+ effectiveModelReporting: Cap,
1100
+ quotaReporting: Cap,
1101
+ authStatusWithoutSecret: Cap,
1102
+ subscriptionModeAssertion: Cap,
1103
+ systemPromptExact: Cap,
1104
+ runtimePinning: Cap,
1105
+ platforms: Type.Object({
1106
+ darwin: Cap,
1107
+ linux: Cap,
1108
+ win32: Cap
1109
+ }, strict$5),
1110
+ hints: Type.Object({
1111
+ memoryPerAgentMb: Type.Number({ minimum: 0 }),
1112
+ coldStartMs: Type.Number({ minimum: 0 }),
1113
+ maxConcurrentAgents: Type.Integer({ minimum: 1 })
1114
+ }, strict$5)
1115
+ }, strict$5);
1116
+ //#endregion
1117
+ //#region ../../packages/runtime-contract/src/session.ts
1118
+ const strict$4 = { additionalProperties: false };
1119
+ const count$3 = () => Type.Integer({ minimum: 0 });
1120
+ const jsonMap = () => Type.Unsafe(Type.Record(Type.String(), JsonValueSchema));
1121
+ /** `format` is a label, e.g. 'jsonl-v3' | 'fake-ndjson-v1' — never interpreted by core */
1122
+ const TranscriptRef = Type.Object({
1123
+ path: Type.String(),
1124
+ format: Type.String()
1125
+ }, strict$4);
1126
+ /** R8: opaque to clients */
1127
+ const RuntimeSessionRef = Type.Object({
1128
+ runtime: Type.String(),
1129
+ engineVersion: Type.String(),
1130
+ sessionId: Type.String(),
1131
+ transcript: TranscriptRef
1132
+ }, strict$4);
1133
+ const UsageTotals = Type.Object({
1134
+ tokens: TokenUsage,
1135
+ modelRequests: count$3(),
1136
+ toolCalls: count$3(),
1137
+ turns: count$3(),
1138
+ wallClockMs: Type.Number({ minimum: 0 })
1139
+ }, strict$4);
1140
+ const EffectiveModel = Type.Object({
1141
+ provider: Type.String(),
1142
+ model: Type.String(),
1143
+ api: Type.Optional(Type.String()),
1144
+ baseUrl: Type.Optional(Type.String())
1145
+ }, strict$4);
1146
+ /** The ADAPTER's typed cause, recorded by the host BEFORE it acts. The engine's own stop reason is never a discriminator [X]. */
1147
+ const AgentStopCause = Type.Union([
1148
+ Type.Literal("host-terminated"),
1149
+ Type.Literal("model-stop"),
1150
+ Type.Literal("output-truncated"),
1151
+ Type.Literal("budget"),
1152
+ Type.Literal("cancelled"),
1153
+ Type.Literal("engine-error"),
1154
+ Type.Literal("process-exit")
1155
+ ]);
1156
+ const AgentExit = Type.Object({
1157
+ outcome: Type.Union([
1158
+ Type.Literal("completed"),
1159
+ Type.Literal("failed"),
1160
+ Type.Literal("cancelled"),
1161
+ Type.Literal("crashed")
1162
+ ]),
1163
+ stop: AgentStopCause,
1164
+ error: Type.Optional(ErrorInfo),
1165
+ usage: UsageTotals,
1166
+ lastSeq: count$3()
1167
+ }, strict$4);
1168
+ const delivery$1 = () => Type.Union([Type.Literal("steer"), Type.Literal("follow-up")]);
1169
+ Type.Union([Type.Object({
1170
+ kind: Type.Literal("user"),
1171
+ messageId: Type.String(),
1172
+ text: Type.String(),
1173
+ delivery: delivery$1()
1174
+ }, strict$4), Type.Object({
1175
+ kind: Type.Literal("host-note"),
1176
+ messageId: Type.String(),
1177
+ text: Type.String(),
1178
+ delivery: delivery$1()
1179
+ }, strict$4)]);
1180
+ Type.Object({
1181
+ runId: RunId,
1182
+ agentId: AgentId,
1183
+ incarnation: Type.Integer({ minimum: 1 }),
1184
+ state: Type.Union([
1185
+ Type.Literal("starting"),
1186
+ Type.Literal("running"),
1187
+ Type.Literal("awaiting-tool"),
1188
+ Type.Literal("paused"),
1189
+ Type.Literal("settling"),
1190
+ Type.Literal("exited")
1191
+ ]),
1192
+ pausedAt: Type.Optional(Type.Union([Type.Literal("tool-boundary"), Type.Literal("model-boundary")])),
1193
+ turn: count$3(),
1194
+ pendingToolCalls: Type.Array(ToolCallId),
1195
+ requestedModel: ModelRef,
1196
+ effectiveModel: Type.Optional(EffectiveModel),
1197
+ authMode: Type.Optional(AuthMode),
1198
+ usage: UsageTotals,
1199
+ contextTokens: Type.Optional(count$3()),
1200
+ contextWindow: Type.Optional(count$3()),
1201
+ session: RuntimeSessionRef,
1202
+ lastSeq: count$3(),
1203
+ /** pid, rssMb, lastHeartbeatAt, engine flags… never secrets */
1204
+ diagnostics: jsonMap()
1205
+ }, strict$4);
1206
+ //#endregion
1207
+ //#region ../../packages/runtime-contract/src/tools.ts
1208
+ const strict$3 = { additionalProperties: false };
1209
+ const RuntimeToolCall = Type.Object({
1210
+ runId: RunId,
1211
+ agentId: AgentId,
1212
+ incarnation: Type.Integer({ minimum: 1 }),
1213
+ /** tc_<incarnation>_<ordinal> */
1214
+ toolCallId: ToolCallId,
1215
+ /** the engine's own id, transcript correlation only */
1216
+ engineToolCallId: Type.Optional(Type.String()),
1217
+ /** 1-based, per incarnation, gapless */
1218
+ ordinal: Type.Integer({ minimum: 1 }),
1219
+ tool: Type.String(),
1220
+ /** as produced by the model after engine-side coercion; the host re-validates strictly */
1221
+ input: JsonValueSchema
1222
+ }, strict$3);
1223
+ const ToolProgress = Type.Object({
1224
+ text: Type.Optional(Type.String()),
1225
+ bytes: Type.Optional(Type.Integer({ minimum: 0 }))
1226
+ }, strict$3);
1227
+ const ToolContent = Type.Union([Type.Object({
1228
+ type: Type.Literal("text"),
1229
+ text: Type.Unsafe(Type.String())
1230
+ }, strict$3), Type.Object({
1231
+ type: Type.Literal("image"),
1232
+ mediaType: Type.String(),
1233
+ dataBase64: Type.String()
1234
+ }, strict$3)]);
1235
+ Type.Object({
1236
+ isError: Type.Boolean(),
1237
+ content: Type.Array(ToolContent),
1238
+ /** host asks the runtime to end the agent loop after this batch */
1239
+ terminate: Type.Optional(Type.Boolean()),
1240
+ /** opaque audit id, stored in the transcript */
1241
+ resultRef: Type.Optional(Type.String())
1242
+ }, strict$3);
1243
+ const TOOL_NAME_PATTERN = "^[a-z][a-z0-9_]{1,40}$";
1244
+ /** JSON Schema 2020-12, ONE flat top-level object: no $ref/$defs/oneOf at root (provider flattening). */
1245
+ const ToolInputSchema = Type.Unsafe(Type.Object({
1246
+ type: Type.Literal("object"),
1247
+ properties: Type.Optional(Type.Record(Type.String(), JsonValueSchema))
1248
+ }, {
1249
+ additionalProperties: true,
1250
+ not: { anyOf: [
1251
+ "$ref",
1252
+ "$defs",
1253
+ "definitions",
1254
+ "oneOf",
1255
+ "anyOf",
1256
+ "allOf"
1257
+ ].map((key) => ({ required: [key] })) }
1258
+ }));
1259
+ /** What the BRAIN needs to know. No paths, no commands: what a call may touch is decided host-side. */
1260
+ const ToolGrant = Type.Object({
1261
+ /** TOOL_NAME_PATTERN; never differs only by case from another grant (see toolGrantProblems) */
1262
+ tool: Type.String({ pattern: TOOL_NAME_PATTERN }),
1263
+ description: Type.String(),
1264
+ inputSchema: ToolInputSchema,
1265
+ /** ordering hint only */
1266
+ effect: Type.Union([
1267
+ Type.Literal("read"),
1268
+ Type.Literal("write"),
1269
+ Type.Literal("execute"),
1270
+ Type.Literal("network"),
1271
+ Type.Literal("control")
1272
+ ]),
1273
+ /** true for the result tool */
1274
+ terminal: Type.Boolean()
1275
+ }, strict$3);
1276
+ /**
1277
+ * The rule a schema cannot say: inside ONE grant list no two names are equal, or differ only by case (providers
1278
+ * fold tool names). Works on unvalidated names too, which is why it re-checks the pattern. Empty = no problem.
1279
+ */
1280
+ function toolGrantProblems(grants) {
1281
+ const pattern = new RegExp(TOOL_NAME_PATTERN);
1282
+ const problems = [];
1283
+ const seen = /* @__PURE__ */ new Map();
1284
+ for (const { tool } of grants) {
1285
+ if (!pattern.test(tool)) problems.push(`tool name ${JSON.stringify(tool)} does not match ${TOOL_NAME_PATTERN}`);
1286
+ const folded = tool.toLowerCase();
1287
+ const earlier = seen.get(folded);
1288
+ if (earlier === void 0) seen.set(folded, tool);
1289
+ else if (earlier === tool) problems.push(`tool ${JSON.stringify(tool)} is granted twice`);
1290
+ else problems.push(`tools ${JSON.stringify(earlier)} and ${JSON.stringify(tool)} differ only by case`);
1291
+ }
1292
+ return problems;
1293
+ }
1294
+ //#endregion
1295
+ //#region ../../packages/runtime-contract/src/events.ts
1296
+ const strict$2 = { additionalProperties: false };
1297
+ const count$2 = () => Type.Integer({ minimum: 0 });
1298
+ const boundary = () => Type.Union([Type.Literal("tool-boundary"), Type.Literal("model-boundary")]);
1299
+ const delivery = () => Type.Union([Type.Literal("steer"), Type.Literal("follow-up")]);
1300
+ const messageRole = () => Type.Union([
1301
+ Type.Literal("assistant"),
1302
+ Type.Literal("user"),
1303
+ Type.Literal("tool-result")
1304
+ ]);
1305
+ /**
1306
+ * A CLOSED five-value set on this frontier: an adapter maps any other engine stop reason (Pi 0.85.1 also has
1307
+ * 'pending' and 'deferred') to 'error' and emits runtime.warning{code: ENGINE_STOP_REASON_UNMAPPED}.
1308
+ */
1309
+ const ModelStop = Type.Union([
1310
+ Type.Literal("stop"),
1311
+ Type.Literal("length"),
1312
+ Type.Literal("tool-use"),
1313
+ Type.Literal("error"),
1314
+ Type.Literal("aborted")
1315
+ ]);
1316
+ const IsolationReport = Type.Object({
1317
+ level: Type.Union([
1318
+ Type.Literal("os"),
1319
+ Type.Literal("process"),
1320
+ Type.Literal("none")
1321
+ ]),
1322
+ filesystem: Type.Union([Type.Literal("enforced"), Type.Literal("advisory")]),
1323
+ network: Type.Union([
1324
+ Type.Literal("enforced"),
1325
+ Type.Literal("partial"),
1326
+ Type.Literal("none")
1327
+ ]),
1328
+ backend: Type.String()
1329
+ }, strict$2);
1330
+ const durable = (data) => ({
1331
+ durability: "durable",
1332
+ data
1333
+ });
1334
+ const ephemeral = (data) => ({
1335
+ durability: "ephemeral",
1336
+ data
1337
+ });
1338
+ /**
1339
+ * One row per event type: its durability and the schema of `data`. EVERY durable type has a named target in the
1340
+ * protocol catalogue, or an explicit "not forwarded" rule (DESIGN 2.3.3). `authSource: 'none'` exists only for
1341
+ * runtimes that hold no credential (the fake); a fake run reports the authMode its plan requested.
1342
+ */
1343
+ const RUNTIME_EVENT_TYPES = {
1344
+ "agent.spawned": durable(Type.Object({
1345
+ session: RuntimeSessionRef,
1346
+ requestedModel: ModelRef,
1347
+ tools: Type.Array(Type.String()),
1348
+ systemPromptSha256: Sha256,
1349
+ effectiveSystemPromptSha256: Sha256,
1350
+ isolation: IsolationReport
1351
+ }, strict$2)),
1352
+ "agent.started": durable(Type.Object({ taskSha256: Sha256 }, strict$2)),
1353
+ "agent.turn.started": ephemeral(Type.Object({ turn: count$2() }, strict$2)),
1354
+ "agent.turn.completed": durable(Type.Object({
1355
+ turn: count$2(),
1356
+ toolCalls: count$2()
1357
+ }, strict$2)),
1358
+ "agent.message.started": ephemeral(Type.Object({
1359
+ messageId: Type.String(),
1360
+ role: messageRole()
1361
+ }, strict$2)),
1362
+ "agent.message.delta": ephemeral(Type.Object({
1363
+ messageId: Type.String(),
1364
+ channel: Type.Union([
1365
+ Type.Literal("text"),
1366
+ Type.Literal("thinking"),
1367
+ Type.Literal("tool-input")
1368
+ ]),
1369
+ contentIndex: count$2(),
1370
+ delta: Type.String()
1371
+ }, strict$2)),
1372
+ "agent.message.completed": durable(Type.Object({
1373
+ messageId: Type.String(),
1374
+ role: messageRole(),
1375
+ textSha256: Sha256,
1376
+ textBytes: count$2(),
1377
+ preview: Type.String({ maxLength: 512 }),
1378
+ stop: Type.Optional(ModelStop)
1379
+ }, strict$2)),
1380
+ "model.requested": durable(Type.Object({
1381
+ requestId: Type.String(),
1382
+ model: ModelRef,
1383
+ contextSha256: Type.Optional(Sha256),
1384
+ contextTokensEstimate: Type.Optional(count$2()),
1385
+ attempt: Type.Integer({ minimum: 1 })
1386
+ }, strict$2)),
1387
+ "model.responded": durable(Type.Object({
1388
+ requestId: Type.String(),
1389
+ requestedModel: ModelRef,
1390
+ effectiveModel: EffectiveModel,
1391
+ authMode: AuthMode,
1392
+ authSource: Type.Union([
1393
+ Type.Literal("oauth"),
1394
+ Type.Literal("api-key"),
1395
+ Type.Literal("none")
1396
+ ]),
1397
+ durationMs: Type.Number({ minimum: 0 }),
1398
+ usage: TokenUsage,
1399
+ httpStatus: Type.Optional(Type.Integer()),
1400
+ attempt: Type.Integer({ minimum: 1 }),
1401
+ stop: ModelStop,
1402
+ quota: QuotaInfo,
1403
+ error: Type.Optional(ErrorInfo)
1404
+ }, strict$2)),
1405
+ "tool.call.requested": durable(Type.Object({ call: RuntimeToolCall }, strict$2)),
1406
+ "tool.call.rejected": durable(Type.Object({
1407
+ engineToolCallId: Type.Optional(Type.String()),
1408
+ tool: Type.String(),
1409
+ cause: Type.Union([
1410
+ Type.Literal("unknown-tool"),
1411
+ Type.Literal("invalid-input"),
1412
+ Type.Literal("output-truncated")
1413
+ ]),
1414
+ message: Type.String()
1415
+ }, strict$2)),
1416
+ "tool.call.progress": ephemeral(Type.Object({
1417
+ toolCallId: ToolCallId,
1418
+ update: ToolProgress
1419
+ }, strict$2)),
1420
+ "tool.call.delivered": durable(Type.Object({
1421
+ toolCallId: ToolCallId,
1422
+ isError: Type.Boolean(),
1423
+ terminate: Type.Boolean(),
1424
+ waitedMs: Type.Number({ minimum: 0 })
1425
+ }, strict$2)),
1426
+ "agent.paused": durable(Type.Object({ at: boundary() }, strict$2)),
1427
+ "agent.resumed": durable(Type.Unsafe(Type.Object({}, strict$2))),
1428
+ "agent.message.accepted": durable(Type.Object({
1429
+ messageId: Type.String(),
1430
+ delivery: delivery()
1431
+ }, strict$2)),
1432
+ "agent.exited": durable(AgentExit),
1433
+ "runtime.warning": durable(Type.Object({
1434
+ code: Type.String(),
1435
+ message: Type.String()
1436
+ }, strict$2))
1437
+ };
1438
+ const RUNTIME_EVENT_TYPE_NAMES = Object.freeze(Object.keys(RUNTIME_EVENT_TYPES));
1439
+ const envelope = {
1440
+ runId: RunId,
1441
+ agentId: AgentId,
1442
+ incarnation: Type.Integer({ minimum: 1 }),
1443
+ seq: count$2(),
1444
+ at: IsoInstant
1445
+ };
1446
+ Type.Unsafe(Type.Union(RUNTIME_EVENT_TYPE_NAMES.map((type) => {
1447
+ const row = RUNTIME_EVENT_TYPES[type];
1448
+ return Type.Object({
1449
+ type: Type.Literal(type),
1450
+ durability: Type.Literal(row.durability),
1451
+ ...envelope,
1452
+ data: row.data
1453
+ }, strict$2);
1454
+ })));
1455
+ //#endregion
1456
+ //#region ../../packages/runtime-contract/src/pin.ts
1457
+ const strict$1 = { additionalProperties: false };
1458
+ const count$1 = () => Type.Integer({ minimum: 0 });
1459
+ /** spec 16 */
1460
+ const RuntimePin = Type.Object({
1461
+ runtimeId: Type.String(),
1462
+ adapterVersion: Type.String(),
1463
+ engine: Type.Union([Type.Object({
1464
+ name: Type.String(),
1465
+ version: Type.String()
1466
+ }, strict$1), Type.Null()]),
1467
+ node: Type.Object({
1468
+ version: Type.String(),
1469
+ execPath: Type.String()
1470
+ }, strict$1),
1471
+ artifacts: Type.Array(Type.Object({
1472
+ role: Type.Union([
1473
+ Type.Literal("agent-host-bundle"),
1474
+ Type.Literal("engine-package-tree"),
1475
+ Type.Literal("install-lock")
1476
+ ]),
1477
+ path: Type.String(),
1478
+ sha256: Sha256,
1479
+ files: Type.Optional(count$1()),
1480
+ bytes: count$1()
1481
+ }, strict$1)),
1482
+ /** sha256(canonicalJson(all of the above)) */
1483
+ digest: Sha256
1484
+ }, strict$1);
1485
+ /** never contains a token, a refresh token or an account secret */
1486
+ const ProviderAuthStatus = Type.Object({
1487
+ provider: Type.String(),
1488
+ /** 'unknown-transient' = credential store locked: NEVER mapped to AUTH_REQUIRED */
1489
+ state: Type.Union([
1490
+ Type.Literal("oauth"),
1491
+ Type.Literal("api-key"),
1492
+ Type.Literal("absent"),
1493
+ Type.Literal("unknown-transient")
1494
+ ]),
1495
+ subscription: Type.Boolean(),
1496
+ source: Type.Optional(Type.String()),
1497
+ checkedAt: IsoInstant,
1498
+ /** non-secret account/tenant label WHEN the engine exposes one without a secret; absent otherwise */
1499
+ accountLabel: Type.Optional(Type.String()),
1500
+ /** Cohorte's own table, not the engine's subscription flag (§3.7) */
1501
+ billing: Type.Union([
1502
+ Type.Literal("plan-limits"),
1503
+ Type.Literal("metered"),
1504
+ Type.Literal("unknown")
1505
+ ]),
1506
+ caveat: Type.Optional(Type.String())
1507
+ }, strict$1);
1508
+ //#endregion
1509
+ //#region ../../packages/runtime-contract/src/spawn.ts
1510
+ const strict = { additionalProperties: false };
1511
+ const count = () => Type.Integer({ minimum: 0 });
1512
+ const limit = () => Type.Optional(Type.Integer({ minimum: 1 }));
1513
+ const ceiling = () => Type.Optional(count());
1514
+ const stringMap = () => Type.Unsafe(Type.Record(Type.String(), Type.String()));
1515
+ /** opaque label; the runtime MUST NOT branch on it */
1516
+ const AgentRole = Type.String();
1517
+ const AuthRequirement = Type.Object({
1518
+ mode: AuthMode,
1519
+ provider: Type.String(),
1520
+ /** pinned catalogue endpoint; a runtime that would talk to anything else MUST refuse to spawn */
1521
+ baseUrl: Type.String(),
1522
+ /** false unless the run plan carries an explicit api opt-in */
1523
+ allowApiKey: Type.Boolean()
1524
+ }, strict);
1525
+ /** rendered by the host into the run snapshot dir */
1526
+ const TaskInput = Type.Object({
1527
+ path: Type.String(),
1528
+ sha256: Sha256,
1529
+ bytes: count()
1530
+ }, strict);
1531
+ /**
1532
+ * `note` is delivered by the runtime AFTER `task` and BEFORE the first model request: as a second user message when
1533
+ * the engine can carry two in one prompt, otherwise appended to the first user message after
1534
+ * CONTINUATION_NOTE_SEPARATOR. Either way each text is a byte-identical contiguous span and `task` comes first
1535
+ * (conformance rule 12).
1536
+ */
1537
+ const Continuation = Type.Object({
1538
+ fromIncarnation: Type.Integer({ minimum: 1 }),
1539
+ note: TaskInput,
1540
+ /** used only if continuationFromTranscript = yes */
1541
+ transcript: Type.Optional(TranscriptRef)
1542
+ }, strict);
1543
+ /** runtime MUST verify sha256 before use */
1544
+ const PromptRef = Type.Object({
1545
+ id: Type.String(),
1546
+ path: Type.String(),
1547
+ sha256: Sha256,
1548
+ bytes: count()
1549
+ }, strict);
1550
+ const ContextEntry = Type.Object({
1551
+ id: Type.String(),
1552
+ tier: Type.Union([
1553
+ Type.Literal("system"),
1554
+ Type.Literal("doctrine"),
1555
+ Type.Literal("data"),
1556
+ Type.Literal("task"),
1557
+ Type.Literal("prior-results")
1558
+ ]),
1559
+ /** agent-output and untrusted-repository can never sit above 'data' */
1560
+ trust: Type.Union([
1561
+ Type.Literal("cohorte"),
1562
+ Type.Literal("human"),
1563
+ Type.Literal("untrusted-repository"),
1564
+ Type.Literal("agent-output")
1565
+ ]),
1566
+ source: Type.Object({
1567
+ kind: Type.Union([
1568
+ Type.Literal("asset"),
1569
+ Type.Literal("project-file"),
1570
+ Type.Literal("artifact"),
1571
+ Type.Literal("event-summary"),
1572
+ Type.Literal("inline")
1573
+ ]),
1574
+ ref: Type.String()
1575
+ }, strict),
1576
+ sha256: Sha256,
1577
+ bytes: count(),
1578
+ tokenEstimate: count()
1579
+ }, strict);
1580
+ /**
1581
+ * PROVENANCE ONLY. "Installer le contexte" (spec 5.2) is defined as: every byte the model sees is in exactly two
1582
+ * host-rendered files — `systemPrompt` (tiers `system` + `doctrine`) and `task` (tiers `data` + `task` +
1583
+ * `prior-results`) — plus, for a later incarnation, `continuation.note`. A runtime installs those three and NOTHING
1584
+ * else: it never opens `entries[].source`, never re-orders or re-renders tiers. The manifest travels so that the
1585
+ * runtime can record `manifestSha256` on `model.requested` and so that a second runtime has nothing to guess.
1586
+ * Tiers are trust/priority tiers (spec 7), not orchestration words.
1587
+ */
1588
+ const ContextManifest = Type.Object({
1589
+ /** sha256(canonicalJson(entries)) = "hash du contexte" of spec 19 */
1590
+ manifestSha256: Sha256,
1591
+ tokenLimit: count(),
1592
+ tokenEstimate: count(),
1593
+ /** deterministic order: tier, then id */
1594
+ entries: Type.Array(ContextEntry),
1595
+ reductions: Type.Array(Type.Object({
1596
+ entryId: Type.String(),
1597
+ strategy: Type.Union([
1598
+ Type.Literal("excerpt"),
1599
+ Type.Literal("outline"),
1600
+ Type.Literal("summary-with-refs"),
1601
+ Type.Literal("dropped")
1602
+ ]),
1603
+ fromBytes: count(),
1604
+ toBytes: count()
1605
+ }, strict)),
1606
+ exclusions: Type.Array(Type.Object({
1607
+ pattern: Type.String(),
1608
+ reason: Type.Union([
1609
+ Type.Literal("secret"),
1610
+ Type.Literal("outside-scope"),
1611
+ Type.Literal("size"),
1612
+ Type.Literal("binary")
1613
+ ])
1614
+ }, strict))
1615
+ }, strict);
1616
+ /** Isolation of the RUNTIME'S OWN agent process (the brain). Tool isolation is host-side. */
1617
+ const SandboxPolicy = Type.Object({
1618
+ /** spawn fails security/sandbox-unavailable below `os` */
1619
+ require: Type.Union([
1620
+ Type.Literal("os"),
1621
+ Type.Literal("os-if-available"),
1622
+ Type.Literal("process")
1623
+ ]),
1624
+ /** absolute canonical roots */
1625
+ filesystem: Type.Object({
1626
+ readOnly: Type.Array(Type.String()),
1627
+ readWrite: Type.Array(Type.String()),
1628
+ denyRead: Type.Array(Type.String())
1629
+ }, strict),
1630
+ network: Type.Object({
1631
+ mode: Type.Union([
1632
+ Type.Literal("none"),
1633
+ Type.Literal("provider-only"),
1634
+ Type.Literal("unrestricted")
1635
+ ]),
1636
+ allowHosts: Type.Array(Type.String())
1637
+ }, strict),
1638
+ /** allowlist; nothing else is inherited (D3) */
1639
+ env: Type.Object({
1640
+ allow: Type.Array(Type.String()),
1641
+ set: stringMap()
1642
+ }, strict),
1643
+ limits: Type.Object({
1644
+ maxOldSpaceMb: limit(),
1645
+ maxCpuSeconds: limit(),
1646
+ maxOpenFiles: limit()
1647
+ }, strict)
1648
+ }, strict);
1649
+ /** hard ceilings for ONE incarnation; absent = unlimited at this level */
1650
+ const Budget = Type.Object({
1651
+ maxTurns: ceiling(),
1652
+ maxModelRequests: ceiling(),
1653
+ maxToolCalls: ceiling(),
1654
+ maxInputTokens: ceiling(),
1655
+ maxOutputTokens: ceiling(),
1656
+ maxTotalTokens: ceiling(),
1657
+ /** stop (never auto-compact) when the last request's context exceeds this */
1658
+ maxContextTokens: ceiling(),
1659
+ maxWallClockMs: ceiling(),
1660
+ maxModelRequestMs: ceiling(),
1661
+ /** 0 = every retry is the host's (spec 11.3 "tous les retries sont visibles") */
1662
+ maxEngineRetries: count()
1663
+ }, strict);
1664
+ const SpawnRequest = Type.Object({
1665
+ runId: RunId,
1666
+ agentId: AgentId,
1667
+ role: AgentRole,
1668
+ model: ModelRef,
1669
+ systemPrompt: PromptRef,
1670
+ context: ContextManifest,
1671
+ tools: Type.Array(ToolGrant),
1672
+ sandbox: SandboxPolicy,
1673
+ budget: Budget,
1674
+ /** absolute, canonical; the path the MODEL is told about. The engine process MUST NOT use it as its cwd */
1675
+ workingDirectory: Type.String(),
1676
+ /** spec 6: spawn is idempotent on (runId, agentId, incarnation) */
1677
+ incarnation: Type.Integer({ minimum: 1 }),
1678
+ thinking: ThinkingLevel,
1679
+ /** spec 10.1 / D3 */
1680
+ auth: AuthRequirement,
1681
+ /** the first user message, by reference */
1682
+ task: TaskInput,
1683
+ /** a later incarnation of the same attempt */
1684
+ continuation: Type.Union([Continuation, Type.Null()])
1685
+ }, strict);
1686
+ //#endregion
1687
+ export { probeSandbox as _, RUNTIME_EVENT_TYPE_NAMES as a, toolGrantProblems as c, SandboxCapabilities as d, AGENT_GIT_DENIED_SUBCOMMANDS as f, createRedactor as g, isTrampoline as h, RUNTIME_EVENT_TYPES as i, AgentExit as l, PROTECTED_REPO_GLOBS as m, ProviderAuthStatus as n, ToolContent as o, DEFAULT_DENY_GLOBS as p, RuntimePin as r, ToolGrant as s, SpawnRequest as t, RuntimeCapabilities as u, createExecutor as v, createNoneBackend as y };
1688
+
1689
+ //# sourceMappingURL=src-CId6kMsY.mjs.map