@bjoernrochel/sevro 0.1.0-rc.2 → 0.1.0-rc.21

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 (243) hide show
  1. package/CONTRIBUTING.md +170 -0
  2. package/LICENSE +63 -136
  3. package/README.md +83 -66
  4. package/docs/README.md +44 -0
  5. package/docs/acknowledgements.md +14 -0
  6. package/docs/architecture.md +146 -0
  7. package/docs/assets/README.md +27 -0
  8. package/docs/assets/sevro-logo.png +0 -0
  9. package/docs/assets/sevro-symbol.svg +1 -0
  10. package/docs/assets/sevro-wordmark.svg +1 -0
  11. package/docs/builtin-graders-v1.md +41 -13
  12. package/docs/codex-host.md +184 -52
  13. package/docs/contracts.md +19 -0
  14. package/docs/creating-cases.md +139 -0
  15. package/docs/development-cli.md +190 -30
  16. package/docs/documentation-quality.md +61 -0
  17. package/docs/evaluation-workflow.md +110 -0
  18. package/docs/evidence/code-mode-controls-71.md +64 -0
  19. package/docs/evidence/code-mode-programs-85.md +64 -0
  20. package/docs/evidence/codex-state-73/README.md +204 -0
  21. package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
  22. package/docs/evidence/codex-state-73/results.json +661 -0
  23. package/docs/evidence/local-http-74.md +104 -0
  24. package/docs/evidence/runtime-startup-30/README.md +185 -0
  25. package/docs/evidence/runtime-startup-30/after.json +319 -0
  26. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
  27. package/docs/evidence/runtime-startup-30/before.json +403 -0
  28. package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
  29. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
  30. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
  31. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
  32. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
  33. package/docs/evidence/skill-reads-70/results.json +823 -0
  34. package/docs/evidence.md +21 -0
  35. package/docs/extension-protocol-v1.md +50 -16
  36. package/docs/fixture-cleanup-validation.md +3 -0
  37. package/docs/getting-started.md +102 -0
  38. package/docs/guide-evaluation.md +89 -0
  39. package/docs/guide-verification.json +239 -0
  40. package/docs/identity-v1.md +35 -1
  41. package/docs/installing.md +88 -0
  42. package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
  43. package/docs/licensing.md +42 -0
  44. package/docs/native-controls.md +76 -5
  45. package/docs/native-hosts.md +228 -0
  46. package/docs/reading-results.md +141 -0
  47. package/docs/recipes.md +489 -0
  48. package/docs/releases.md +81 -40
  49. package/docs/report-v1.md +29 -1
  50. package/docs/results-v1.md +180 -0
  51. package/docs/run-settings-v1.md +147 -0
  52. package/docs/running-evaluations.md +31 -0
  53. package/docs/runtime-v1.md +614 -0
  54. package/docs/specs/repository-guide.md +58 -0
  55. package/docs/troubleshooting.md +30 -0
  56. package/docs/typescript-quality.md +234 -0
  57. package/docs/usability-study.md +114 -0
  58. package/examples/basic/host.ts +2 -2
  59. package/examples/recipes/candidate.ts +26 -0
  60. package/examples/recipes/cases/coding-change.json +21 -0
  61. package/examples/recipes/cases/host-tools.json +21 -0
  62. package/examples/recipes/cases/mise.json +21 -0
  63. package/examples/recipes/cases/node.json +21 -0
  64. package/examples/recipes/cases/python-uv.json +21 -0
  65. package/examples/recipes/cases/semantic.json +17 -0
  66. package/examples/recipes/coding-change/check.ts +23 -0
  67. package/examples/recipes/extension-artifacts.ts +65 -0
  68. package/examples/recipes/extension-methods.ts +174 -0
  69. package/examples/recipes/extension.ts +37 -0
  70. package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
  71. package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
  72. package/examples/recipes/repository-skill/README.md +3 -0
  73. package/examples/recipes/run-inputs.ts +188 -0
  74. package/examples/recipes/run-options.ts +131 -0
  75. package/examples/recipes/run.ts +63 -0
  76. package/examples/recipes/runtime/empty.json +1 -0
  77. package/examples/recipes/runtime/goals.json +1 -0
  78. package/examples/recipes/runtime/mise.json +6 -0
  79. package/examples/recipes/runtime/path.json +1 -0
  80. package/examples/recipes/runtime/plugins.json +4 -0
  81. package/examples/recipes/runtime/python-uv.json +10 -0
  82. package/examples/recipes/semantic-judge.ts +29 -0
  83. package/examples/templates/coding/adapter.ts +17 -0
  84. package/examples/templates/coding/case.json +15 -0
  85. package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
  86. package/examples/templates/repository-skill/extension-data.ts +64 -0
  87. package/examples/templates/repository-skill/extension.ts +31 -0
  88. package/examples/templates/response/adapter.ts +12 -0
  89. package/examples/templates/response/case.json +13 -0
  90. package/package.json +39 -6
  91. package/schemas/report-v1.schema.json +9 -0
  92. package/schemas/run-evidence-v1.schema.json +2 -1
  93. package/schemas/runtime-v1.schema.json +193 -0
  94. package/src/advisory.ts +10 -5
  95. package/src/candidate-artifacts.ts +208 -0
  96. package/src/candidate-codex-state.ts +185 -0
  97. package/src/case-validation.ts +105 -0
  98. package/src/cli/cancellation.ts +20 -0
  99. package/src/cli/command.ts +81 -0
  100. package/src/cli/completion.ts +15 -0
  101. package/src/cli/demo.ts +59 -0
  102. package/src/cli/evaluation.ts +55 -0
  103. package/src/cli/extension.ts +79 -0
  104. package/src/cli/help.ts +222 -0
  105. package/src/cli/hosts.ts +59 -0
  106. package/src/cli/init-settings.ts +30 -0
  107. package/src/cli/init.ts +111 -0
  108. package/src/cli/invocation.ts +627 -0
  109. package/src/cli/loading.ts +151 -0
  110. package/src/cli/preparation.ts +163 -0
  111. package/src/cli/presentation.ts +91 -0
  112. package/src/cli/progress.ts +6 -0
  113. package/src/cli/run-manifest.ts +133 -0
  114. package/src/cli/run-options.ts +101 -0
  115. package/src/cli/run-overrides.ts +83 -0
  116. package/src/cli/settings.ts +160 -0
  117. package/src/cli/summary.ts +43 -0
  118. package/src/cli.ts +13 -843
  119. package/src/contract-types.ts +94 -0
  120. package/src/engine.ts +19 -2373
  121. package/src/evaluation/advisory.ts +150 -0
  122. package/src/evaluation/check-plan.ts +510 -0
  123. package/src/evaluation/collection.ts +88 -0
  124. package/src/evaluation/context.ts +10 -0
  125. package/src/evaluation/grading-host.ts +39 -0
  126. package/src/evaluation/grading.ts +258 -0
  127. package/src/evaluation/invocation-plan.ts +548 -0
  128. package/src/evaluation/plan.ts +736 -0
  129. package/src/evaluation/progress.ts +19 -0
  130. package/src/evaluation/resources.ts +241 -0
  131. package/src/evaluation/run.ts +206 -0
  132. package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
  133. package/src/evaluation/semantic.ts +312 -0
  134. package/src/evaluation/trial.ts +866 -0
  135. package/src/evaluation/types.ts +207 -0
  136. package/src/evidence/host-results.ts +92 -0
  137. package/src/evidence/retained-artifacts.ts +18 -0
  138. package/src/evidence/trial-writer.ts +203 -0
  139. package/src/evidence/usage.ts +17 -0
  140. package/src/extension-client.ts +228 -148
  141. package/src/extension-session.ts +326 -195
  142. package/src/fixtures/advisory.ts +338 -0
  143. package/src/fixtures/bin.ts +57 -0
  144. package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
  145. package/src/fixtures/create.ts +187 -0
  146. package/src/fixtures/generated.ts +300 -0
  147. package/src/fixtures/git-hooks.ts +24 -0
  148. package/src/fixtures/preparation.ts +270 -0
  149. package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
  150. package/src/fixtures/setup.ts +224 -0
  151. package/src/fixtures/tools.ts +88 -0
  152. package/src/fixtures/workspace-state.ts +212 -0
  153. package/src/generated/cli-result.cjs +2 -0
  154. package/src/generated/cli-result.d.cts +3 -0
  155. package/src/generated/extension.cjs +2 -0
  156. package/src/generated/extension.d.cts +3 -0
  157. package/src/generated/report.cjs +2 -0
  158. package/src/generated/report.d.cts +3 -0
  159. package/src/generated/run-evidence.cjs +2 -0
  160. package/src/generated/run-evidence.d.cts +3 -0
  161. package/src/generated/runtime.cjs +2 -0
  162. package/src/generated/runtime.d.cts +3 -0
  163. package/src/graders/artifact.ts +56 -23
  164. package/src/graders/git-head.ts +79 -75
  165. package/src/graders/output.ts +222 -140
  166. package/src/graders/semantic.ts +102 -43
  167. package/src/graders/shell.ts +379 -207
  168. package/src/host-diagnostic-redaction.ts +52 -0
  169. package/src/host-diagnostics.ts +319 -0
  170. package/src/hosts/claude-continuation.ts +63 -18
  171. package/src/hosts/claude-credential.ts +43 -62
  172. package/src/hosts/claude-events.ts +86 -51
  173. package/src/hosts/claude-keychain.ts +59 -0
  174. package/src/hosts/claude-native-goal.ts +57 -33
  175. package/src/hosts/claude-process.ts +111 -0
  176. package/src/hosts/claude-repository-invocation.ts +208 -117
  177. package/src/hosts/claude-settings.ts +240 -21
  178. package/src/hosts/claude-skill-activations.ts +166 -0
  179. package/src/hosts/claude-state.ts +403 -0
  180. package/src/hosts/claude-tool-calls.ts +171 -106
  181. package/src/hosts/claude.ts +561 -451
  182. package/src/hosts/codex-app-server.ts +326 -178
  183. package/src/hosts/codex-configuration.ts +44 -18
  184. package/src/hosts/codex-events.ts +97 -60
  185. package/src/hosts/codex-exec-controls.ts +516 -0
  186. package/src/hosts/codex-invocation.ts +70 -0
  187. package/src/hosts/codex-marketplace.ts +283 -0
  188. package/src/hosts/codex-native-calls.ts +546 -500
  189. package/src/hosts/codex-process.ts +202 -0
  190. package/src/hosts/codex-profile.ts +257 -28
  191. package/src/hosts/codex-session-files.ts +106 -0
  192. package/src/hosts/codex-state.ts +492 -0
  193. package/src/hosts/codex.ts +783 -832
  194. package/src/hosts/isolation-common.ts +94 -0
  195. package/src/hosts/isolation-roots.ts +54 -27
  196. package/src/hosts/isolation.ts +10 -0
  197. package/src/hosts/linux-sandbox.ts +159 -0
  198. package/src/hosts/mac-sandbox.ts +187 -61
  199. package/src/hosts/native-controls.ts +152 -62
  200. package/src/hosts/process-lifecycle.ts +37 -0
  201. package/src/hosts/protected-worktrees.ts +51 -19
  202. package/src/hosts/skill-hook-catalog.ts +131 -0
  203. package/src/hosts/skill-hook-handler.ts +374 -0
  204. package/src/hosts/skill-hooks.ts +580 -0
  205. package/src/hosts/workspace-fingerprint.ts +55 -35
  206. package/src/identity.ts +102 -48
  207. package/src/instrumentation.ts +109 -48
  208. package/src/native-transcript-state.ts +131 -0
  209. package/src/native-transcript-view.ts +90 -0
  210. package/src/native-transcripts.ts +286 -0
  211. package/src/process-lifecycle.ts +150 -0
  212. package/src/provenance.ts +161 -81
  213. package/src/prune.ts +215 -0
  214. package/src/report.ts +172 -140
  215. package/src/resolved-case.ts +33 -0
  216. package/src/results-lock.ts +29 -0
  217. package/src/results.ts +133 -86
  218. package/src/run-owner.ts +154 -63
  219. package/src/runtime/config.ts +456 -0
  220. package/src/runtime/discovery-apple.ts +90 -0
  221. package/src/runtime/discovery-providers.ts +63 -0
  222. package/src/runtime/discovery.ts +70 -0
  223. package/src/runtime/hook-processes.ts +97 -0
  224. package/src/runtime/hooks.ts +476 -0
  225. package/src/runtime/paths.ts +313 -0
  226. package/src/runtime/query.ts +85 -0
  227. package/src/runtime/seeds.ts +134 -0
  228. package/src/runtime/shared-cache.ts +143 -0
  229. package/src/runtime/state.ts +143 -0
  230. package/src/schema-types.ts +198 -0
  231. package/src/schema.ts +15 -19
  232. package/src/transcript-retention.ts +146 -0
  233. package/src/value-guards.ts +20 -0
  234. package/src/advisory-fixture.ts +0 -282
  235. package/src/fixture-bin.ts +0 -87
  236. package/src/fixture-setup.ts +0 -163
  237. package/src/generated-fixture.ts +0 -220
  238. package/src/git-hooks.ts +0 -63
  239. package/src/hosts/claude-nested-skills.ts +0 -218
  240. package/src/hosts/codex-command-output.ts +0 -399
  241. package/src/hosts/codex-executor-command.ts +0 -195
  242. package/src/hosts/codex-skill-reads.ts +0 -391
  243. package/src/preparation.ts +0 -211
@@ -1,880 +1,831 @@
1
- import { runCodexAppServer } from "./codex-app-server";
2
- import { randomUUID } from "node:crypto";
3
1
  import { existsSync } from "node:fs";
4
2
  import {
5
- lstat,
6
- mkdir,
7
- mkdtemp,
8
- readFile,
9
- readdir,
10
- realpath,
11
- rm,
12
- stat,
13
- writeFile,
14
- } from "node:fs/promises";
3
+ requireCandidateRuntime,
4
+ requireCandidateArtifactRoots,
5
+ } from "../candidate-artifacts";
6
+ import { candidateCodexStateRetained } from "../candidate-codex-state";
7
+ import { lstat, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
15
8
  import { tmpdir } from "node:os";
16
- import { delimiter, dirname, isAbsolute, join, relative, sep } from "node:path";
17
- import type { HostAdapter } from "../engine";
18
- import { codexNativeControls } from "./native-controls";
19
- import { fixtureParts } from "../preparation";
20
- import { summarizeCodexEvents } from "./codex-events";
9
+ import { isAbsolute, join } from "node:path";
10
+ import type { HostAdapter, HostResult } from "../engine";
11
+ import {
12
+ allocateNativeState,
13
+ requireNativeTranscriptView,
14
+ } from "../native-transcript-state";
15
+ import {
16
+ failedNativeTranscripts,
17
+ retainEnabledNativeTranscripts,
18
+ } from "../native-transcripts";
19
+ import { runtimeNativeGoalEnabled } from "../runtime/config";
20
+ import {
21
+ releaseRuntimeHooks,
22
+ runtimeHookObservation,
23
+ type HookMounts,
24
+ } from "../runtime/hooks";
25
+ import { runtimeObservations } from "../runtime/state";
26
+ import { isOptionalNonblankString } from "../value-guards";
27
+ import { runCodexAppServer } from "./codex-app-server";
28
+ import { summarizeCodexEvents, type CodexEventSummary } from "./codex-events";
29
+ import { verifyCodexInvocation } from "./codex-invocation";
30
+ import { installCodexPlugins } from "./codex-marketplace";
21
31
  import {
22
32
  codexNativeCallObservation,
23
- codexNativeSkillReadRecovery,
24
33
  codexNativeSessionLastOrdinal,
25
34
  } from "./codex-native-calls";
26
- import { codexSkillReadObservation } from "./codex-skill-reads";
27
- import { codexPermissionProfile } from "./codex-profile";
28
- import { evaluationProtectedRoots } from "./isolation-roots";
35
+ import { runCodexProcess, CodexProcessFailure } from "./codex-process";
36
+ import {
37
+ codexHookMounts,
38
+ prepareCodexState,
39
+ type CodexHostOptions,
40
+ } from "./codex-state";
41
+ import { codexNativeControls } from "./native-controls";
29
42
  import { workspaceFingerprint } from "./workspace-fingerprint";
30
-
31
- const MAX_AUTH_BYTES = 1024 * 1024;
32
- const MAX_EVENT_BYTES = 8 * 1024 * 1024;
43
+ import { codexSkillCatalogRoots } from "./skill-hook-catalog";
44
+ import {
45
+ prepareSkillTracking,
46
+ skillTrackingObservation,
47
+ skillActivationClaims,
48
+ skillExposureClaims,
49
+ type SkillTracking,
50
+ } from "./skill-hooks";
51
+ import {
52
+ cleanupNativeState,
53
+ createHostDiagnostics,
54
+ diagnoseHostRequest,
55
+ diagnosticConfiguration,
56
+ diagnosticStage,
57
+ readDiagnosticCredentials,
58
+ retainReturnedHostFailure,
59
+ type HostDiagnostics,
60
+ } from "../host-diagnostics";
61
+ export type { CodexHostOptions } from "./codex-state";
33
62
  const DEFAULT_TIMEOUT_MS = 10 * 60_000;
34
63
  const MAX_TIMEOUT_MS = 30 * 60_000;
35
64
 
36
- async function verifyCodexInvocation(
37
- request: Parameters<HostAdapter["run"]>[0],
38
- ): Promise<void> {
39
- const selected = request.explicitSkillInvocation;
40
- if (!selected) return;
41
- const { skillName, token } = selected;
42
- const tokenCount =
43
- request.prompt.split(token).length -
44
- 1 +
45
- (request.followUpPrompt?.split(token).length ?? 1) -
46
- 1;
47
- if (
48
- !/^[A-Za-z0-9._-]+$/.test(skillName) ||
49
- skillName === "." ||
50
- skillName === ".." ||
51
- tokenCount !== 1
52
- )
53
- throw new Error("invalid Codex explicit skill invocation");
54
- if (selected.scope === "repository") {
55
- if (token !== `$${skillName}`)
56
- throw new Error("invalid Codex explicit skill invocation");
57
- let path = request.workspace;
58
- for (const [index, part] of [
59
- ".agents",
60
- "skills",
61
- skillName,
62
- "SKILL.md",
63
- ].entries()) {
64
- path = join(path, part);
65
- const entry = await lstat(path).catch(() => null);
66
- if (
67
- !entry ||
68
- entry.isSymbolicLink() ||
69
- (index === 3
70
- ? !entry.isFile() || entry.size > 1024 * 1024
71
- : !entry.isDirectory())
72
- )
73
- throw new Error(
74
- "invoked Codex repository skill is unavailable or unsafe",
75
- );
76
- }
77
- return;
78
- }
79
- const { pluginName } = selected;
80
- if (
81
- !request.codexMarketplace?.pluginNames.includes(pluginName) ||
82
- !/^[a-z][a-z0-9-]*$/.test(pluginName) ||
83
- token !== `$${pluginName}:${skillName}`
84
- )
85
- throw new Error("invalid Codex explicit skill invocation");
65
+ type Request = Parameters<HostAdapter["run"]>[0];
66
+ type ProcessResult = Pick<
67
+ Awaited<ReturnType<typeof runCodexProcess>>,
68
+ "code" | "out"
69
+ >;
70
+ type AppServerResult = Awaited<ReturnType<typeof runCodexAppServer>>;
71
+ type Observation = {
72
+ id: string;
73
+ completeness: "complete" | "partial";
74
+ data: Record<string, unknown>;
75
+ };
76
+ type CodexState = Awaited<ReturnType<typeof prepareCodexState>>;
77
+ interface ExecutionContext {
78
+ options: CodexHostOptions;
79
+ request: Request;
80
+ state: CodexState;
81
+ timeoutMs: number;
82
+ hooks: HookMounts;
83
+ skillTracking: SkillTracking;
84
+ diagnostics?: HostDiagnostics;
86
85
  }
87
-
88
- export interface CodexHostOptions {
89
- binary: string;
90
- authFile: string;
91
- model: string;
92
- effort: string;
93
- projectRoot: string;
94
- resultsRoot: string;
95
- additionalProtectedRoots: string[];
96
- agentConcurrencyLimit?: number | null;
97
- timeoutMs?: number;
98
- entrypoint?: "exec" | "app-server";
99
- /** Integration-test seam; production uses the same CLI for execution and preflight. */
100
- sandboxBinary?: string;
86
+ interface TurnResult {
87
+ execution: ProcessResult;
88
+ summary: CodexEventSummary;
89
+ followUp: ProcessResult | null;
90
+ followUpSummary: CodexEventSummary | null;
91
+ appServer: AppServerResult | null;
92
+ continuation: Observation | undefined;
101
93
  }
102
94
 
103
- function stopProcess(proc: Bun.Subprocess): void {
104
- if (process.platform !== "win32") {
105
- try {
106
- process.kill(-proc.pid, "SIGKILL");
107
- return;
108
- } catch {
109
- // The group may have exited before cancellation.
110
- }
111
- }
112
- try {
113
- proc.kill("SIGKILL");
114
- } catch {
115
- // The process already exited.
116
- }
95
+ function validCodexPaths(options: CodexHostOptions): boolean {
96
+ return (
97
+ [
98
+ options.binary,
99
+ options.authFile,
100
+ options.projectRoot,
101
+ options.resultsRoot,
102
+ ...options.additionalProtectedRoots,
103
+ ].every(isAbsolute) &&
104
+ (options.sandboxBinary === undefined || isAbsolute(options.sandboxBinary))
105
+ );
106
+ }
107
+ function validConcurrencyLimit(limit: number | null): boolean {
108
+ return limit === null || (Number.isSafeInteger(limit) && limit >= 1);
109
+ }
110
+ function validEntrypoint(entrypoint: CodexHostOptions["entrypoint"]): boolean {
111
+ return (
112
+ entrypoint === undefined || ["exec", "app-server"].includes(entrypoint)
113
+ );
114
+ }
115
+ function validCodexTimeout(timeoutMs: number): boolean {
116
+ return (
117
+ Number.isSafeInteger(timeoutMs) &&
118
+ timeoutMs >= 1 &&
119
+ timeoutMs <= MAX_TIMEOUT_MS
120
+ );
121
+ }
122
+ function validCodexIdentity(
123
+ options: CodexHostOptions,
124
+ limit: number | null,
125
+ timeoutMs: number,
126
+ ): boolean {
127
+ return (
128
+ !!options.model &&
129
+ !!options.effort &&
130
+ validEntrypoint(options.entrypoint) &&
131
+ validConcurrencyLimit(limit) &&
132
+ validCodexTimeout(timeoutMs)
133
+ );
134
+ }
135
+ function codexCapabilities(
136
+ entrypoint: CodexHostOptions["entrypoint"],
137
+ ): string[] {
138
+ return [
139
+ "sevro.host.continuation",
140
+ "sevro.host.runtime",
141
+ "sevro.host.native-transcripts",
142
+ "sevro.host.hooks",
143
+ "sevro.host.skill-activations",
144
+ "sevro.host.skill-activation-claims",
145
+ "sevro.host.skill-exposure-claims",
146
+ ...(entrypoint === "app-server" ? ["sevro.host.native-goal"] : []),
147
+ "sevro.codex.plugin-marketplace",
148
+ "sevro.codex.explicit-invocation",
149
+ "sevro.codex.repository-invocation",
150
+ "sevro.codex.native-calls",
151
+ "sevro.host.native-controls",
152
+ ];
153
+ }
154
+ function codexConfiguration(
155
+ limit: number | null,
156
+ entrypoint: CodexHostOptions["entrypoint"],
157
+ retainDiagnostics?: boolean,
158
+ ) {
159
+ return {
160
+ ...diagnosticConfiguration(retainDiagnostics),
161
+ "sevro.codex.agent-concurrency-limit": limit,
162
+ ...(entrypoint === "app-server"
163
+ ? { "sevro.codex.entrypoint": "app-server" }
164
+ : {}),
165
+ };
117
166
  }
118
167
 
119
- function shellQuote(value: string): string {
120
- return `'${value.replaceAll("'", `'"'"'`)}'`;
168
+ /** Construct one Codex route without inheriting user settings or credentials. */
169
+ export function createCodexHost(options: CodexHostOptions): HostAdapter {
170
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
171
+ const limit = options.agentConcurrencyLimit ?? null;
172
+ if (
173
+ !validCodexPaths(options) ||
174
+ !validCodexIdentity(options, limit, timeoutMs)
175
+ )
176
+ throw new Error("invalid Codex host configuration");
177
+ return {
178
+ id: "sevro.host.codex",
179
+ hostCapabilities: codexCapabilities(options.entrypoint),
180
+ model: options.model,
181
+ effort: options.effort,
182
+ configuration: codexConfiguration(
183
+ limit,
184
+ options.entrypoint,
185
+ options.retainHostDiagnostics,
186
+ ),
187
+ run(request) {
188
+ return runCodexRequest(options, timeoutMs, limit, request);
189
+ },
190
+ };
121
191
  }
122
192
 
123
- async function boundedText(
124
- stream: ReadableStream<Uint8Array>,
125
- limit: number,
126
- ): Promise<string> {
127
- const reader = stream.getReader();
128
- const chunks: Uint8Array[] = [];
129
- let size = 0;
130
- while (true) {
131
- const { value, done } = await reader.read();
132
- if (done) break;
133
- size += value.byteLength;
134
- if (size > limit)
135
- throw new Error("Codex event stream exceeds the size limit");
136
- chunks.push(value);
137
- }
138
- return new TextDecoder("utf-8", { fatal: true }).decode(
139
- Buffer.concat(chunks),
193
+ function requireCodexFollowUp(request: Request): void {
194
+ if (!isOptionalNonblankString(request.followUpPrompt))
195
+ throw new Error("Codex follow-up prompt must be nonempty");
196
+ }
197
+ function requireCodexFixtureBin(request: Request): void {
198
+ if (
199
+ request.fixtureBinDir !== undefined &&
200
+ request.fixtureBinDir !== join(request.workspace, ".git", "fixture-bin")
201
+ )
202
+ throw new Error("fixture binary path is outside the workspace");
203
+ }
204
+ function requirePassiveCodexRequest(request: Request): void {
205
+ if (request.instrumentation?.length)
206
+ throw new Error("Codex instrumentation is unavailable");
207
+ if (request.condition !== "passive")
208
+ throw new Error("Codex enforcement instrumentation is unavailable");
209
+ }
210
+ function allocateCodexRequestState(request: Request): Promise<string> {
211
+ return allocateNativeState(
212
+ "codex-",
213
+ request.runtimePolicy ? request.workspace : undefined,
140
214
  );
141
215
  }
142
216
 
143
- async function runProcess(options: {
144
- argv: string[];
145
- cwd: string;
146
- env: Record<string, string>;
147
- input?: string;
148
- timeoutMs: number;
149
- signal?: AbortSignal;
150
- }): Promise<{ code: number; out: string }> {
151
- if (options.signal?.aborted) throw new Error("Codex run cancelled");
152
- const proc = Bun.spawn(options.argv, {
153
- cwd: options.cwd,
154
- env: options.env,
155
- detached: process.platform !== "win32",
156
- stdin: options.input === undefined ? "ignore" : "pipe",
157
- stdout: "pipe",
158
- stderr: "ignore",
217
+ async function runCodexRequest(
218
+ options: CodexHostOptions,
219
+ timeoutMs: number,
220
+ limit: number | null,
221
+ request: Request,
222
+ ): Promise<HostResult> {
223
+ const diagnostics = createHostDiagnostics(
224
+ "codex",
225
+ options.retainHostDiagnostics,
226
+ request,
227
+ );
228
+ return diagnoseHostRequest(diagnostics, async () => {
229
+ diagnosticStage(diagnostics, "request-validation");
230
+ await readDiagnosticCredentials(diagnostics, options.authFile);
231
+ return executeCodexRequest(options, timeoutMs, limit, request, diagnostics);
159
232
  });
160
- let timer: ReturnType<typeof setTimeout> | undefined;
161
- let cancel: (() => void) | undefined;
233
+ }
234
+
235
+ async function executeCodexRequest(
236
+ options: CodexHostOptions,
237
+ timeoutMs: number,
238
+ limit: number | null,
239
+ request: Request,
240
+ diagnostics?: HostDiagnostics,
241
+ ): Promise<HostResult> {
242
+ await validateCodexRequest(options, request);
243
+ diagnosticStage(diagnostics, "state-allocation");
244
+ const stateRoot = await allocateCodexRequestState(request);
245
+ const helperRoot =
246
+ process.platform === "linux"
247
+ ? await mkdtemp(join(tmpdir(), "sevro-codex-helper-"))
248
+ : undefined;
249
+ let activeHooks: HookMounts | undefined;
162
250
  try {
163
- const written = (async () => {
164
- if (options.input === undefined) return;
165
- if (!proc.stdin || typeof proc.stdin === "number")
166
- throw new Error("Codex input pipe unavailable");
167
- await proc.stdin.write(options.input);
168
- await proc.stdin.end();
169
- })();
170
- const completed = Promise.all([
171
- boundedText(proc.stdout, MAX_EVENT_BYTES),
172
- proc.exited,
173
- written,
174
- ]).then(([out, code]) => ({ out, code }));
175
- const timeout = new Promise<never>((_resolve, reject) => {
176
- timer = setTimeout(
177
- () => reject(new Error("Codex run timed out")),
178
- options.timeoutMs,
179
- );
180
- });
181
- const aborted = new Promise<never>((_resolve, reject) => {
182
- if (!options.signal) return;
183
- cancel = () => reject(new Error("Codex run cancelled"));
184
- if (options.signal.aborted) cancel();
185
- else options.signal.addEventListener("abort", cancel, { once: true });
251
+ diagnosticStage(diagnostics, "state-preparation");
252
+ const state = await prepareCodexState(
253
+ stateRoot,
254
+ options,
255
+ limit,
256
+ request,
257
+ helperRoot,
258
+ );
259
+ const installedPluginRoots = await installCodexPlugins({
260
+ binary: options.binary,
261
+ env: state.env,
262
+ pluginCacheRoot: state.pluginCacheRoot,
263
+ request,
264
+ diagnostics,
186
265
  });
187
- return await Promise.race([completed, timeout, aborted]);
188
- } catch (error) {
189
- stopProcess(proc);
190
- await proc.exited;
191
- throw error;
266
+ diagnosticStage(diagnostics, "hooks-preparation");
267
+ const hooks = await codexHookMounts(request, state, installedPluginRoots);
268
+ activeHooks = hooks;
269
+ const skillTracking = await prepareCodexSkillTracking(
270
+ state,
271
+ request,
272
+ installedPluginRoots,
273
+ );
274
+ await codexPreflight(options, request, state, diagnostics);
275
+ const context: ExecutionContext = {
276
+ options,
277
+ request,
278
+ state,
279
+ timeoutMs,
280
+ hooks,
281
+ skillTracking,
282
+ diagnostics,
283
+ };
284
+ const turns = await new CodexTurnExecution(context).run();
285
+ if (turns.summary.complete)
286
+ diagnosticStage(diagnostics, "result-processing");
287
+ return await retainCodexResult(context, turns);
288
+ } catch (cause) {
289
+ return await failedNativeTranscripts(
290
+ cause,
291
+ request.runtimePolicy,
292
+ join(stateRoot, "codex-home", "sessions"),
293
+ "codex",
294
+ request.runtimeRole,
295
+ diagnostics,
296
+ );
192
297
  } finally {
193
- if (timer) clearTimeout(timer);
194
- if (cancel) options.signal?.removeEventListener("abort", cancel);
298
+ await cleanupNativeState(diagnostics, async () => {
299
+ await releaseRuntimeHooks(activeHooks);
300
+ if (!candidateCodexStateRetained(request.candidateRuntimeRoot, stateRoot))
301
+ await rm(stateRoot, { recursive: true, force: true });
302
+ if (helperRoot) await rm(helperRoot, { recursive: true, force: true });
303
+ });
195
304
  }
196
305
  }
197
306
 
198
- async function copyAuth(source: string, target: string): Promise<void> {
199
- let bytes: Buffer;
200
- try {
201
- if ((await stat(source)).size > MAX_AUTH_BYTES)
202
- throw new Error("Codex auth file exceeds the size limit");
203
- bytes = await readFile(source);
204
- } catch {
205
- throw new Error("Codex auth file is unreadable or oversized");
206
- }
207
- if (bytes.byteLength > MAX_AUTH_BYTES)
208
- throw new Error("Codex auth file exceeds the size limit");
209
- await writeFile(target, bytes, { flag: "wx", mode: 0o600 });
307
+ async function retainCodexResult(context: ExecutionContext, turns: TurnResult) {
308
+ const result = await codexHostResult(context, turns);
309
+ await retainReturnedHostFailure(
310
+ context.diagnostics,
311
+ result,
312
+ "Codex host execution did not complete",
313
+ );
314
+ await retainEnabledNativeTranscripts(
315
+ result,
316
+ context.request.runtimePolicy,
317
+ join(context.state.codexHome, "sessions"),
318
+ "codex",
319
+ context.request.runtimeRole,
320
+ turns.summary.threadId,
321
+ );
322
+ return result;
210
323
  }
211
324
 
212
- function inside(root: string, path: string): boolean {
213
- const child = relative(root, path);
214
- return (
215
- child === "" ||
216
- (child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child))
325
+ async function prepareCodexSkillTracking(
326
+ state: CodexState,
327
+ request: Request,
328
+ pluginRoots: string[],
329
+ ): Promise<SkillTracking> {
330
+ const roots = await codexSkillCatalogRoots(request.workspace, pluginRoots, [
331
+ { directory: join(state.codexHome, "skills"), boundary: state.codexHome },
332
+ {
333
+ directory: join(state.parentHome, ".agents/skills"),
334
+ boundary: state.parentHome,
335
+ },
336
+ {
337
+ directory: join(state.commandHome, ".agents/skills"),
338
+ boundary: state.commandHome,
339
+ },
340
+ {
341
+ directory: join(state.commandHome, ".codex/skills"),
342
+ boundary: state.commandHome,
343
+ },
344
+ ]);
345
+ const tracking = await prepareSkillTracking({
346
+ privateRoot: state.codexHome,
347
+ host: "codex",
348
+ roots,
349
+ });
350
+ await writeFile(
351
+ join(state.codexHome, "hooks.json"),
352
+ JSON.stringify({ hooks: tracking.hooks }),
353
+ {
354
+ flag: "wx",
355
+ mode: 0o600,
356
+ },
217
357
  );
358
+ return tracking;
218
359
  }
219
360
 
220
- async function marketplaceRoot(
221
- workspace: string,
222
- declaration: NonNullable<
223
- Parameters<HostAdapter["run"]>[0]["codexMarketplace"]
224
- >,
225
- ): Promise<string> {
226
- const { artifactRoot, marketplaceName, pluginNames } = declaration;
227
- const expectedPaths = new Set(declaration.artifactPaths);
361
+ async function validateCodexRequest(
362
+ options: CodexHostOptions,
363
+ request: Request,
364
+ ): Promise<void> {
365
+ requireCodexFollowUp(request);
366
+ await requireNativeTranscriptView(request.candidateTranscriptRoot);
367
+ await requireCandidateRuntime(
368
+ request.candidateRuntimeRoot,
369
+ request.runtimeRole,
370
+ );
371
+ await requireCandidateArtifactRoots(request.candidateArtifactRoots);
372
+ requireCodexFixtureBin(request);
373
+ requirePassiveCodexRequest(request);
374
+ requireCodexGoalRoute(request, options);
375
+ await verifyCodexInvocation(request);
376
+ await requireCodexFixtureConfiguration(request);
377
+ }
378
+
379
+ async function requireCodexFixtureConfiguration(
380
+ request: Request,
381
+ ): Promise<void> {
382
+ const path = join(request.workspace, ".codex");
383
+ if (!existsSync(path)) return;
384
+ if (request.runtimePolicy?.codexState !== true)
385
+ throw new Error("fixture Codex configuration is unsupported");
386
+ if (!(await lstat(path)).isDirectory())
387
+ throw new Error("fixture Codex configuration must be a physical directory");
388
+ }
389
+
390
+ function requireCodexGoalRoute(
391
+ request: Request,
392
+ options: CodexHostOptions,
393
+ ): void {
228
394
  if (
229
- !/^[a-z][a-z0-9-]*$/.test(marketplaceName) ||
230
- !pluginNames.length ||
231
- pluginNames.some((name) => !/^[a-z][a-z0-9-]*$/.test(name)) ||
232
- new Set(pluginNames).size !== pluginNames.length ||
233
- !declaration.artifactPaths.length ||
234
- expectedPaths.size !== declaration.artifactPaths.length ||
235
- declaration.artifactPaths.some(
236
- (path) =>
237
- !path.startsWith(`${artifactRoot}/`) ||
238
- fixtureParts(path).join("/") !== path,
239
- )
395
+ runtimeNativeGoalEnabled(request.runtimePolicy) &&
396
+ options.entrypoint !== "app-server"
240
397
  )
241
- throw new Error("invalid Codex marketplace declaration");
242
- const root = join(workspace, ...fixtureParts(artifactRoot));
243
- const actualWorkspace = await realpath(workspace);
244
- const actualRoot = await realpath(root);
245
- if (!inside(actualWorkspace, actualRoot) || actualRoot === actualWorkspace)
246
- throw new Error("Codex marketplace escapes the workspace");
247
- let manifest: unknown;
248
- try {
249
- manifest = JSON.parse(
250
- await readFile(
251
- join(actualRoot, ".claude-plugin", "marketplace.json"),
252
- "utf8",
398
+ throw new Error("native-goal runtime policy requires Codex app-server");
399
+ }
400
+ function sandboxArgs(
401
+ state: CodexState,
402
+ request: Request,
403
+ command: string[],
404
+ ): string[] {
405
+ return [
406
+ state.sandboxBinary,
407
+ "sandbox",
408
+ "-P",
409
+ state.profileId,
410
+ "-C",
411
+ request.workspace,
412
+ ...command,
413
+ ];
414
+ }
415
+ async function sandboxPreflight(
416
+ state: CodexState,
417
+ request: Request,
418
+ command: string[],
419
+ diagnostics?: HostDiagnostics,
420
+ ) {
421
+ return runCodexProcess({
422
+ diagnostics,
423
+ argv: sandboxArgs(state, request, command),
424
+ cwd: request.workspace,
425
+ env: state.env,
426
+ timeoutMs: 60_000,
427
+ phase: "isolation-preflight",
428
+ signal: request.signal,
429
+ });
430
+ }
431
+ async function codexPreflight(
432
+ options: CodexHostOptions,
433
+ request: Request,
434
+ state: CodexState,
435
+ diagnostics?: HostDiagnostics,
436
+ ): Promise<void> {
437
+ diagnosticStage(diagnostics, "isolation-preflight");
438
+ const probe = join(state.commandTemp, "isolation-probe");
439
+ await writeFile(probe, "probe\n", { flag: "wx", mode: 0o600 });
440
+ const checked = await sandboxPreflight(
441
+ state,
442
+ request,
443
+ [
444
+ "/bin/sh",
445
+ "-c",
446
+ '/bin/cat "$1" >/dev/null && ! /bin/ls "$2" >/dev/null 2>&1 && ! /bin/cat "$3" >/dev/null 2>&1 && ! /bin/cat "$4" >/dev/null 2>&1 || exit 70; "$5" --version >/dev/null 2>&1 || exit 71',
447
+ "sevro-probe",
448
+ probe,
449
+ options.projectRoot,
450
+ join(state.codexHome, "auth.json"),
451
+ // The opt-in config read is tested separately; credentials always stay denied.
452
+ join(
453
+ state.codexHome,
454
+ request.runtimePolicy?.codexState === true
455
+ ? "auth.json"
456
+ : "config.toml",
457
+ ),
458
+ state.sandboxBinary,
459
+ ],
460
+ diagnostics,
461
+ );
462
+ if (checked.code === 71)
463
+ throw new CodexProcessFailure("executable-preflight", checked);
464
+ if (checked.code !== 0)
465
+ throw new CodexProcessFailure("isolation-preflight", checked);
466
+ await rm(probe);
467
+ }
468
+
469
+ function routeArgs(context: ExecutionContext): string[] {
470
+ return [
471
+ "-m",
472
+ context.options.model,
473
+ "-c",
474
+ `model_reasoning_effort=${JSON.stringify(context.options.effort)}`,
475
+ "-c",
476
+ `default_permissions=${JSON.stringify(context.state.profileId)}`,
477
+ "-c",
478
+ 'approval_policy="never"',
479
+ "-c",
480
+ "features.hooks=true",
481
+ ];
482
+ }
483
+ function initialTurnArgs(context: ExecutionContext): string[] {
484
+ return [
485
+ context.options.binary,
486
+ "exec",
487
+ "--json",
488
+ "--strict-config",
489
+ "--skip-git-repo-check",
490
+ ...(context.request.followUpPrompt ||
491
+ context.request.runtimePolicy?.nativeTranscripts
492
+ ? []
493
+ : ["--ephemeral"]),
494
+ "--ignore-rules",
495
+ "--dangerously-bypass-hook-trust",
496
+ "-C",
497
+ context.request.workspace,
498
+ ...routeArgs(context),
499
+ "-",
500
+ ];
501
+ }
502
+ function followUpTurnArgs(
503
+ context: ExecutionContext,
504
+ threadId: string,
505
+ ): string[] {
506
+ return [
507
+ context.options.binary,
508
+ "exec",
509
+ "resume",
510
+ "--json",
511
+ "--strict-config",
512
+ "--skip-git-repo-check",
513
+ "--ignore-rules",
514
+ "--dangerously-bypass-hook-trust",
515
+ ...routeArgs(context),
516
+ threadId,
517
+ "-",
518
+ ];
519
+ }
520
+ async function codexProcessTurn(
521
+ context: ExecutionContext,
522
+ argv: string[],
523
+ input: string,
524
+ ): Promise<ProcessResult> {
525
+ diagnosticStage(context.diagnostics, "exec");
526
+ const result = await runCodexProcess({
527
+ diagnostics: context.diagnostics,
528
+ argv,
529
+ cwd: context.request.workspace,
530
+ env: context.state.env,
531
+ input,
532
+ timeoutMs: context.timeoutMs,
533
+ signal: context.request.signal,
534
+ });
535
+ return result;
536
+ }
537
+ async function initialFingerprint(request: Request): Promise<string | null> {
538
+ return request.followUpPrompt
539
+ ? workspaceFingerprint(request.workspace)
540
+ : null;
541
+ }
542
+ async function continuationBoundary(
543
+ context: ExecutionContext,
544
+ threadId: string,
545
+ originalFingerprint: string | null,
546
+ goal: Record<string, unknown> = {},
547
+ ): Promise<Observation> {
548
+ const boundary = await workspaceFingerprint(context.request.workspace);
549
+ const measured = originalFingerprint !== null && boundary !== null;
550
+ return {
551
+ id: "sevro.codex.continuation",
552
+ completeness: measured ? "complete" : "partial",
553
+ data: {
554
+ method: "same_thread_resume",
555
+ ...goal,
556
+ threadId,
557
+ nativeAfterOrdinal: await codexNativeSessionLastOrdinal(
558
+ context.state.codexHome,
559
+ threadId,
253
560
  ),
561
+ preFollowUpWorktreeUnchanged: measured
562
+ ? originalFingerprint === boundary
563
+ : null,
564
+ },
565
+ };
566
+ }
567
+ function requireInitialCompletion(
568
+ summary: CodexEventSummary,
569
+ appServer: AppServerResult | null,
570
+ ): void {
571
+ if (!summary.complete && !appServer)
572
+ throw new Error("Codex turn did not complete");
573
+ }
574
+ function requireFollowUpCompletion(
575
+ followUp: CodexEventSummary,
576
+ original: CodexEventSummary,
577
+ ): void {
578
+ if (!followUp.complete || followUp.threadId !== original.threadId)
579
+ throw new Error(
580
+ "Codex follow-up turn did not complete in the original thread",
581
+ );
582
+ }
583
+ async function appServerSummary(
584
+ result: AppServerResult,
585
+ workspace: string,
586
+ ): Promise<CodexEventSummary> {
587
+ return {
588
+ threadId: result.evidence.threadId ?? "",
589
+ complete: result.code === 0,
590
+ finalMessage:
591
+ result.code === 0
592
+ ? await readFile(join(workspace, ".git", "last-message.md"), "utf8")
593
+ : null,
594
+ inputTokens: null,
595
+ outputTokens: null,
596
+ usageComplete: false,
597
+ };
598
+ }
599
+ async function initialSummary(
600
+ appServer: AppServerResult | null,
601
+ execution: ProcessResult,
602
+ request: Request,
603
+ ): Promise<CodexEventSummary> {
604
+ return appServer
605
+ ? appServerSummary(appServer, request.workspace)
606
+ : summarizeCodexEvents(execution.out, execution.code);
607
+ }
608
+ class CodexTurnExecution {
609
+ private appServerContinuation?: Observation;
610
+ constructor(private context: ExecutionContext) {}
611
+ async run(): Promise<TurnResult> {
612
+ const fingerprint = await initialFingerprint(this.context.request);
613
+ const appServer = await this.maybeAppServer(fingerprint);
614
+ const execution =
615
+ appServer ??
616
+ (await codexProcessTurn(
617
+ this.context,
618
+ initialTurnArgs(this.context),
619
+ this.context.request.prompt,
620
+ ));
621
+ const summary = await initialSummary(
622
+ appServer,
623
+ execution,
624
+ this.context.request,
254
625
  );
255
- } catch {
256
- throw new Error("Codex marketplace manifest is unreadable");
626
+ requireInitialCompletion(summary, appServer);
627
+ const follow = await this.maybeFollowUp(summary, appServer, fingerprint);
628
+ return {
629
+ execution,
630
+ summary,
631
+ appServer,
632
+ ...follow,
633
+ };
257
634
  }
258
- if (!manifest || typeof manifest !== "object")
259
- throw new Error("invalid Codex marketplace manifest");
260
- const document = manifest as Record<string, unknown>;
261
- const plugins = document.plugins;
262
- if (
263
- document.name !== marketplaceName ||
264
- !Array.isArray(plugins) ||
265
- plugins.length !== pluginNames.length
266
- )
267
- throw new Error("Codex marketplace manifest does not match declaration");
268
- const found = new Set<string>();
269
- for (const entry of plugins) {
270
- if (!entry || typeof entry !== "object")
271
- throw new Error("invalid Codex marketplace plugin");
272
- const plugin = entry as Record<string, unknown>;
273
- if (
274
- typeof plugin.name !== "string" ||
275
- !pluginNames.includes(plugin.name) ||
276
- found.has(plugin.name) ||
277
- typeof plugin.source !== "string" ||
278
- !plugin.source.startsWith("./")
279
- )
280
- throw new Error(
281
- "Codex marketplace plugin is not a declared local source",
282
- );
283
- found.add(plugin.name);
284
- const source = join(actualRoot, ...fixtureParts(plugin.source.slice(2)));
285
- if (!inside(actualRoot, await realpath(source)))
286
- throw new Error("Codex marketplace plugin escapes its artifact root");
635
+
636
+ private async maybeAppServer(
637
+ fingerprint: string | null,
638
+ ): Promise<AppServerResult | null> {
639
+ const { options, request, state, timeoutMs } = this.context;
640
+ if (options.entrypoint !== "app-server") return null;
641
+ diagnosticStage(this.context.diagnostics, "app-server");
642
+ return runCodexAppServer({
643
+ diagnostics: this.context.diagnostics,
644
+ argv: [
645
+ options.binary,
646
+ "app-server",
647
+ "--stdio",
648
+ "--strict-config",
649
+ "-c",
650
+ `default_permissions=${JSON.stringify(state.profileId)}`,
651
+ "-c",
652
+ "features.hooks=true",
653
+ ],
654
+ env: state.env,
655
+ permissionProfile: state.profileId,
656
+ vettedHooks: true,
657
+ request: {
658
+ repoDir: request.workspace,
659
+ prompt: request.prompt,
660
+ model: options.model,
661
+ effort: options.effort,
662
+ signal: request.signal,
663
+ control: {
664
+ appServerTimeoutMs: timeoutMs,
665
+ ...(request.followUpPrompt
666
+ ? { followUpPrompt: request.followUpPrompt }
667
+ : {}),
668
+ },
669
+ },
670
+ followUpBoundary: async (threadId, goal) => {
671
+ this.appServerContinuation = await continuationBoundary(
672
+ this.context,
673
+ threadId,
674
+ fingerprint,
675
+ goal,
676
+ );
677
+ return JSON.stringify({
678
+ type: "sevro.codex.feedback-boundary",
679
+ ...this.appServerContinuation.data,
680
+ });
681
+ },
682
+ });
287
683
  }
288
- // The package is materialized from declared artifacts; links can otherwise
289
- // pull files from outside that snapshot during the local CLI installation.
290
- const pending = [actualRoot];
291
- const seen = new Set<string>();
292
- while (pending.length) {
293
- const directory = pending.pop()!;
294
- for (const entry of await readdir(directory, { withFileTypes: true })) {
295
- if (entry.isSymbolicLink())
296
- throw new Error("Codex marketplace package contains a symlink");
297
- if (entry.isDirectory()) pending.push(join(directory, entry.name));
298
- else if (!entry.isFile())
299
- throw new Error("Codex marketplace package contains a special file");
300
- else {
301
- const path = `${artifactRoot}/${relative(actualRoot, join(directory, entry.name)).split(sep).join("/")}`;
302
- if (!expectedPaths.has(path))
303
- throw new Error(
304
- "Codex marketplace package contains undeclared files",
305
- );
306
- seen.add(path);
307
- }
308
- }
684
+
685
+ private async maybeFollowUp(
686
+ summary: CodexEventSummary,
687
+ appServer: AppServerResult | null,
688
+ fingerprint: string | null,
689
+ ) {
690
+ const prompt = this.context.request.followUpPrompt;
691
+ if (!prompt || appServer)
692
+ return {
693
+ followUp: null,
694
+ followUpSummary: null,
695
+ continuation: this.appServerContinuation,
696
+ };
697
+ const continuation = await continuationBoundary(
698
+ this.context,
699
+ summary.threadId,
700
+ fingerprint,
701
+ );
702
+ const followUp = await codexProcessTurn(
703
+ this.context,
704
+ followUpTurnArgs(this.context, summary.threadId),
705
+ prompt,
706
+ );
707
+ const followUpSummary = summarizeCodexEvents(followUp.out, followUp.code);
708
+ requireFollowUpCompletion(followUpSummary, summary);
709
+ return { followUp, followUpSummary, continuation };
309
710
  }
310
- if (seen.size !== expectedPaths.size)
311
- throw new Error("Codex marketplace package is missing declared files");
312
- return actualRoot;
313
711
  }
314
712
 
315
- /** Construct one Codex route without inheriting user settings or credentials. */
316
- export function createCodexHost(options: CodexHostOptions): HostAdapter {
317
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
318
- const agentConcurrencyLimit = options.agentConcurrencyLimit ?? null;
319
- if (
320
- !isAbsolute(options.binary) ||
321
- !isAbsolute(options.authFile) ||
322
- !isAbsolute(options.projectRoot) ||
323
- !isAbsolute(options.resultsRoot) ||
324
- options.additionalProtectedRoots.some((root) => !isAbsolute(root)) ||
325
- (options.sandboxBinary !== undefined &&
326
- !isAbsolute(options.sandboxBinary)) ||
327
- !options.model ||
328
- !options.effort ||
329
- (options.entrypoint !== undefined &&
330
- !["exec", "app-server"].includes(options.entrypoint)) ||
331
- (agentConcurrencyLimit !== null &&
332
- (!Number.isSafeInteger(agentConcurrencyLimit) ||
333
- agentConcurrencyLimit < 1)) ||
334
- !Number.isSafeInteger(timeoutMs) ||
335
- timeoutMs < 1 ||
336
- timeoutMs > MAX_TIMEOUT_MS
337
- )
338
- throw new Error("invalid Codex host configuration");
713
+ function explicitInvocationReceipt(
714
+ selected: Request["explicitSkillInvocation"],
715
+ completed: boolean,
716
+ ) {
717
+ if (!selected) return null;
339
718
  return {
340
- id: "sevro.host.codex",
341
- hostCapabilities: [
342
- "sevro.host.continuation",
343
- ...(options.entrypoint === "app-server"
344
- ? ["sevro.host.native-goal"]
345
- : []),
346
- "sevro.codex.plugin-marketplace",
347
- "sevro.codex.explicit-invocation",
348
- "sevro.codex.repository-invocation",
349
- "sevro.codex.native-calls",
350
- "sevro.host.native-controls",
351
- "sevro.codex.initial-skill-reads",
352
- "sevro.codex.follow-up-skill-reads",
353
- ],
354
- model: options.model,
355
- effort: options.effort,
356
- configuration: {
357
- "sevro.codex.agent-concurrency-limit": agentConcurrencyLimit,
358
- ...(options.entrypoint === "app-server"
359
- ? { "sevro.codex.entrypoint": "app-server" }
360
- : {}),
719
+ id: "sevro.codex.explicit-invocation",
720
+ completeness: completed ? ("complete" as const) : ("partial" as const),
721
+ data: {
722
+ method: "explicit_invocation",
723
+ primarySkill: selected.skillName,
361
724
  },
362
- async run(request) {
363
- if (
364
- request.followUpPrompt !== undefined &&
365
- (typeof request.followUpPrompt !== "string" ||
366
- !request.followUpPrompt.trim())
367
- )
368
- throw new Error("Codex follow-up prompt must be nonempty");
369
- if (
370
- request.fixtureBinDir !== undefined &&
371
- request.fixtureBinDir !== join(request.workspace, ".git", "fixture-bin")
372
- )
373
- throw new Error("fixture binary path is outside the workspace");
374
- if (request.instrumentation?.length)
375
- throw new Error("Codex instrumentation is unavailable");
376
- if (request.condition !== "passive")
377
- throw new Error("Codex enforcement instrumentation is unavailable");
378
- await verifyCodexInvocation(request);
379
- if (existsSync(join(request.workspace, ".codex")))
380
- throw new Error("fixture Codex configuration is unsupported");
381
- const stateRoot = await mkdtemp(join(tmpdir(), "sevro-codex-state-"));
382
- try {
383
- const codexHome = join(stateRoot, "codex-home");
384
- const parentHome = join(stateRoot, "home");
385
- const parentTemp = join(stateRoot, "tmp");
386
- const commandHome = join(stateRoot, "command-home");
387
- const commandTemp = join(stateRoot, "command-tmp");
388
- const pluginCacheRoot = join(codexHome, "plugins", "cache");
389
- await Promise.all(
390
- [codexHome, parentHome, parentTemp, commandHome, commandTemp].map(
391
- (path) => mkdir(path, { mode: 0o700 }),
392
- ),
393
- );
394
- await copyAuth(options.authFile, join(codexHome, "auth.json"));
395
- const protectedRoots = await evaluationProtectedRoots({
396
- workspace: request.workspace,
397
- projectRoot: options.projectRoot,
398
- resultsRoot: options.resultsRoot,
399
- additionalRoots: [...options.additionalProtectedRoots, stateRoot],
400
- });
401
- const sandboxBinary = options.sandboxBinary ?? options.binary;
402
- const executableReadRoots = await Promise.all(
403
- [options.binary, sandboxBinary].flatMap((binary) => [
404
- realpath(dirname(binary)),
405
- realpath(binary).then(dirname),
406
- ]),
407
- );
408
- if (
409
- executableReadRoots.some((readRoot) =>
410
- protectedRoots.some(
411
- (protectedRoot) =>
412
- readRoot === protectedRoot ||
413
- readRoot.startsWith(`${protectedRoot}${sep}`),
414
- ),
415
- )
416
- )
417
- throw new Error("Codex executable resides inside a protected root");
418
- const profileId = `sevro_${randomUUID().replaceAll("-", "")}`;
419
- await writeFile(
420
- join(codexHome, "config.toml"),
421
- codexPermissionProfile({
422
- id: profileId,
423
- workspace: request.workspace,
424
- commandHome,
425
- commandTemp,
426
- executableReadRoots,
427
- ...(request.codexMarketplace
428
- ? { pluginReadRoot: pluginCacheRoot }
429
- : {}),
430
- protectedRoots,
431
- }) +
432
- (agentConcurrencyLimit === null
433
- ? ""
434
- : `\n[agents]\nmax_concurrent_threads_per_session = ${agentConcurrencyLimit}\n`),
435
- { flag: "wx", mode: 0o600 },
436
- );
437
- const shellRoot = join(request.workspace, ".git", "sevro-shell");
438
- if (request.fixtureBinDir) {
439
- await mkdir(shellRoot, { mode: 0o700 });
440
- await writeFile(
441
- join(shellRoot, ".zprofile"),
442
- `export PATH=${shellQuote(request.fixtureBinDir)}:"$PATH"\n`,
443
- { flag: "wx", mode: 0o600 },
444
- );
445
- }
446
- const env: Record<string, string> = {
447
- PATH: request.fixtureBinDir
448
- ? `${request.fixtureBinDir}${delimiter}${process.env.PATH ?? "/usr/bin:/bin"}`
449
- : (process.env.PATH ?? "/usr/bin:/bin"),
450
- LANG: process.env.LANG ?? "C",
451
- HOME: parentHome,
452
- TMPDIR: parentTemp,
453
- CODEX_HOME: codexHome,
454
- NO_COLOR: "1",
455
- ...(request.fixtureBinDir ? { ZDOTDIR: shellRoot } : {}),
456
- };
457
- const installedPluginRoots: string[] = [];
458
- if (request.codexMarketplace) {
459
- const root = await marketplaceRoot(
460
- request.workspace,
461
- request.codexMarketplace,
462
- );
463
- const added = await runProcess({
464
- argv: [
465
- options.binary,
466
- "plugin",
467
- "marketplace",
468
- "add",
469
- root,
470
- "--json",
471
- ],
472
- cwd: request.workspace,
473
- env,
474
- timeoutMs: 30_000,
475
- signal: request.signal,
476
- });
477
- if (added.code !== 0)
478
- throw new Error("Codex local marketplace installation failed");
479
- for (const pluginName of request.codexMarketplace.pluginNames) {
480
- const installed = await runProcess({
481
- argv: [
482
- options.binary,
483
- "plugin",
484
- "add",
485
- `${pluginName}@${request.codexMarketplace.marketplaceName}`,
486
- "--json",
487
- ],
488
- cwd: request.workspace,
489
- env,
490
- timeoutMs: 30_000,
491
- signal: request.signal,
492
- });
493
- if (installed.code !== 0)
494
- throw new Error("Codex local plugin installation failed");
495
- let receipt: unknown;
496
- try {
497
- receipt = JSON.parse(installed.out);
498
- } catch {
499
- throw new Error("Codex plugin installation receipt is invalid");
500
- }
501
- const entry = receipt as Record<string, unknown>;
502
- const actualInstalledPath =
503
- typeof entry?.installedPath === "string"
504
- ? await realpath(entry.installedPath)
505
- : null;
506
- if (
507
- !entry ||
508
- entry.name !== pluginName ||
509
- entry.marketplaceName !==
510
- request.codexMarketplace.marketplaceName ||
511
- !actualInstalledPath ||
512
- !inside(await realpath(pluginCacheRoot), actualInstalledPath)
513
- )
514
- throw new Error("Codex plugin installation receipt is invalid");
515
- installedPluginRoots.push(actualInstalledPath);
516
- if (
517
- request.explicitSkillInvocation?.pluginName === pluginName &&
518
- !(
519
- await stat(
520
- join(
521
- actualInstalledPath,
522
- "skills",
523
- request.explicitSkillInvocation.skillName,
524
- "SKILL.md",
525
- ),
526
- ).catch(() => null)
527
- )?.isFile()
528
- )
529
- throw new Error(
530
- "invoked Codex skill is absent from the installation",
531
- );
532
- }
533
- }
534
- const probe = join(commandTemp, "isolation-probe");
535
- await writeFile(probe, "probe\n", { flag: "wx", mode: 0o600 });
536
- const checked = await runProcess({
537
- argv: [
538
- sandboxBinary,
539
- "sandbox",
540
- "-P",
541
- profileId,
542
- "-C",
543
- request.workspace,
544
- "/bin/sh",
545
- "-c",
546
- '/bin/cat "$1" >/dev/null && ! /bin/ls "$2" >/dev/null 2>&1 && ! /bin/cat "$3" >/dev/null 2>&1',
547
- "sevro-probe",
548
- probe,
549
- options.projectRoot,
550
- join(codexHome, "auth.json"),
551
- ],
552
- cwd: request.workspace,
553
- env,
554
- timeoutMs: 10_000,
555
- signal: request.signal,
556
- });
557
- if (checked.code !== 0)
558
- throw new Error("Codex isolation preflight failed");
559
- const executableCheck = await runProcess({
560
- argv: [
561
- sandboxBinary,
562
- "sandbox",
563
- "-P",
564
- profileId,
565
- "-C",
566
- request.workspace,
567
- sandboxBinary,
568
- "--version",
569
- ],
570
- cwd: request.workspace,
571
- env,
572
- timeoutMs: 10_000,
573
- signal: request.signal,
574
- });
575
- if (executableCheck.code !== 0)
576
- throw new Error("Codex executable preflight failed");
577
- await rm(probe);
578
- const initialWorkspaceFingerprint = request.followUpPrompt
579
- ? await workspaceFingerprint(request.workspace)
580
- : null;
581
- const executionArgs = [
582
- options.binary,
583
- "exec",
584
- "--json",
585
- "--strict-config",
586
- "--skip-git-repo-check",
587
- ...(request.followUpPrompt ? [] : ["--ephemeral"]),
588
- "--ignore-rules",
589
- "-C",
590
- request.workspace,
591
- "-m",
592
- options.model,
593
- "-c",
594
- `model_reasoning_effort=${JSON.stringify(options.effort)}`,
595
- "-c",
596
- `default_permissions=${JSON.stringify(profileId)}`,
597
- "-c",
598
- 'approval_policy="never"',
599
- ];
600
- let appServerContinuation:
601
- | {
602
- id: string;
603
- completeness: "complete" | "partial";
604
- data: Record<string, unknown>;
605
- }
606
- | undefined;
607
- const appServer =
608
- options.entrypoint === "app-server"
609
- ? await runCodexAppServer({
610
- argv: [
611
- options.binary,
612
- "app-server",
613
- "--stdio",
614
- "--strict-config",
615
- "-c",
616
- `default_permissions=${JSON.stringify(profileId)}`,
617
- ],
618
- env,
619
- permissionProfile: profileId,
620
- request: {
621
- repoDir: request.workspace,
622
- prompt: request.prompt,
623
- model: options.model,
624
- effort: options.effort,
625
- signal: request.signal,
626
- control: {
627
- appServerTimeoutMs: timeoutMs,
628
- ...(request.followUpPrompt
629
- ? { followUpPrompt: request.followUpPrompt }
630
- : {}),
631
- },
632
- },
633
- followUpBoundary: async (threadId, goal) => {
634
- const boundary = await workspaceFingerprint(
635
- request.workspace,
636
- );
637
- const measured =
638
- initialWorkspaceFingerprint !== null && boundary !== null;
639
- appServerContinuation = {
640
- id: "sevro.codex.continuation",
641
- completeness: measured ? "complete" : "partial",
642
- data: {
643
- method: "same_thread_resume",
644
- ...goal,
645
- threadId,
646
- nativeAfterOrdinal: await codexNativeSessionLastOrdinal(
647
- codexHome,
648
- threadId,
649
- ),
650
- preFollowUpWorktreeUnchanged: measured
651
- ? initialWorkspaceFingerprint === boundary
652
- : null,
653
- },
654
- };
655
- return JSON.stringify({
656
- type: "sevro.codex.feedback-boundary",
657
- ...appServerContinuation.data,
658
- });
659
- },
660
- })
661
- : null;
662
- const execution =
663
- appServer ??
664
- (await runProcess({
665
- argv: [...executionArgs, "-"],
666
- cwd: request.workspace,
667
- env,
668
- input: request.prompt,
669
- timeoutMs,
670
- signal: request.signal,
671
- }));
672
- const summary = appServer
673
- ? {
674
- threadId: appServer.evidence.threadId ?? "",
675
- complete: appServer.code === 0,
676
- finalMessage:
677
- appServer.code === 0
678
- ? await readFile(
679
- join(request.workspace, ".git", "last-message.md"),
680
- "utf8",
681
- )
682
- : null,
683
- inputTokens: null,
684
- outputTokens: null,
685
- usageComplete: false,
686
- }
687
- : summarizeCodexEvents(execution.out, execution.code);
688
- if (!summary.complete && !appServer)
689
- throw new Error("Codex turn did not complete");
690
- let followUp: { out: string; code: number } | null = null;
691
- let followUpSummary: typeof summary | null = null;
692
- let continuationObservation:
693
- | {
694
- id: string;
695
- completeness: "complete" | "partial";
696
- data: Record<string, unknown>;
697
- }
698
- | undefined = appServerContinuation;
699
- if (request.followUpPrompt && !appServer) {
700
- const beforeFollowUp = await workspaceFingerprint(request.workspace);
701
- const nativeAfterOrdinal = await codexNativeSessionLastOrdinal(
702
- codexHome,
703
- summary.threadId,
704
- );
705
- const measured =
706
- initialWorkspaceFingerprint !== null && beforeFollowUp !== null;
707
- continuationObservation = {
708
- id: "sevro.codex.continuation",
709
- completeness: measured ? "complete" : "partial",
710
- data: {
711
- method: "same_thread_resume",
712
- threadId: summary.threadId,
713
- nativeAfterOrdinal,
714
- preFollowUpWorktreeUnchanged: measured
715
- ? initialWorkspaceFingerprint === beforeFollowUp
716
- : null,
717
- },
718
- };
719
- followUp = await runProcess({
720
- argv: [
721
- options.binary,
722
- "exec",
723
- "resume",
724
- "--json",
725
- "--strict-config",
726
- "--skip-git-repo-check",
727
- "--ignore-rules",
728
- "-m",
729
- options.model,
730
- "-c",
731
- `model_reasoning_effort=${JSON.stringify(options.effort)}`,
732
- "-c",
733
- `default_permissions=${JSON.stringify(profileId)}`,
734
- "-c",
735
- 'approval_policy="never"',
736
- summary.threadId,
737
- "-",
738
- ],
739
- cwd: request.workspace,
740
- env,
741
- input: request.followUpPrompt,
742
- timeoutMs,
743
- signal: request.signal,
744
- });
745
- followUpSummary = summarizeCodexEvents(followUp.out, followUp.code);
746
- if (
747
- !followUpSummary.complete ||
748
- followUpSummary.threadId !== summary.threadId
749
- )
750
- throw new Error(
751
- "Codex follow-up turn did not complete in the original thread",
752
- );
753
- }
754
- const observedEvents = followUp
755
- ? `${execution.out.trimEnd()}\n${followUp.out.trimStart()}`
756
- : execution.out;
757
- if (Buffer.byteLength(observedEvents, "utf8") > MAX_EVENT_BYTES)
758
- throw new Error("Codex combined event stream exceeds the size limit");
759
- const recoveredReads = await codexNativeSkillReadRecovery(
760
- codexHome,
761
- summary.threadId,
762
- );
763
- const skillReads = await codexSkillReadObservation(
764
- observedEvents,
765
- request.workspace,
766
- installedPluginRoots,
767
- recoveredReads,
768
- );
769
- const initialSkillReads = followUp
770
- ? {
771
- ...(await codexSkillReadObservation(
772
- execution.out,
773
- request.workspace,
774
- installedPluginRoots,
775
- recoveredReads,
776
- )),
777
- id: "sevro.codex.initial-skill-reads",
778
- }
779
- : null;
780
- const followUpSkillReads = followUp
781
- ? {
782
- ...(await codexSkillReadObservation(
783
- followUp.out,
784
- request.workspace,
785
- installedPluginRoots,
786
- recoveredReads,
787
- )),
788
- id: "sevro.codex.follow-up-skill-reads",
789
- }
790
- : null;
791
- const nativeCalls = await codexNativeCallObservation(
792
- codexHome,
793
- summary.threadId,
725
+ };
726
+ }
727
+ function appServerObservation(result: AppServerResult | null): Observation[] {
728
+ return result
729
+ ? [
730
+ {
731
+ id: "sevro.host.native-goal",
732
+ completeness: result.code === 0 ? "complete" : "partial",
733
+ data: { ...result.evidence },
734
+ },
735
+ ]
736
+ : [];
737
+ }
738
+ async function codexObservations(context: ExecutionContext, turns: TurnResult) {
739
+ const tracking = await skillTrackingObservation(
740
+ context.skillTracking,
741
+ turns.summary.threadId,
742
+ turns.summary.complete,
743
+ );
744
+ const native = await codexNativeCallObservation(
745
+ context.state.codexHome,
746
+ turns.summary.threadId,
747
+ { followUpPrompt: context.request.followUpPrompt },
748
+ );
749
+ const explicit = explicitInvocationReceipt(
750
+ context.request.explicitSkillInvocation,
751
+ turns.summary.complete,
752
+ );
753
+ return [
754
+ ...(await codexHookObservations(context)),
755
+ ...runtimeObservations(
756
+ context.request.runtimePolicy,
757
+ context.request.runtimeRole,
758
+ ),
759
+ ...appServerObservation(turns.appServer),
760
+ native,
761
+ codexNativeControls(native),
762
+ ...(turns.continuation ? [turns.continuation] : []),
763
+ ...(explicit ? [explicit] : []),
764
+ tracking,
765
+ ...skillActivationClaims(tracking),
766
+ ...skillExposureClaims(tracking),
767
+ ];
768
+ }
769
+
770
+ async function codexHookObservations(context: ExecutionContext) {
771
+ if (!context.request.runtimePolicy) return [];
772
+ return [await runtimeHookObservation(context.hooks)];
773
+ }
774
+ function codexArtifacts(turns: TurnResult) {
775
+ return [
776
+ {
777
+ id: "sevro.codex.events",
778
+ bytes: Buffer.from(turns.execution.out, "utf8"),
779
+ },
780
+ ...(turns.followUp
781
+ ? [
794
782
  {
795
- workspace: request.workspace,
796
- installedPluginRoots,
797
- ...(request.followUpPrompt
798
- ? { followUpPrompt: request.followUpPrompt }
799
- : {}),
783
+ id: "sevro.codex.follow-up-events",
784
+ bytes: Buffer.from(turns.followUp.out, "utf8"),
800
785
  },
801
- );
802
- const explicit = request.explicitSkillInvocation;
803
- const explicitReceipt = explicit
804
- ? {
805
- id: "sevro.codex.explicit-invocation",
806
- completeness: skillReads.completeness,
807
- data: {
808
- method: "explicit_invocation",
809
- primarySkill: explicit.skillName,
810
- observedSkills: [
811
- explicit.skillName,
812
- ...skillReads.data.observedSkills.filter(
813
- (skill) => skill !== explicit.skillName,
814
- ),
815
- ],
816
- },
817
- }
818
- : null;
819
- return {
820
- finalMessage: (followUpSummary ?? summary).finalMessage,
821
- ...(appServer ? { executionFailed: !summary.complete } : {}),
822
- complete: (followUpSummary ?? summary).finalMessage !== null,
823
- observations: [
824
- ...(appServer
825
- ? [
826
- {
827
- id: "sevro.host.native-goal",
828
- completeness:
829
- appServer.code === 0
830
- ? ("complete" as const)
831
- : ("partial" as const),
832
- data: { ...appServer.evidence },
833
- },
834
- ]
835
- : []),
836
- skillReads,
837
- ...(initialSkillReads ? [initialSkillReads] : []),
838
- ...(followUpSkillReads ? [followUpSkillReads] : []),
839
- nativeCalls,
840
- codexNativeControls(nativeCalls),
841
- ...(continuationObservation ? [continuationObservation] : []),
842
- ...(explicitReceipt ? [explicitReceipt] : []),
843
- ],
844
- artifacts: [
845
- {
846
- id: "sevro.codex.events",
847
- bytes: Buffer.from(execution.out, "utf8"),
848
- },
849
- ...(followUp
850
- ? [
851
- {
852
- id: "sevro.codex.follow-up-events",
853
- bytes: Buffer.from(followUp.out, "utf8"),
854
- },
855
- ]
856
- : []),
857
- ],
858
- actualCondition: "passive" as const,
859
- inputTokens: followUpSummary
860
- ? summary.inputTokens !== null &&
861
- followUpSummary.inputTokens !== null
862
- ? summary.inputTokens + followUpSummary.inputTokens
863
- : null
864
- : summary.inputTokens,
865
- outputTokens: followUpSummary
866
- ? summary.outputTokens !== null &&
867
- followUpSummary.outputTokens !== null
868
- ? summary.outputTokens + followUpSummary.outputTokens
869
- : null
870
- : summary.outputTokens,
871
- usageComplete:
872
- summary.usageComplete && (followUpSummary?.usageComplete ?? true),
873
- costUsd: null,
874
- };
875
- } finally {
876
- await rm(stateRoot, { recursive: true, force: true });
877
- }
878
- },
786
+ ]
787
+ : []),
788
+ ];
789
+ }
790
+ function tokenCount(
791
+ initial: number | null,
792
+ followUp: number | null | undefined,
793
+ ): number | null {
794
+ if (followUp === undefined) return initial;
795
+ return initial !== null && followUp !== null ? initial + followUp : null;
796
+ }
797
+ function codexUsageComplete(
798
+ summary: CodexEventSummary,
799
+ followUp: CodexEventSummary | null,
800
+ ): boolean {
801
+ return summary.usageComplete && (followUp?.usageComplete ?? true);
802
+ }
803
+ function codexUsage(turns: TurnResult) {
804
+ return {
805
+ inputTokens: tokenCount(
806
+ turns.summary.inputTokens,
807
+ turns.followUpSummary?.inputTokens,
808
+ ),
809
+ outputTokens: tokenCount(
810
+ turns.summary.outputTokens,
811
+ turns.followUpSummary?.outputTokens,
812
+ ),
813
+ usageComplete: codexUsageComplete(turns.summary, turns.followUpSummary),
814
+ costUsd: null,
815
+ };
816
+ }
817
+ async function codexHostResult(
818
+ context: ExecutionContext,
819
+ turns: TurnResult,
820
+ ): Promise<HostResult> {
821
+ const final = turns.followUpSummary ?? turns.summary;
822
+ return {
823
+ finalMessage: final.finalMessage,
824
+ ...(turns.appServer ? { executionFailed: !turns.summary.complete } : {}),
825
+ complete: final.finalMessage !== null,
826
+ observations: await codexObservations(context, turns),
827
+ artifacts: codexArtifacts(turns),
828
+ actualCondition: "passive",
829
+ ...codexUsage(turns),
879
830
  };
880
831
  }