@agilno-tech/rivet 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +59 -0
  4. package/bin/cli.js +42 -0
  5. package/dist/agents/backend-django-agent/SKILL.md +35 -0
  6. package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
  7. package/dist/agents/boss-agent/SKILL.md +28 -0
  8. package/dist/agents/bounded-worker/SKILL.md +28 -0
  9. package/dist/agents/data-performance-agent/SKILL.md +32 -0
  10. package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
  11. package/dist/agents/devops-agent/SKILL.md +34 -0
  12. package/dist/agents/engineering-manager/SKILL.md +28 -0
  13. package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
  14. package/dist/agents/frontend-web-agent/SKILL.md +31 -0
  15. package/dist/agents/incident-response-agent/SKILL.md +42 -0
  16. package/dist/agents/jest-agent/SKILL.md +77 -0
  17. package/dist/agents/mobile-agent/SKILL.md +32 -0
  18. package/dist/agents/playwright-agent/SKILL.md +54 -0
  19. package/dist/agents/product-design-manager/SKILL.md +27 -0
  20. package/dist/agents/qa-agent/SKILL.md +29 -0
  21. package/dist/agents/quality-manager/SKILL.md +28 -0
  22. package/dist/agents/vitest-agent/SKILL.md +77 -0
  23. package/dist/governance/pre-push-rules.md +75 -0
  24. package/dist/governance/prompt-hygiene.md +22 -0
  25. package/dist/governance/review-checklist.md +24 -0
  26. package/dist/governance/safety-and-data.md +27 -0
  27. package/dist/governance/usage-rules.md +21 -0
  28. package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
  29. package/dist/mandatory/agentic-goal/SKILL.md +42 -0
  30. package/dist/mandatory/agentic-status/SKILL.md +74 -0
  31. package/dist/mandatory/apply-design/SKILL.md +228 -0
  32. package/dist/mandatory/check-ac/SKILL.md +124 -0
  33. package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
  34. package/dist/mandatory/design/SKILL.md +312 -0
  35. package/dist/mandatory/feature-workflow/SKILL.md +87 -0
  36. package/dist/mandatory/hotfix/SKILL.md +195 -0
  37. package/dist/mandatory/pre-push/SKILL.md +272 -0
  38. package/dist/mandatory/project-context/SKILL.md +180 -0
  39. package/dist/mandatory/release-docs/SKILL.md +191 -0
  40. package/dist/mandatory/review-pr/SKILL.md +323 -0
  41. package/dist/mandatory/security-review/SKILL.md +157 -0
  42. package/dist/skills/api-contract/SKILL.md +77 -0
  43. package/dist/skills/backend-django/SKILL.md +27 -0
  44. package/dist/skills/backend-nestjs/SKILL.md +65 -0
  45. package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
  46. package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
  47. package/dist/skills/database-migration/SKILL.md +97 -0
  48. package/dist/skills/debugging/SKILL.md +32 -0
  49. package/dist/skills/devops-infra/SKILL.md +26 -0
  50. package/dist/skills/documentation/SKILL.md +24 -0
  51. package/dist/skills/frontend-nextjs/SKILL.md +27 -0
  52. package/dist/skills/incident-postmortem/SKILL.md +38 -0
  53. package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
  54. package/dist/skills/mobile-react-native/SKILL.md +37 -0
  55. package/dist/skills/postgres-analytics/SKILL.md +29 -0
  56. package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
  57. package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
  58. package/dist/skills/refactoring/SKILL.md +32 -0
  59. package/dist/skills/sprint-planning/SKILL.md +32 -0
  60. package/dist/skills/testing-quality/SKILL.md +62 -0
  61. package/dist/v2/protocols/agent-orchestration.md +49 -0
  62. package/dist/v2/protocols/delivery.md +45 -0
  63. package/dist/v2/protocols/design-authority.md +29 -0
  64. package/dist/v2/protocols/goal-graph.md +44 -0
  65. package/dist/v2/protocols/pattern-first-development.md +29 -0
  66. package/dist/v2/protocols/qa-evidence.md +29 -0
  67. package/dist/v2/protocols/security.md +29 -0
  68. package/dist/v2/schemas/event.schema.json +86 -0
  69. package/dist/v2/schemas/evidence.schema.json +94 -0
  70. package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
  71. package/dist/v2/schemas/feature-plan.schema.json +95 -0
  72. package/dist/v2/schemas/goal-graph.schema.json +61 -0
  73. package/dist/v2/schemas/integration.schema.json +119 -0
  74. package/dist/v2/schemas/orchestration.schema.json +69 -0
  75. package/dist/v2/schemas/project.schema.json +209 -0
  76. package/dist/v2/schemas/providers.schema.json +142 -0
  77. package/dist/v2/schemas/quality.schema.json +50 -0
  78. package/dist/v2/schemas/work-action.schema.json +44 -0
  79. package/dist/v2/schemas/work-request.schema.json +253 -0
  80. package/dist/v2/templates/evidence/qa-bundle.json +194 -0
  81. package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
  82. package/dist/v2/templates/harness/SKILL.md +90 -0
  83. package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
  84. package/dist/v2/templates/project/.rivet/project.yaml +14 -0
  85. package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
  86. package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
  87. package/package.json +58 -0
  88. package/protocols/agent-orchestration.md +49 -0
  89. package/protocols/delivery.md +45 -0
  90. package/protocols/design-authority.md +29 -0
  91. package/protocols/goal-graph.md +44 -0
  92. package/protocols/pattern-first-development.md +29 -0
  93. package/protocols/qa-evidence.md +29 -0
  94. package/protocols/security.md +29 -0
  95. package/schemas/event.schema.json +86 -0
  96. package/schemas/evidence.schema.json +94 -0
  97. package/schemas/feature-decomposition.schema.json +48 -0
  98. package/schemas/feature-plan.schema.json +95 -0
  99. package/schemas/goal-graph.schema.json +61 -0
  100. package/schemas/integration.schema.json +119 -0
  101. package/schemas/orchestration.schema.json +69 -0
  102. package/schemas/project.schema.json +209 -0
  103. package/schemas/providers.schema.json +142 -0
  104. package/schemas/quality.schema.json +50 -0
  105. package/schemas/work-action.schema.json +44 -0
  106. package/schemas/work-request.schema.json +253 -0
  107. package/src/adapters/confluence.js +138 -0
  108. package/src/adapters/contract.js +657 -0
  109. package/src/adapters/factory.js +122 -0
  110. package/src/adapters/figma.js +178 -0
  111. package/src/adapters/fixtures.js +183 -0
  112. package/src/adapters/github.js +314 -0
  113. package/src/adapters/http.js +704 -0
  114. package/src/adapters/jira.js +195 -0
  115. package/src/adapters/linear.js +171 -0
  116. package/src/adapters/node-transport.js +59 -0
  117. package/src/cli/integration-setup-prompt.js +32 -0
  118. package/src/cli/interrupt.js +25 -0
  119. package/src/cli/main.js +596 -0
  120. package/src/cli/output.js +174 -0
  121. package/src/cli/parse-args.js +246 -0
  122. package/src/cli/project-discovery.js +70 -0
  123. package/src/cli/task-confirmation.js +38 -0
  124. package/src/cli/task-presentation.js +22 -0
  125. package/src/clients/claude.js +214 -0
  126. package/src/clients/codex.js +180 -0
  127. package/src/clients/compatibility.js +23 -0
  128. package/src/clients/contract.js +224 -0
  129. package/src/clients/fake.js +170 -0
  130. package/src/clients/process-runner.js +657 -0
  131. package/src/clients/result-contract.js +78 -0
  132. package/src/commands/delivery-publish.js +80 -0
  133. package/src/commands/delivery-remote.js +230 -0
  134. package/src/commands/delivery-review-update.js +39 -0
  135. package/src/commands/delivery-tracker.js +94 -0
  136. package/src/commands/delivery-transition.js +79 -0
  137. package/src/commands/delivery.js +223 -0
  138. package/src/commands/dependency-approval.js +21 -0
  139. package/src/commands/doctor.js +191 -0
  140. package/src/commands/evidence.js +44 -0
  141. package/src/commands/feature.js +212 -0
  142. package/src/commands/goals.js +231 -0
  143. package/src/commands/human-run.js +208 -0
  144. package/src/commands/human-task.js +278 -0
  145. package/src/commands/init.js +848 -0
  146. package/src/commands/install.js +814 -0
  147. package/src/commands/integration-setup.js +65 -0
  148. package/src/commands/integrations.js +54 -0
  149. package/src/commands/models.js +158 -0
  150. package/src/commands/orchestrate.js +61 -0
  151. package/src/commands/preflight.js +125 -0
  152. package/src/commands/protocols.js +228 -0
  153. package/src/commands/repositories.js +77 -0
  154. package/src/commands/setup-remote.js +41 -0
  155. package/src/commands/setup.js +214 -0
  156. package/src/commands/status.js +224 -0
  157. package/src/commands/support.js +44 -0
  158. package/src/commands/task-completion.js +104 -0
  159. package/src/commands/uninstall.js +234 -0
  160. package/src/commands/verify.js +171 -0
  161. package/src/commands/work.js +220 -0
  162. package/src/commands/worktrees.js +52 -0
  163. package/src/config/command-readiness.js +218 -0
  164. package/src/config/commands.js +206 -0
  165. package/src/config/defaults.js +55 -0
  166. package/src/config/load.js +230 -0
  167. package/src/config/validate.js +560 -0
  168. package/src/delivery/bitbucket-review.js +123 -0
  169. package/src/delivery/branch-publication.js +23 -0
  170. package/src/delivery/contract.js +379 -0
  171. package/src/delivery/github-deployment.js +132 -0
  172. package/src/delivery/github-review.js +5 -0
  173. package/src/delivery/github.js +382 -0
  174. package/src/delivery/gitlab-review.js +5 -0
  175. package/src/delivery/gitlab.js +395 -0
  176. package/src/delivery/prepare.js +90 -0
  177. package/src/delivery/publication-process.js +33 -0
  178. package/src/delivery/publication-transport.js +112 -0
  179. package/src/delivery/review-request.js +184 -0
  180. package/src/delivery/review-update.js +74 -0
  181. package/src/delivery/service.js +400 -0
  182. package/src/delivery/store.js +76 -0
  183. package/src/delivery/tracker-target.js +51 -0
  184. package/src/delivery/tracker-transition.js +158 -0
  185. package/src/delivery/tracker.js +164 -0
  186. package/src/discovery/git.js +85 -0
  187. package/src/discovery/portable.js +108 -0
  188. package/src/discovery/project.js +345 -0
  189. package/src/discovery/tools.js +145 -0
  190. package/src/evaluations/approval.js +20 -0
  191. package/src/evaluations/cost-policy.js +20 -0
  192. package/src/evaluations/harness-attempt.js +113 -0
  193. package/src/evaluations/live-runner.js +55 -0
  194. package/src/evaluations/profile.js +28 -0
  195. package/src/evaluations/report.js +11 -0
  196. package/src/evaluations/text-attempt.js +18 -0
  197. package/src/evidence/checksum.js +87 -0
  198. package/src/evidence/collect.js +727 -0
  199. package/src/evidence/validate.js +354 -0
  200. package/src/feature/accepted-integration.js +46 -0
  201. package/src/feature/actions.js +37 -0
  202. package/src/feature/client-profile.js +56 -0
  203. package/src/feature/decomposition-contract.js +58 -0
  204. package/src/feature/host-execution.js +633 -0
  205. package/src/feature/host-lock-recovery.js +96 -0
  206. package/src/feature/host-run-lock.js +32 -0
  207. package/src/feature/local-approval.js +183 -0
  208. package/src/feature/plan-contract.js +215 -0
  209. package/src/feature/planner.js +253 -0
  210. package/src/feature/run-store.js +241 -0
  211. package/src/feature/runtime-bridge.js +840 -0
  212. package/src/feature/verification-report.js +168 -0
  213. package/src/feature/workflow.js +375 -0
  214. package/src/git/client.js +509 -0
  215. package/src/git/integration-worktree.js +153 -0
  216. package/src/git/reconcile.js +222 -0
  217. package/src/git/reservations.js +450 -0
  218. package/src/git/worktrees.js +478 -0
  219. package/src/graph/completion.js +189 -0
  220. package/src/graph/fixtures.js +116 -0
  221. package/src/graph/reducer.js +124 -0
  222. package/src/graph/scheduler.js +252 -0
  223. package/src/graph/validate.js +106 -0
  224. package/src/install/managed.js +579 -0
  225. package/src/install/project-reference.js +33 -0
  226. package/src/install/project-runtime.js +142 -0
  227. package/src/install/runtime-integrity.cjs +141 -0
  228. package/src/integrations/capabilities.js +69 -0
  229. package/src/integrations/host-observation.js +26 -0
  230. package/src/integrations/registry.js +56 -0
  231. package/src/models/delegate.js +102 -0
  232. package/src/models/profiles.js +53 -0
  233. package/src/models/protocols.js +208 -0
  234. package/src/models/registry.js +83 -0
  235. package/src/models/transport.js +59 -0
  236. package/src/policy/approvals.js +251 -0
  237. package/src/policy/authority.js +282 -0
  238. package/src/policy/budget.js +149 -0
  239. package/src/policy/command-bootstrap.js +219 -0
  240. package/src/policy/commands.js +803 -0
  241. package/src/prompts/launch-contract.js +43 -0
  242. package/src/prompts/planning-contract.js +92 -0
  243. package/src/protocols/presentation.js +53 -0
  244. package/src/protocols/project.js +289 -0
  245. package/src/quality/runner.js +497 -0
  246. package/src/quality/traceability.js +215 -0
  247. package/src/repositories/identity.js +59 -0
  248. package/src/repositories/index.js +2 -0
  249. package/src/repositories/provider.js +210 -0
  250. package/src/runtime/application.js +353 -0
  251. package/src/runtime/dependency-directory.js +33 -0
  252. package/src/runtime/harness-discovery.js +121 -0
  253. package/src/runtime/heartbeat.js +47 -0
  254. package/src/runtime/instance-store.js +112 -0
  255. package/src/runtime/orchestrator.js +1133 -0
  256. package/src/runtime/portable-dependencies.js +94 -0
  257. package/src/runtime/recovery.js +190 -0
  258. package/src/runtime/retry.js +54 -0
  259. package/src/runtime/supervisor.js +166 -0
  260. package/src/runtime/worktree-bootstrap.js +169 -0
  261. package/src/state/event-store.js +333 -0
  262. package/src/state/lock.js +266 -0
  263. package/src/state/paths.js +274 -0
  264. package/src/state/redact.js +106 -0
  265. package/src/state/snapshot-store.js +310 -0
  266. package/src/status/public/app.js +96 -0
  267. package/src/status/public/index.html +37 -0
  268. package/src/status/public/styles.css +50 -0
  269. package/src/status/server.js +323 -0
  270. package/src/status/view-model.js +356 -0
  271. package/src/support/bundle.js +195 -0
  272. package/src/support/failure-report.js +96 -0
  273. package/src/work-request/contract.js +250 -0
  274. package/src/work-request/host.js +78 -0
  275. package/src/work-request/local.js +163 -0
  276. package/src/work-request/tracker.js +105 -0
  277. package/templates/evidence/qa-bundle.json +194 -0
  278. package/templates/github-actions/rivet-deploy.yml +98 -0
  279. package/templates/harness/SKILL.md +90 -0
  280. package/templates/project/.rivet/orchestration.yaml +42 -0
  281. package/templates/project/.rivet/project.yaml +14 -0
  282. package/templates/project/.rivet/providers.yaml +8 -0
  283. package/templates/project/.rivet/quality.yaml +16 -0
@@ -0,0 +1,657 @@
1
+ import { serializeProtocolLaunch } from '../protocols/presentation.js';
2
+ import { spawn } from 'node:child_process';
3
+ import { lstat, open, realpath } from 'node:fs/promises';
4
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
+
6
+ import { AgentContractError, containsSecretMaterial, failAgent, immutableJson } from './contract.js';
7
+ import { validateLaunchPayload } from '../prompts/launch-contract.js';
8
+ import { parsePlanningResult, validatePlanningPayload } from '../prompts/planning-contract.js';
9
+
10
+ const CONFIG_KEYS = new Set(['executable', 'interpreter', 'worktree', 'worktreeIdentity', 'environment', 'signal', 'launchTimeoutMs', 'timeoutMs', 'termGraceMs', 'killGraceMs', 'maxOutputBytes', 'maxInputBytes', 'allowOptionArgs', 'resultFormat']);
11
+ const REQUEST_KEYS = new Set(['args', 'cwd', 'payload', 'signal', 'protocolContext']);
12
+ // Keep the provider environment narrow, but preserve the non-secret account
13
+ // context required by native macOS clients to resolve their local login
14
+ // (Claude Code uses HOME and the shell identity variables when consulting its
15
+ // Keychain-backed session). Credential/token variables remain excluded below.
16
+ const SAFE_ENV = new Set(['LANG', 'LC_ALL', 'TZ', 'TERM', 'TMPDIR', 'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL']);
17
+ const SENSITIVE_ENV = /(?:^|_)(?:TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH|API_KEY|ACCESS_KEY|PRIVATE_KEY|KEY)(?:_|$)/i;
18
+ const WINDOWS_RESERVED = /^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9]|lpt[1-9])(?:\..*)?$/i;
19
+
20
+ export function assertSupportedClientPlatform(platform = process.platform) {
21
+ if (platform !== 'darwin' && platform !== 'linux') failAgent('unsupported-platform');
22
+ }
23
+
24
+ function capture(input, keys, required, reason) {
25
+ if (!input || typeof input !== 'object' || Array.isArray(input)) failAgent(reason);
26
+ let ownKeys;
27
+ try { ownKeys = Reflect.ownKeys(input); } catch { failAgent(reason); }
28
+ if (ownKeys.some(key => typeof key !== 'string' || !keys.has(key))) failAgent(reason);
29
+ const output = Object.create(null);
30
+ try {
31
+ for (const key of ownKeys) {
32
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
33
+ if (!descriptor?.enumerable) failAgent(reason);
34
+ output[key] = input[key];
35
+ }
36
+ } catch { failAgent(reason); }
37
+ if (required.some(key => !Object.hasOwn(output, key))) failAgent(reason);
38
+ return output;
39
+ }
40
+
41
+ function boundedInteger(value, fallback, maximum, reason = 'invalid-contract') {
42
+ const result = value ?? fallback;
43
+ if (!Number.isSafeInteger(result) || result <= 0 || result > maximum) failAgent(reason);
44
+ return result;
45
+ }
46
+
47
+ function captureSignal(value, reason = 'invalid-contract') {
48
+ try {
49
+ if (value !== undefined && (!(value instanceof AbortSignal) || Object.getPrototypeOf(value) !== AbortSignal.prototype)) failAgent(reason);
50
+ return value;
51
+ } catch { failAgent(reason); }
52
+ }
53
+
54
+ function signalAborted(signal, reason = 'invalid-contract') {
55
+ if (!signal) return false;
56
+ try { return signal.aborted === true; } catch { failAgent(reason); }
57
+ }
58
+
59
+ function watchSignal(signal, onAbort, reason = 'invalid-contract') {
60
+ if (!signal) return () => {};
61
+ let installed = false;
62
+ try {
63
+ signal.addEventListener('abort', onAbort, { once: true });
64
+ installed = true;
65
+ if (signal.aborted) onAbort();
66
+ } catch {
67
+ if (installed) {
68
+ try { signal.removeEventListener('abort', onAbort); } catch {}
69
+ }
70
+ failAgent(reason);
71
+ }
72
+ return () => {
73
+ try { signal.removeEventListener('abort', onAbort); } catch {}
74
+ };
75
+ }
76
+
77
+ function sameIdentity(left, right) { return left.dev === right.dev && left.ino === right.ino; }
78
+ function identity(metadata) { return Object.freeze({ dev: metadata.dev.toString(), ino: metadata.ino.toString() }); }
79
+
80
+ async function verifyExecutable(path, expected, guard = () => {}) {
81
+ if (typeof path !== 'string' || path.length < 2 || path.length > 1024 || !isAbsolute(path) || resolve(path) !== path || /[\u0000\r\n]/.test(path)) failAgent('executable-unsafe');
82
+ try {
83
+ const before = await lstat(path, { bigint: true });
84
+ guard();
85
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || (before.mode & 0o111n) === 0n) failAgent('executable-unsafe');
86
+ const canonical = await realpath(path);
87
+ guard();
88
+ const after = await lstat(path, { bigint: true });
89
+ guard();
90
+ if (canonical !== path || !after.isFile() || after.isSymbolicLink() || after.nlink !== 1n || !sameIdentity(before, after)) failAgent('executable-unsafe');
91
+ const current = Object.freeze({
92
+ ...identity(after), size: after.size.toString(), mode: after.mode.toString(),
93
+ mtimeMs: after.mtimeMs.toString(), ctimeMs: after.ctimeMs.toString(),
94
+ });
95
+ if (expected && Reflect.ownKeys(current).some(key => expected[key] !== current[key])) failAgent('executable-unsafe');
96
+ return current;
97
+ } catch (error) {
98
+ if (error instanceof AgentContractError) throw error;
99
+ if (error?.code === 'ENOENT') failAgent('provider-unavailable');
100
+ failAgent('executable-unsafe');
101
+ }
102
+ }
103
+
104
+ function sameSnapshot(left, right) {
105
+ return left && right && Reflect.ownKeys(left).length === Reflect.ownKeys(right).length
106
+ && Reflect.ownKeys(left).every(key => left[key] === right[key]);
107
+ }
108
+
109
+ function nativeHeader(header) {
110
+ const magic = header.subarray(0, 4).toString('hex');
111
+ if (process.platform === 'linux') return magic === '7f454c46';
112
+ return ['feedface', 'feedfacf', 'cefaedfe', 'cffaedfe', 'cafebabe', 'bebafeca'].includes(magic);
113
+ }
114
+
115
+ async function readPinnedHeader(path, expectedIdentity, guard = () => {}) {
116
+ let handle;
117
+ try {
118
+ handle = await open(path, 'r');
119
+ guard();
120
+ const metadata = await handle.stat({ bigint: true });
121
+ guard();
122
+ const current = Object.freeze({
123
+ ...identity(metadata), size: metadata.size.toString(), mode: metadata.mode.toString(),
124
+ mtimeMs: metadata.mtimeMs.toString(), ctimeMs: metadata.ctimeMs.toString(),
125
+ });
126
+ if (!sameSnapshot(current, expectedIdentity)) failAgent('executable-unsafe');
127
+ const header = Buffer.alloc(512);
128
+ const { bytesRead } = await handle.read(header, 0, header.length, 0);
129
+ guard();
130
+ return header.subarray(0, bytesRead);
131
+ } catch (error) {
132
+ if (error instanceof AgentContractError) throw error;
133
+ failAgent('executable-unsafe');
134
+ } finally {
135
+ try { await handle?.close(); } catch {}
136
+ }
137
+ }
138
+
139
+ async function inspectNativeInterpreter(path, expectedIdentity, guard = () => {}) {
140
+ const interpreterIdentity = await verifyExecutable(path, expectedIdentity, guard);
141
+ const header = await readPinnedHeader(path, interpreterIdentity, guard);
142
+ await verifyExecutable(path, interpreterIdentity, guard);
143
+ guard();
144
+ if (!nativeHeader(header)) failAgent('executable-unsafe');
145
+ return interpreterIdentity;
146
+ }
147
+
148
+ async function inspectLaunchTarget(executable, interpreter, expected, guard = () => {}) {
149
+ const executableIdentity = await verifyExecutable(executable, expected?.executableIdentity, guard);
150
+ const header = await readPinnedHeader(executable, executableIdentity, guard);
151
+ await verifyExecutable(executable, executableIdentity, guard);
152
+ guard();
153
+ let target;
154
+ if (nativeHeader(header)) {
155
+ if (interpreter !== undefined) failAgent('executable-unsafe');
156
+ target = Object.freeze({
157
+ kind: 'native', executableIdentity, interpreterIdentity: null,
158
+ spawnExecutable: executable, scriptPath: null,
159
+ });
160
+ } else {
161
+ let firstLine;
162
+ try {
163
+ const newline = header.indexOf(0x0a);
164
+ if (newline < 3 || newline > 256) failAgent('executable-unsafe');
165
+ firstLine = new TextDecoder('utf-8', { fatal: true }).decode(header.subarray(0, newline));
166
+ } catch { failAgent('executable-unsafe'); }
167
+ const pinnedShebang = typeof interpreter === 'string' && firstLine === `#!${interpreter}`;
168
+ const npmNodeShebang = firstLine === '#!/usr/bin/env node' && typeof interpreter === 'string' && basename(interpreter) === 'node';
169
+ if (!pinnedShebang && !npmNodeShebang) failAgent('executable-unsafe');
170
+ const interpreterIdentity = await inspectNativeInterpreter(interpreter, expected?.interpreterIdentity, guard);
171
+ target = Object.freeze({
172
+ kind: 'script', executableIdentity, interpreterIdentity,
173
+ spawnExecutable: interpreter, scriptPath: executable,
174
+ });
175
+ }
176
+ if (expected && (expected.kind !== target.kind || expected.spawnExecutable !== target.spawnExecutable
177
+ || expected.scriptPath !== target.scriptPath)) failAgent('executable-unsafe');
178
+ return target;
179
+ }
180
+
181
+ function invocation(target, args) {
182
+ return target.kind === 'script'
183
+ ? Object.freeze({ executable: target.spawnExecutable, args: Object.freeze([target.scriptPath, ...args]) })
184
+ : Object.freeze({ executable: target.spawnExecutable, args });
185
+ }
186
+
187
+ async function verifyDirectory(path, expected, reason = 'cwd-unsafe', guard = () => {}) {
188
+ if (typeof path !== 'string' || !isAbsolute(path) || resolve(path) !== path || path.length > 1024) failAgent(reason);
189
+ try {
190
+ const before = await lstat(path, { bigint: true });
191
+ guard();
192
+ if (!before.isDirectory() || before.isSymbolicLink()) failAgent(reason);
193
+ const canonical = await realpath(path);
194
+ guard();
195
+ const after = await lstat(path, { bigint: true });
196
+ guard();
197
+ if (canonical !== path || !after.isDirectory() || after.isSymbolicLink() || !sameIdentity(before, after)) failAgent(reason);
198
+ const current = identity(after);
199
+ if (expected && (expected.dev !== current.dev || expected.ino !== current.ino)) failAgent(reason);
200
+ return current;
201
+ } catch (error) {
202
+ if (error instanceof AgentContractError) throw error;
203
+ failAgent(reason);
204
+ }
205
+ }
206
+
207
+ function within(parent, child) {
208
+ const path = relative(parent, child);
209
+ return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path));
210
+ }
211
+
212
+ async function verifyCwd(worktree, value, guard = () => {}) {
213
+ if (typeof value !== 'string' || value.length === 0 || value.length > 500 || isAbsolute(value)
214
+ || value.includes('//') || value.includes('\\') || value.includes(':') || /[\u0000-\u001f\u007f]/.test(value)) failAgent('cwd-unsafe');
215
+ const normalized = value.normalize('NFKC');
216
+ if (normalized !== value) failAgent('cwd-unsafe');
217
+ const parts = value === '.' ? [] : value.split('/');
218
+ if (parts.some(part => !part || part === '.' || part === '..' || part.endsWith('.') || part.endsWith(' ') || WINDOWS_RESERVED.test(part))) failAgent('cwd-unsafe');
219
+ let cursor = worktree;
220
+ for (const part of parts) {
221
+ cursor = join(cursor, part);
222
+ try {
223
+ const metadata = await lstat(cursor, { bigint: true });
224
+ guard();
225
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) failAgent('cwd-unsafe');
226
+ } catch (error) {
227
+ if (error instanceof AgentContractError) throw error;
228
+ failAgent('cwd-unsafe');
229
+ }
230
+ }
231
+ const candidate = resolve(worktree, ...parts);
232
+ if (!within(worktree, candidate)) failAgent('cwd-unsafe');
233
+ await verifyDirectory(candidate, undefined, 'cwd-unsafe', guard);
234
+ guard();
235
+ return candidate;
236
+ }
237
+
238
+ function snapshotArgs(input, allowOptions) {
239
+ if (!Array.isArray(input)) failAgent('argument-invalid');
240
+ let length;
241
+ try { length = input.length; } catch { failAgent('argument-invalid'); }
242
+ if (!Number.isSafeInteger(length) || length > 128) failAgent('argument-invalid');
243
+ const output = [];
244
+ try {
245
+ for (let index = 0; index < length; index += 1) {
246
+ if (!Object.hasOwn(input, index)) failAgent('argument-invalid');
247
+ const value = input[index];
248
+ if (typeof value !== 'string' || value.length > 4096 || value.length === 0 || /[\u0000\r\n]/.test(value)
249
+ || (!allowOptions && value.startsWith('-'))) failAgent('argument-invalid');
250
+ output.push(value);
251
+ }
252
+ } catch { failAgent('argument-invalid'); }
253
+ return Object.freeze(output);
254
+ }
255
+
256
+ function environment(input) {
257
+ if (input === undefined) input = Object.create(null);
258
+ if (!input || typeof input !== 'object' || Array.isArray(input)) failAgent('invalid-contract');
259
+ let keys;
260
+ try { keys = Reflect.ownKeys(input); } catch { failAgent('invalid-contract'); }
261
+ if (keys.length > 64 || keys.some(key => typeof key !== 'string')) failAgent('invalid-contract');
262
+ const result = Object.create(null);
263
+ try {
264
+ for (const key of keys) {
265
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
266
+ if (!descriptor?.enumerable) failAgent('invalid-contract');
267
+ const value = input[key];
268
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(key) || typeof value !== 'string' || value.length > 32768 || value.includes('\0')) failAgent('invalid-contract');
269
+ if (SAFE_ENV.has(key) && !SENSITIVE_ENV.test(key)) result[key] = value;
270
+ }
271
+ } catch { failAgent('invalid-contract'); }
272
+ return Object.freeze({
273
+ PATH: result.PATH ?? '/usr/bin:/bin',
274
+ LANG: result.LANG ?? 'C.UTF-8', LC_ALL: result.LC_ALL ?? 'C.UTF-8', TZ: result.TZ ?? 'UTC',
275
+ TERM: 'dumb', NO_COLOR: '1', CI: '1', GIT_TERMINAL_PROMPT: '0',
276
+ ...(result.TMPDIR ? { TMPDIR: result.TMPDIR } : {}),
277
+ ...(result.HOME ? { HOME: result.HOME } : {}),
278
+ ...(result.USER !== undefined ? { USER: result.USER } : {}),
279
+ ...(result.LOGNAME !== undefined ? { LOGNAME: result.LOGNAME } : {}),
280
+ ...(result.SHELL !== undefined ? { SHELL: result.SHELL } : {}),
281
+ });
282
+ }
283
+
284
+ function strictDecode(buffer) {
285
+ try { return new TextDecoder('utf-8', { fatal: true }).decode(buffer); } catch { failAgent('output-invalid'); }
286
+ }
287
+
288
+ function claudeMeasuredUsage(envelope) {
289
+ const budgetStop = envelope?.subtype === 'error_max_budget_usd';
290
+ if (envelope?.type !== 'result' || (budgetStop ? envelope.is_error !== true : envelope.subtype !== 'success' || envelope.is_error !== false)
291
+ || !envelope.modelUsage || typeof envelope.modelUsage !== 'object' || Array.isArray(envelope.modelUsage)
292
+ || typeof envelope.total_cost_usd !== 'number' || !Number.isFinite(envelope.total_cost_usd)
293
+ || envelope.total_cost_usd < 0 || envelope.total_cost_usd > 100_000) failAgent('output-invalid');
294
+ const models = Object.values(envelope.modelUsage);
295
+ if (models.length === 0 || models.length > 64) failAgent('output-invalid');
296
+ let tokens = 0;
297
+ // modelUsage includes nested agents. The aggregate usage object covers only
298
+ // the main loop; adding both would count that loop twice. Cost is the native
299
+ // CLI's estimate, not a billing receipt or a model-written claim.
300
+ for (const model of models) {
301
+ if (!model || typeof model !== 'object' || Array.isArray(model)) failAgent('output-invalid');
302
+ for (const key of ['inputTokens', 'outputTokens', 'cacheCreationInputTokens', 'cacheReadInputTokens']) {
303
+ const count = model[key];
304
+ if (!Number.isSafeInteger(count) || count < 0) failAgent('output-invalid');
305
+ tokens += count;
306
+ if (!Number.isSafeInteger(tokens) || tokens > 10_000_000) failAgent('output-invalid');
307
+ }
308
+ }
309
+ return {tokens, costUsd: envelope.total_cost_usd};
310
+ }
311
+
312
+ function parseEnvelope(buffer, contract, resultFormat, requireBudgetStop = false) {
313
+ const source = strictDecode(buffer);
314
+ const document = source.trim();
315
+ if (!document || source.length - document.length > 16 || /[^\t\n\r ]/.test(source.slice(0, source.indexOf(document)))
316
+ || /[^\t\n\r ]/.test(source.slice(source.indexOf(document) + document.length))) failAgent('output-invalid');
317
+ let parsed;
318
+ try { parsed = JSON.parse(document); } catch { failAgent('output-invalid'); }
319
+ const captured = immutableJson(parsed);
320
+ if (resultFormat === 'claude-json' && captured?.type === 'result' && captured.subtype === 'error_max_budget_usd') {
321
+ return immutableJson({version: 1, status: 'budget-exhausted',
322
+ output: {summary: 'Claude stopped at its configured cost limit.', evidence: []},
323
+ usage: claudeMeasuredUsage(captured)});
324
+ }
325
+ if (requireBudgetStop) failAgent('provider-unavailable');
326
+ let value = captured && typeof captured === 'object' && !Array.isArray(captured)
327
+ && Object.hasOwn(captured, 'structured_output')
328
+ ? (captured.type === 'result' && captured.subtype === 'success' && captured.structured_output
329
+ && typeof captured.structured_output === 'object' && !Array.isArray(captured.structured_output)
330
+ ? captured.structured_output
331
+ : failAgent('output-invalid'))
332
+ : captured;
333
+ const keys = Reflect.ownKeys(value);
334
+ if (keys.length !== 4 || !['version', 'status', 'output', 'usage'].every(key => Object.hasOwn(value, key))
335
+ || value.version !== 1 || !['success', 'retry', 'failed', 'blocked', 'budget-exhausted'].includes(value.status)
336
+ || !value.output || typeof value.output !== 'object' || Array.isArray(value.output)
337
+ || !value.usage || typeof value.usage !== 'object' || Array.isArray(value.usage)) failAgent('output-invalid');
338
+ const outputKeys = Reflect.ownKeys(value.output);
339
+ const usageKeys = Reflect.ownKeys(value.usage);
340
+ if (outputKeys.length !== 2 || !['summary', 'evidence'].every(key => Object.hasOwn(value.output, key))
341
+ || typeof value.output.summary !== 'string' || value.output.summary.length === 0 || value.output.summary.length > 16_384
342
+ || containsSecretMaterial(value.output.summary)
343
+ || !Array.isArray(value.output.evidence) || value.output.evidence.length > 128
344
+ || value.output.evidence.some(item => typeof item !== 'string' || item.length === 0 || item.length > 500 || containsSecretMaterial(item))
345
+ || usageKeys.length !== 2 || !['tokens', 'costUsd'].every(key => Object.hasOwn(value.usage, key))
346
+ || !Number.isSafeInteger(value.usage.tokens) || value.usage.tokens < 0 || value.usage.tokens > 10_000_000
347
+ || typeof value.usage.costUsd !== 'number' || !Number.isFinite(value.usage.costUsd) || value.usage.costUsd < 0 || value.usage.costUsd > 100_000) {
348
+ failAgent('output-invalid');
349
+ }
350
+ if (resultFormat === 'claude-json') {
351
+ const usage = claudeMeasuredUsage(captured);
352
+ const exhausted = usage.tokens > contract.budget.maxTokens || usage.costUsd > contract.budget.maxCostUsd;
353
+ value = immutableJson({...value, usage, status: exhausted ? 'budget-exhausted' : value.status});
354
+ }
355
+ if (value.status !== 'budget-exhausted'
356
+ && (value.usage.tokens > contract.budget.maxTokens || value.usage.costUsd > contract.budget.maxCostUsd)) failAgent('output-invalid');
357
+ return value;
358
+ }
359
+
360
+ function killTree(child, signal) {
361
+ try {
362
+ if (process.platform !== 'win32' && child.pid) process.kill(-child.pid, signal);
363
+ else child.kill(signal);
364
+ } catch {}
365
+ }
366
+
367
+ // Sending SIGKILL is asynchronous: the direct child may close before its
368
+ // detached descendants finish exiting. Keep cleanup alive until the group is
369
+ // gone, or the bounded grace expires (a reparented zombie can retain the PGID).
370
+ async function settleTermination(child, graceMs) {
371
+ if (!child?.pid) return;
372
+ killTree(child, 'SIGKILL');
373
+ const deadline = performance.now() + graceMs;
374
+ while (performance.now() < deadline) {
375
+ try { process.kill(-child.pid, 0); }
376
+ catch (error) { if (error.code === 'ESRCH') return; }
377
+ await new Promise(resolve => setTimeout(resolve, 10));
378
+ }
379
+ }
380
+
381
+ export async function createProcessRunner(input) {
382
+ assertSupportedClientPlatform();
383
+ const config = capture(input, CONFIG_KEYS, ['executable', 'worktree'], 'invalid-contract');
384
+ const timeoutMs = boundedInteger(config.timeoutMs, 30_000, 10 * 60_000);
385
+ const launchTimeoutMs = boundedInteger(config.launchTimeoutMs, 5_000, 30_000);
386
+ const termGraceMs = boundedInteger(config.termGraceMs, 250, 10_000);
387
+ const killGraceMs = boundedInteger(config.killGraceMs, 1_000, 30_000);
388
+ const maxOutputBytes = boundedInteger(config.maxOutputBytes, 256 * 1024, 10 * 1024 * 1024);
389
+ const maxInputBytes = boundedInteger(config.maxInputBytes, 128 * 1024, 1024 * 1024);
390
+ if (config.allowOptionArgs !== undefined && typeof config.allowOptionArgs !== 'boolean') failAgent('invalid-contract');
391
+ if (config.resultFormat !== undefined && config.resultFormat !== 'claude-json') failAgent('invalid-contract');
392
+ const executable = config.executable;
393
+ const interpreter = config.interpreter;
394
+ const worktree = config.worktree;
395
+ const boundSignal = captureSignal(config.signal);
396
+ let constructionCancelled = signalAborted(boundSignal);
397
+ if (constructionCancelled) failAgent('aborted');
398
+ const removeConstructionListener = watchSignal(boundSignal, () => { constructionCancelled = true; });
399
+ const constructionGuard = () => {
400
+ if (constructionCancelled || signalAborted(boundSignal)) failAgent('aborted');
401
+ };
402
+ let launchTarget;
403
+ let worktreeIdentity;
404
+ try {
405
+ constructionGuard();
406
+ launchTarget = await inspectLaunchTarget(executable, interpreter, undefined, constructionGuard);
407
+ constructionGuard();
408
+ worktreeIdentity = await verifyDirectory(worktree, undefined, 'cwd-unsafe', constructionGuard);
409
+ constructionGuard();
410
+ if (config.worktreeIdentity !== undefined) {
411
+ const supplied = capture(config.worktreeIdentity, new Set(['dev', 'ino']), ['dev', 'ino'], 'cwd-unsafe');
412
+ if (typeof supplied.dev !== 'string' || typeof supplied.ino !== 'string'
413
+ || !/^[0-9]+$/.test(supplied.dev) || !/^[0-9]+$/.test(supplied.ino)
414
+ || supplied.dev !== worktreeIdentity.dev || supplied.ino !== worktreeIdentity.ino) failAgent('cwd-unsafe');
415
+ }
416
+ } finally { removeConstructionListener(); }
417
+ const executionEnvironment = environment(config.environment);
418
+
419
+ async function probe(args, help = false) {
420
+ let cancelled = signalAborted(boundSignal);
421
+ if (cancelled) failAgent('aborted');
422
+ let terminateChild;
423
+ const removeListener = watchSignal(boundSignal, () => {
424
+ cancelled = true;
425
+ terminateChild?.('aborted');
426
+ });
427
+ const guard = () => { if (cancelled || signalAborted(boundSignal)) failAgent('aborted'); };
428
+ try {
429
+ await verifyDirectory(worktree, worktreeIdentity, 'cwd-unsafe', guard);
430
+ guard();
431
+ const currentTarget = await inspectLaunchTarget(executable, interpreter, launchTarget, guard);
432
+ guard();
433
+ const command = invocation(currentTarget, args);
434
+ return await new Promise((resolvePromise, rejectPromise) => {
435
+ let child;
436
+ let classification;
437
+ let cleanupDeadline;
438
+ let settled = false;
439
+ let timer;
440
+ let killTimer;
441
+ let hardTimer;
442
+ const chunks = [];
443
+ let bytes = 0;
444
+ const finish = (error, value) => {
445
+ if (settled) return;
446
+ settled = true;
447
+ clearTimeout(timer); clearTimeout(killTimer); clearTimeout(hardTimer);
448
+ if (error && classification && child) {
449
+ settleTermination(child, Math.max(0, cleanupDeadline - performance.now())).then(() => rejectPromise(error), () => rejectPromise(error));
450
+ } else if (error) rejectPromise(error); else resolvePromise(value);
451
+ };
452
+ terminateChild = reason => {
453
+ if (classification || !child) return;
454
+ classification = reason;
455
+ cleanupDeadline = performance.now() + termGraceMs + killGraceMs;
456
+ killTree(child, 'SIGTERM');
457
+ killTimer = setTimeout(() => killTree(child, 'SIGKILL'), termGraceMs);
458
+ killTimer.unref?.();
459
+ hardTimer = setTimeout(() => {
460
+ killTree(child, 'SIGKILL');
461
+ finish(new AgentContractError(reason));
462
+ }, termGraceMs + killGraceMs);
463
+ hardTimer.unref?.();
464
+ };
465
+ if (cancelled) { finish(new AgentContractError('aborted')); return; }
466
+ try {
467
+ child = spawn(command.executable, command.args, {
468
+ cwd: worktree, shell: false, windowsHide: true, detached: true,
469
+ env: executionEnvironment, stdio: ['ignore', 'pipe', 'pipe'],
470
+ });
471
+ } catch { finish(new AgentContractError('provider-unavailable')); return; }
472
+ if (cancelled) terminateChild('aborted');
473
+ const collect = (chunk, keep) => {
474
+ if (classification) return;
475
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
476
+ const available = Math.max(0, (help ? 65536 : 4096) - bytes);
477
+ if (keep && available > 0) chunks.push(buffer.subarray(0, available));
478
+ bytes += Math.min(buffer.byteLength, available);
479
+ if (buffer.byteLength > available) terminateChild('provider-unavailable');
480
+ };
481
+ child.stdout.on('data', chunk => collect(chunk, true));
482
+ child.stderr.on('data', chunk => collect(chunk, false));
483
+ child.once('error', () => {
484
+ if (classification) killTree(child, 'SIGKILL');
485
+ finish(new AgentContractError(classification ?? 'provider-unavailable'));
486
+ });
487
+ child.once('close', code => {
488
+ if (classification) {
489
+ // A detached adapter can exit before a descendant in its process
490
+ // group. Kill the group before clearing the escalation timer.
491
+ killTree(child, 'SIGKILL');
492
+ finish(new AgentContractError(classification));
493
+ return;
494
+ }
495
+ if (code !== 0) { finish(new AgentContractError('provider-unavailable')); return; }
496
+ try {
497
+ const value = strictDecode(Buffer.concat(chunks)).trim();
498
+ if (!value || (!help && (value.length > 200 || /[\u0000-\u001f\u007f]/.test(value)))) failAgent('provider-unavailable');
499
+ finish(null, value);
500
+ } catch { finish(new AgentContractError('provider-unavailable')); }
501
+ });
502
+ timer = setTimeout(() => terminateChild('provider-unavailable'), Math.min(launchTimeoutMs, timeoutMs));
503
+ timer.unref?.();
504
+ });
505
+ } finally {
506
+ terminateChild = undefined;
507
+ removeListener();
508
+ }
509
+ }
510
+
511
+ async function execute(inputRequest, protocol) {
512
+ const request = capture(inputRequest, REQUEST_KEYS, ['args', 'cwd', 'payload'], 'invalid-contract');
513
+ const args = snapshotArgs(request.args, config.allowOptionArgs === true);
514
+ if (typeof request.payload !== 'string' || Buffer.byteLength(request.payload, 'utf8') > maxInputBytes || request.payload.includes('\0')) failAgent('invalid-contract');
515
+ let contract;
516
+ try {
517
+ contract = protocol === 'launch'
518
+ ? validateLaunchPayload(request.payload)
519
+ : validatePlanningPayload(request.payload);
520
+ } catch { failAgent('invalid-contract'); }
521
+ if (protocol !== 'launch' && request.protocolContext !== undefined) failAgent('invalid-contract');
522
+ let stdinPayload = protocol === 'launch'
523
+ ? serializeProtocolLaunch(contract, request.protocolContext) : request.payload;
524
+ if (Buffer.byteLength(stdinPayload, 'utf8') > maxInputBytes) failAgent('invalid-contract');
525
+ if (contract.worktree.path !== worktree
526
+ || contract.worktree.dev !== worktreeIdentity.dev || contract.worktree.ino !== worktreeIdentity.ino) failAgent('cwd-unsafe');
527
+ const requestSignal = captureSignal(request.signal);
528
+ if (boundSignal && requestSignal && boundSignal !== requestSignal) failAgent('invalid-contract');
529
+ const signal = requestSignal ?? boundSignal;
530
+ let cancelled = signalAborted(signal);
531
+ if (cancelled) failAgent('aborted');
532
+ let terminateChild;
533
+ const removeListener = watchSignal(signal, () => {
534
+ cancelled = true;
535
+ terminateChild?.('aborted');
536
+ });
537
+ const guard = () => { if (cancelled || signalAborted(signal)) failAgent('aborted'); };
538
+ try {
539
+ await verifyDirectory(worktree, worktreeIdentity, 'cwd-unsafe', guard);
540
+ guard();
541
+ const cwd = await verifyCwd(worktree, request.cwd, guard);
542
+ guard();
543
+ const currentTarget = await inspectLaunchTarget(executable, interpreter, launchTarget, guard);
544
+ guard();
545
+ if (protocol === 'launch') {
546
+ stdinPayload = serializeProtocolLaunch(contract, request.protocolContext);
547
+ if (Buffer.byteLength(stdinPayload, 'utf8') > maxInputBytes) failAgent('invalid-contract');
548
+ }
549
+ const command = invocation(currentTarget, args);
550
+ return await new Promise((resolvePromise, rejectPromise) => {
551
+ let child;
552
+ const stdout = [];
553
+ let totalBytes = 0;
554
+ let classification;
555
+ let cleanupDeadline;
556
+ let settled = false;
557
+ let runtimeTimer;
558
+ let launchTimer;
559
+ let termTimer;
560
+ let hardTimer;
561
+ const finish = (error, value) => {
562
+ if (settled) return;
563
+ settled = true;
564
+ clearTimeout(runtimeTimer); clearTimeout(launchTimer); clearTimeout(termTimer); clearTimeout(hardTimer);
565
+ if (error && classification && child) {
566
+ settleTermination(child, Math.max(0, cleanupDeadline - performance.now())).then(() => rejectPromise(error), () => rejectPromise(error));
567
+ } else if (error) rejectPromise(error); else resolvePromise(value);
568
+ };
569
+ terminateChild = reason => {
570
+ if (classification || !child) return;
571
+ classification = reason;
572
+ cleanupDeadline = performance.now() + termGraceMs + killGraceMs;
573
+ killTree(child, 'SIGTERM');
574
+ termTimer = setTimeout(() => killTree(child, 'SIGKILL'), termGraceMs);
575
+ termTimer.unref?.();
576
+ hardTimer = setTimeout(() => {
577
+ killTree(child, 'SIGKILL');
578
+ finish(new AgentContractError(reason));
579
+ }, termGraceMs + killGraceMs);
580
+ hardTimer.unref?.();
581
+ };
582
+ if (cancelled) { finish(new AgentContractError('aborted')); return; }
583
+ try {
584
+ child = spawn(command.executable, command.args, {
585
+ cwd, shell: false, windowsHide: true, detached: true,
586
+ env: executionEnvironment, stdio: ['pipe', 'pipe', 'pipe'],
587
+ });
588
+ } catch { finish(new AgentContractError('spawn-failed')); return; }
589
+ const collect = (target, chunk) => {
590
+ if (classification) return;
591
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
592
+ const available = Math.max(0, maxOutputBytes - totalBytes);
593
+ if (target && available > 0) target.push(buffer.subarray(0, available));
594
+ totalBytes += Math.min(buffer.byteLength, available);
595
+ if (buffer.byteLength > available) terminateChild('output-overflow');
596
+ };
597
+ child.stdout.on('data', chunk => collect(stdout, chunk));
598
+ child.stderr.on('data', chunk => collect(null, chunk));
599
+ // Once spawn() has returned a child, a closed provider stdin is an
600
+ // execution/availability failure. Classifying EPIPE as a spawn failure
601
+ // makes the result depend on whether stdin or close wins the event race.
602
+ child.stdin.on('error', () => terminateChild('provider-unavailable'));
603
+ child.once('error', () => {
604
+ if (classification) killTree(child, 'SIGKILL');
605
+ finish(new AgentContractError(classification ?? 'spawn-failed'));
606
+ });
607
+ child.once('spawn', () => clearTimeout(launchTimer));
608
+ child.once('close', code => {
609
+ if (classification) {
610
+ // The direct adapter may be gone while its descendants remain.
611
+ killTree(child, 'SIGKILL');
612
+ finish(new AgentContractError(classification));
613
+ return;
614
+ }
615
+ // The child emitted a close event, so it did spawn successfully. A
616
+ // non-zero exit is a provider execution failure, not a spawn failure;
617
+ // classify it as unavailable so the runtime can apply its bounded
618
+ // transient-provider retry policy.
619
+ if (code !== 0 && !(protocol === 'launch' && config.resultFormat === 'claude-json')) {
620
+ finish(new AgentContractError('provider-unavailable')); return;
621
+ }
622
+ try {
623
+ const bytes = Buffer.concat(stdout);
624
+ // A native cost-cap result can accompany a nonzero process exit.
625
+ // Only that validated terminal envelope preserves its accounting;
626
+ // any other nonzero exit remains a failed provider execution.
627
+ finish(null, protocol === 'launch' ? parseEnvelope(bytes, contract, config.resultFormat, code !== 0) : parsePlanningResult(bytes));
628
+ } catch (error) {
629
+ finish(error instanceof AgentContractError ? error : new AgentContractError('output-invalid'));
630
+ }
631
+ });
632
+ const runtimeLimit = protocol === 'launch' ? Math.min(timeoutMs, contract.budget.maxRuntimeMs) : timeoutMs;
633
+ runtimeTimer = setTimeout(() => terminateChild('timeout'), runtimeLimit);
634
+ runtimeTimer.unref?.();
635
+ launchTimer = setTimeout(() => terminateChild('launch-timeout'), Math.min(launchTimeoutMs, runtimeLimit));
636
+ launchTimer.unref?.();
637
+ if (cancelled) { terminateChild('aborted'); return; }
638
+ try { child.stdin.end(stdinPayload, 'utf8'); } catch { terminateChild('provider-unavailable'); }
639
+ });
640
+ } finally {
641
+ terminateChild = undefined;
642
+ removeListener();
643
+ }
644
+ }
645
+
646
+ async function run(inputRequest) { return execute(inputRequest, 'launch'); }
647
+ async function runPlanning(inputRequest) { return execute(inputRequest, 'planning'); }
648
+
649
+ return Object.freeze({
650
+ probeVersion: () => probe(['--version']),
651
+ probeHelp: provider => {
652
+ if (!['claude', 'codex'].includes(provider)) failAgent('invalid-contract');
653
+ return probe(provider === 'codex' ? ['exec', '--help'] : ['--help'], true);
654
+ },
655
+ run, runPlanning,
656
+ });
657
+ }