@bjoernrochel/sevro 0.1.0-rc.7 → 1.0.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 (230) hide show
  1. package/CONTRIBUTING.md +22 -6
  2. package/README.md +5 -1
  3. package/docs/README.md +19 -15
  4. package/docs/architecture.md +138 -17
  5. package/docs/assets/README.md +5 -4
  6. package/docs/builtin-graders-v1.md +17 -3
  7. package/docs/codex-host.md +155 -49
  8. package/docs/contracts.md +2 -1
  9. package/docs/creating-cases.md +9 -2
  10. package/docs/development-cli.md +187 -19
  11. package/docs/documentation-quality.md +12 -2
  12. package/docs/evaluation-workflow.md +110 -0
  13. package/docs/extension-protocol-v1.md +26 -3
  14. package/docs/getting-started.md +23 -2
  15. package/docs/guide-evaluation.md +33 -1
  16. package/docs/identity-v1.md +25 -2
  17. package/docs/installing.md +11 -11
  18. package/docs/native-controls.md +76 -5
  19. package/docs/native-hosts.md +196 -7
  20. package/docs/reading-results.md +102 -0
  21. package/docs/recipes.md +489 -0
  22. package/docs/releases.md +31 -12
  23. package/docs/report-v1.md +29 -1
  24. package/docs/results-v1.md +141 -4
  25. package/docs/run-settings-v1.md +147 -0
  26. package/docs/runtime-v1.md +307 -18
  27. package/docs/troubleshooting.md +12 -12
  28. package/docs/typescript-quality.md +89 -26
  29. package/examples/recipes/candidate.ts +26 -0
  30. package/examples/recipes/cases/coding-change.json +21 -0
  31. package/examples/recipes/cases/host-tools.json +21 -0
  32. package/examples/recipes/cases/mise.json +21 -0
  33. package/examples/recipes/cases/node.json +21 -0
  34. package/examples/recipes/cases/python-uv.json +21 -0
  35. package/examples/recipes/cases/semantic.json +17 -0
  36. package/examples/recipes/coding-change/check.ts +23 -0
  37. package/examples/recipes/extension-artifacts.ts +65 -0
  38. package/examples/recipes/extension-methods.ts +174 -0
  39. package/examples/recipes/extension.ts +37 -0
  40. package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
  41. package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
  42. package/examples/recipes/repository-skill/README.md +3 -0
  43. package/examples/recipes/run-inputs.ts +188 -0
  44. package/examples/recipes/run-options.ts +131 -0
  45. package/examples/recipes/run.ts +63 -0
  46. package/examples/recipes/runtime/empty.json +1 -0
  47. package/examples/recipes/runtime/goals.json +1 -0
  48. package/examples/recipes/runtime/mise.json +6 -0
  49. package/examples/recipes/runtime/path.json +1 -0
  50. package/examples/recipes/runtime/plugins.json +4 -0
  51. package/examples/recipes/runtime/python-uv.json +10 -0
  52. package/examples/recipes/semantic-judge.ts +29 -0
  53. package/examples/templates/coding/adapter.ts +17 -0
  54. package/examples/templates/coding/case.json +15 -0
  55. package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
  56. package/examples/templates/repository-skill/extension-data.ts +64 -0
  57. package/examples/templates/repository-skill/extension.ts +31 -0
  58. package/examples/templates/response/adapter.ts +12 -0
  59. package/examples/templates/response/case.json +13 -0
  60. package/package.json +11 -5
  61. package/schemas/report-v1.schema.json +9 -0
  62. package/schemas/run-evidence-v1.schema.json +2 -1
  63. package/schemas/runtime-v1.schema.json +63 -7
  64. package/src/advisory.ts +2 -2
  65. package/src/candidate-artifacts.ts +206 -0
  66. package/src/candidate-codex-state.ts +185 -0
  67. package/src/case-validation.ts +73 -50
  68. package/src/cli/cancellation.ts +20 -0
  69. package/src/cli/command.ts +87 -0
  70. package/src/cli/completion.ts +15 -0
  71. package/src/cli/demo.ts +60 -0
  72. package/src/cli/evaluation.ts +55 -0
  73. package/src/cli/extension.ts +79 -0
  74. package/src/cli/help.ts +198 -0
  75. package/src/cli/hosts.ts +55 -0
  76. package/src/cli/init-settings.ts +30 -0
  77. package/src/cli/init.ts +111 -0
  78. package/src/{cli-invocation.ts → cli/invocation.ts} +214 -218
  79. package/src/cli/loading.ts +151 -0
  80. package/src/cli/preparation.ts +154 -0
  81. package/src/cli/presentation.ts +93 -0
  82. package/src/cli/progress.ts +6 -0
  83. package/src/cli/run-manifest.ts +128 -0
  84. package/src/cli/run-options.ts +403 -0
  85. package/src/cli/run-overrides.ts +69 -0
  86. package/src/cli/settings.ts +154 -0
  87. package/src/cli/summary.ts +43 -0
  88. package/src/cli.ts +13 -591
  89. package/src/contract-types.ts +94 -0
  90. package/src/engine.ts +13 -5
  91. package/src/evaluation/advisory.ts +150 -0
  92. package/src/{evaluation-check-plan.ts → evaluation/check-plan.ts} +103 -67
  93. package/src/evaluation/collection.ts +88 -0
  94. package/src/evaluation/context.ts +10 -0
  95. package/src/{evaluation-grading-host.ts → evaluation/grading-host.ts} +5 -5
  96. package/src/evaluation/grading.ts +235 -0
  97. package/src/{evaluation-invocation-plan.ts → evaluation/invocation-plan.ts} +147 -58
  98. package/src/{evaluation-plan.ts → evaluation/plan.ts} +208 -141
  99. package/src/evaluation/progress.ts +19 -0
  100. package/src/evaluation/resources.ts +241 -0
  101. package/src/evaluation/run.ts +206 -0
  102. package/src/{evaluation-semantic.ts → evaluation/semantic.ts} +133 -115
  103. package/src/evaluation/trial.ts +800 -0
  104. package/src/evaluation/types.ts +207 -0
  105. package/src/evidence/host-results.ts +85 -0
  106. package/src/evidence/retained-artifacts.ts +18 -0
  107. package/src/evidence/trial-writer.ts +203 -0
  108. package/src/evidence/usage.ts +17 -0
  109. package/src/extension-client.ts +34 -96
  110. package/src/extension-session.ts +24 -48
  111. package/src/{advisory-fixture.ts → fixtures/advisory.ts} +15 -76
  112. package/src/{fixture-bin.ts → fixtures/bin.ts} +1 -1
  113. package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
  114. package/src/fixtures/create.ts +183 -0
  115. package/src/{generated-fixture.ts → fixtures/generated.ts} +10 -53
  116. package/src/{git-hooks.ts → fixtures/git-hooks.ts} +1 -1
  117. package/src/{preparation.ts → fixtures/preparation.ts} +6 -16
  118. package/src/{repository-fixture.ts → fixtures/repository.ts} +30 -64
  119. package/src/{fixture-setup.ts → fixtures/setup.ts} +33 -51
  120. package/src/{fixture-tools.ts → fixtures/tools.ts} +1 -1
  121. package/src/fixtures/workspace-state.ts +212 -0
  122. package/src/generated/cli-result.d.cts +2 -1
  123. package/src/generated/extension.d.cts +2 -1
  124. package/src/generated/report.cjs +1 -1
  125. package/src/generated/report.d.cts +2 -1
  126. package/src/generated/run-evidence.cjs +1 -1
  127. package/src/generated/run-evidence.d.cts +2 -1
  128. package/src/generated/runtime.cjs +1 -1
  129. package/src/generated/runtime.d.cts +2 -1
  130. package/src/git.ts +93 -0
  131. package/src/graders/git-head.ts +29 -84
  132. package/src/graders/shell.ts +57 -68
  133. package/src/host-diagnostic-redaction.ts +52 -0
  134. package/src/host-diagnostics.ts +331 -0
  135. package/src/hosts/claude-continuation.ts +57 -40
  136. package/src/hosts/claude-credential.ts +160 -63
  137. package/src/hosts/claude-events.ts +34 -18
  138. package/src/hosts/claude-keychain.ts +78 -21
  139. package/src/hosts/claude-login-recovery.ts +85 -0
  140. package/src/hosts/claude-login-refresh.ts +178 -0
  141. package/src/hosts/claude-login-store.ts +162 -0
  142. package/src/hosts/claude-login.ts +107 -0
  143. package/src/hosts/claude-native-goal.ts +15 -12
  144. package/src/hosts/claude-process.ts +10 -50
  145. package/src/hosts/claude-refresh-lock.ts +81 -0
  146. package/src/hosts/claude-repository-invocation.ts +9 -34
  147. package/src/hosts/claude-settings.ts +91 -14
  148. package/src/hosts/claude-skill-activations.ts +169 -0
  149. package/src/hosts/claude-state.ts +46 -16
  150. package/src/hosts/claude-tool-calls.ts +13 -10
  151. package/src/hosts/claude.ts +122 -73
  152. package/src/hosts/codex-app-server.ts +78 -52
  153. package/src/hosts/codex-configuration.ts +5 -10
  154. package/src/hosts/codex-exec-controls.ts +516 -0
  155. package/src/hosts/codex-marketplace.ts +12 -10
  156. package/src/hosts/codex-native-calls.ts +15 -122
  157. package/src/hosts/codex-process.ts +10 -54
  158. package/src/hosts/codex-profile.ts +50 -21
  159. package/src/hosts/codex-session-files.ts +1 -9
  160. package/src/hosts/codex-state.ts +141 -19
  161. package/src/hosts/codex.ts +221 -179
  162. package/src/hosts/isolation-common.ts +15 -11
  163. package/src/hosts/isolation-roots.ts +13 -20
  164. package/src/hosts/linux-sandbox.ts +27 -3
  165. package/src/hosts/mac-sandbox.ts +111 -18
  166. package/src/hosts/native-controls.ts +72 -22
  167. package/src/hosts/native-request.ts +55 -0
  168. package/src/hosts/process-lifecycle.ts +221 -45
  169. package/src/hosts/protected-worktrees.ts +1 -1
  170. package/src/hosts/skill-hook-catalog.ts +131 -0
  171. package/src/hosts/skill-hook-handler.ts +374 -0
  172. package/src/hosts/skill-hooks.ts +577 -0
  173. package/src/instrumentation.ts +7 -8
  174. package/src/native-transcript-state.ts +22 -3
  175. package/src/native-transcript-view.ts +52 -13
  176. package/src/native-transcripts.ts +34 -15
  177. package/src/paths.ts +22 -0
  178. package/src/process-lifecycle.ts +150 -0
  179. package/src/provenance.ts +27 -7
  180. package/src/prune.ts +300 -0
  181. package/src/report.ts +63 -36
  182. package/src/resolved-case.ts +1 -1
  183. package/src/results-lock.ts +42 -0
  184. package/src/results.ts +14 -18
  185. package/src/run-owner.ts +4 -4
  186. package/src/{runtime-config.ts → runtime/config.ts} +182 -44
  187. package/src/{runtime-discovery-apple.ts → runtime/discovery-apple.ts} +3 -3
  188. package/src/{runtime-discovery-providers.ts → runtime/discovery-providers.ts} +3 -9
  189. package/src/{runtime-discovery.ts → runtime/discovery.ts} +4 -4
  190. package/src/{runtime-hook-processes.ts → runtime/hook-processes.ts} +1 -1
  191. package/src/{runtime-hooks.ts → runtime/hooks.ts} +33 -22
  192. package/src/{runtime-paths.ts → runtime/paths.ts} +6 -18
  193. package/src/{runtime-query.ts → runtime/query.ts} +5 -5
  194. package/src/{runtime-seeds.ts → runtime/seeds.ts} +3 -3
  195. package/src/runtime/shared-cache.ts +130 -0
  196. package/src/{runtime-state.ts → runtime/state.ts} +28 -11
  197. package/src/schema-types.ts +73 -161
  198. package/src/shell.ts +4 -0
  199. package/src/transcript-retention.ts +146 -0
  200. package/src/value-guards.ts +22 -0
  201. package/docs/assets/sevro-logo.png +0 -0
  202. package/docs/assets/sevro-symbol.svg +0 -1
  203. package/docs/assets/sevro-wordmark.svg +0 -1
  204. package/docs/cache-ownership-validation.md +0 -49
  205. package/docs/claude-continuation-validation.md +0 -47
  206. package/docs/concurrency-validation.md +0 -86
  207. package/docs/configuration-root-validation.md +0 -70
  208. package/docs/engine-slice.md +0 -114
  209. package/docs/evidence/runtime-startup-30/README.md +0 -185
  210. package/docs/evidence/runtime-startup-30/after.json +0 -319
  211. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +0 -2
  212. package/docs/evidence/runtime-startup-30/before.json +0 -403
  213. package/docs/evidence/runtime-startup-30/consumer-smoke.json +0 -63
  214. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +0 -3
  215. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +0 -3
  216. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +0 -20
  217. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +0 -2
  218. package/docs/evidence.md +0 -17
  219. package/docs/fixture-cleanup-validation.md +0 -70
  220. package/docs/guide-verification.json +0 -239
  221. package/src/evaluation-context.ts +0 -257
  222. package/src/evaluation-fixture.ts +0 -313
  223. package/src/evaluation-run.ts +0 -198
  224. package/src/evaluation-trial.ts +0 -946
  225. package/src/evaluation-types.ts +0 -149
  226. package/src/hosts/claude-nested-skills.ts +0 -272
  227. package/src/hosts/codex-command-output.ts +0 -510
  228. package/src/hosts/codex-executor-command.ts +0 -291
  229. package/src/hosts/codex-skill-reads.ts +0 -609
  230. /package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +0 -0
@@ -1,14 +1,12 @@
1
1
  import { isRecord, isUnknownArray } from "../value-guards";
2
- import { nativeCommandOutputs } from "./codex-command-output";
2
+ import {
3
+ codexExecCallReceipts,
4
+ type ExecCallReceipt,
5
+ } from "./codex-exec-controls";
3
6
  import {
4
7
  readNativeSession,
5
- readNativeSessionText,
6
8
  locateNativeSession as sessionPath,
7
9
  } from "./codex-session-files";
8
- import {
9
- codexNativeReadDiagnostic,
10
- type NativeReadDiagnostic,
11
- } from "./codex-skill-reads";
12
10
 
13
11
  const MAX_SESSION_ENTRIES = 10_000;
14
12
  const MAX_RETAINED_CALLS = 128;
@@ -63,7 +61,6 @@ interface ChildSession {
63
61
  threadId: string;
64
62
  status: "available" | "unavailable" | "ambiguous" | "partial";
65
63
  resultStatus?: "completed" | "unavailable";
66
- readDiagnostics?: NativeReadDiagnostic;
67
64
  nestedSpawns?: NestedSpawn[];
68
65
  requestsTruncated?: boolean;
69
66
  }
@@ -81,9 +78,7 @@ interface NestedSpawn {
81
78
  readerResultStatus: "completed" | "unavailable";
82
79
  }
83
80
 
84
- interface NativeSkillContext {
85
- workspace: string;
86
- installedPluginRoots: string[];
81
+ interface NativeCallContext {
87
82
  followUpPrompt?: string;
88
83
  }
89
84
 
@@ -95,9 +90,9 @@ interface NativeCallObservation {
95
90
  calls: NativeCall[];
96
91
  toolCalls: NativeToolCall[];
97
92
  submittedExecCalls: number;
93
+ execCalls?: ExecCallReceipt[];
98
94
  acceptedSpawns: AcceptedSpawn[];
99
95
  feedbackCalls: NativeFeedbackCall[];
100
- parentReadDiagnostics?: NativeReadDiagnostic;
101
96
  childSessions: ChildSession[];
102
97
  childrenTruncated: boolean;
103
98
  };
@@ -531,6 +526,9 @@ function sessionObservation(
531
526
  calls: state.calls,
532
527
  toolCalls: state.toolCalls,
533
528
  submittedExecCalls: state.submittedExecCalls,
529
+ ...(state.submittedExecCalls > 0
530
+ ? { execCalls: codexExecCallReceipts(state.entries) }
531
+ : {}),
534
532
  ...sessionReceipts(state, followUpPrompt),
535
533
  childSessions: [],
536
534
  childrenTruncated: false,
@@ -696,7 +694,6 @@ async function childSessionStatus(
696
694
  home: string,
697
695
  parentThread: string,
698
696
  threadId: string,
699
- skillContext?: NativeSkillContext,
700
697
  rootThread?: string,
701
698
  ): Promise<ChildSession> {
702
699
  if (threadId === parentThread) return { threadId, status: "partial" };
@@ -705,41 +702,21 @@ async function childSessionStatus(
705
702
  try {
706
703
  const text = await readNativeSession(located.path);
707
704
  if (text === null) return { threadId, status: "partial" };
708
- return await availableChildSession(
709
- home,
710
- threadId,
711
- text,
712
- skillContext,
713
- rootThread,
714
- );
705
+ return await availableChildSession(home, threadId, text, rootThread);
715
706
  } catch {
716
707
  return { threadId, status: "partial" };
717
708
  }
718
709
  }
719
710
 
720
- async function readDiagnostics(
721
- entries: NativeEntry[],
722
- context: NativeSkillContext | undefined,
723
- ) {
724
- return context
725
- ? codexNativeReadDiagnostic(
726
- entries,
727
- context.workspace,
728
- context.installedPluginRoots,
729
- )
730
- : undefined;
731
- }
732
711
  async function availableChildSession(
733
712
  home: string,
734
713
  threadId: string,
735
714
  text: string,
736
- context: NativeSkillContext | undefined,
737
715
  rootThread: string | undefined,
738
716
  ): Promise<ChildSession> {
739
717
  const parsed = parseSession(text);
740
718
  if (parsed.observation.completeness !== "complete")
741
719
  return { threadId, status: "partial" };
742
- const diagnostics = await readDiagnostics(parsed.entries, context);
743
720
  const nested = rootThread
744
721
  ? await nestedSpawnReceipts(home, rootThread, threadId, parsed.entries)
745
722
  : undefined;
@@ -749,7 +726,6 @@ async function availableChildSession(
749
726
  resultStatus: childResultCompleted(parsed.entries)
750
727
  ? "completed"
751
728
  : "unavailable",
752
- ...(diagnostics ? { readDiagnostics: diagnostics } : {}),
753
729
  ...nested,
754
730
  };
755
731
  }
@@ -779,7 +755,7 @@ function lastSessionOrdinal(text: string): number | null {
779
755
  export async function codexNativeCallObservation(
780
756
  home: string,
781
757
  threadId: string,
782
- skillContext?: NativeSkillContext,
758
+ context?: NativeCallContext,
783
759
  ): Promise<NativeCallObservation> {
784
760
  const located = await sessionPath(home, threadId);
785
761
  if (located.status !== "found")
@@ -791,7 +767,7 @@ export async function codexNativeCallObservation(
791
767
  home,
792
768
  threadId,
793
769
  located.path,
794
- skillContext,
770
+ context,
795
771
  );
796
772
  } catch {
797
773
  return unavailable("partial");
@@ -802,109 +778,26 @@ async function nativeSessionObservation(
802
778
  home: string,
803
779
  threadId: string,
804
780
  path: string,
805
- skillContext: NativeSkillContext | undefined,
781
+ context: NativeCallContext | undefined,
806
782
  ): Promise<NativeCallObservation> {
807
783
  const text = await readNativeSession(path);
808
784
  if (text === null) return unavailable("partial");
809
- const { observation, entries } = parseSession(
810
- text,
811
- skillContext?.followUpPrompt,
812
- );
785
+ const { observation } = parseSession(text, context?.followUpPrompt);
813
786
  if (observation.completeness !== "complete") return observation;
814
- const parentReadDiagnostics = await readDiagnostics(entries, skillContext);
815
787
  const children = [
816
788
  ...new Set(observation.data.acceptedSpawns.map((spawn) => spawn.threadId)),
817
789
  ];
818
790
  const childSessions = await Promise.all(
819
791
  children
820
792
  .slice(0, MAX_CHILD_SESSIONS)
821
- .map((child) =>
822
- childSessionStatus(home, threadId, child, skillContext, threadId),
823
- ),
793
+ .map((child) => childSessionStatus(home, threadId, child, threadId)),
824
794
  );
825
795
  return {
826
796
  ...observation,
827
797
  data: {
828
798
  ...observation.data,
829
- ...(parentReadDiagnostics ? { parentReadDiagnostics } : {}),
830
799
  childSessions,
831
800
  childrenTruncated: children.length > MAX_CHILD_SESSIONS,
832
801
  },
833
802
  };
834
803
  }
835
-
836
- /** Actor-local recovery stays in host memory and never enters public observations. */
837
- export async function codexNativeSkillReadRecovery(
838
- home: string,
839
- threadId: string,
840
- ): Promise<Map<string, { command: string; output: string }>> {
841
- const found = await sessionPath(home, threadId);
842
- if (found.status !== "found") return new Map();
843
- try {
844
- const text = await readNativeSessionText(found.path);
845
- if (text === null) return new Map();
846
- const parsed = parseSession(text);
847
- if (parsed.observation.completeness !== "complete") return new Map();
848
- return recoveredSessionReads(parsed.entries);
849
- } catch {
850
- return new Map();
851
- }
852
- }
853
-
854
- function completedCommandItem(
855
- payload: Record<string, unknown>,
856
- ): Record<string, unknown> | null {
857
- if (payload.type !== "item_completed") return null;
858
- if (!isRecord(payload.item) || payload.item.type !== "CommandExecution")
859
- return null;
860
- return payload.item;
861
- }
862
- function commandIdentityCounts(entries: NativeEntry[]): Map<string, number> {
863
- const counts = new Map<string, number>();
864
- for (const { payload } of entries) {
865
- const item = completedCommandItem(payload);
866
- if (typeof item?.id === "string")
867
- counts.set(item.id, (counts.get(item.id) ?? 0) + 1);
868
- }
869
- return counts;
870
- }
871
- function uniqueRecoveryItem(
872
- payload: Record<string, unknown>,
873
- counts: Map<string, number>,
874
- ) {
875
- const item = completedCommandItem(payload);
876
- if (!item || typeof item.id !== "string" || counts.get(item.id) !== 1)
877
- return null;
878
- return { id: item.id, command: item.command };
879
- }
880
- function recoveredLiteralCommand(command: unknown): string | null {
881
- if (!isUnknownArray(command) || command.length !== 3) return null;
882
- if (
883
- !["-c", "-lc"].includes(String(command[1])) ||
884
- typeof command[2] !== "string"
885
- )
886
- return null;
887
- return command[2];
888
- }
889
- function recoveredSessionReads(
890
- entries: NativeEntry[],
891
- ): Map<string, { command: string; output: string }> {
892
- const output = nativeCommandOutputs(entries);
893
- const counts = commandIdentityCounts(entries);
894
- const recovered = new Map<string, { command: string; output: string }>();
895
- for (const { ordinal, payload } of entries) {
896
- const item = uniqueRecoveryItem(payload, counts);
897
- if (!item) continue;
898
- const read = recoveredCommandRead(
899
- item.command,
900
- output.get(ordinal)?.output,
901
- );
902
- if (read) recovered.set(item.id, read);
903
- }
904
- return recovered;
905
- }
906
- function recoveredCommandRead(value: unknown, output: string | undefined) {
907
- if (output === undefined) return null;
908
- const command = recoveredLiteralCommand(value);
909
- return command === null ? null : { command, output };
910
- }
@@ -1,61 +1,17 @@
1
1
  import {
2
- awaitHostProcess,
3
- boundedProcessText,
4
- MAX_EVENT_BYTES,
2
+ HostProcessFailure,
3
+ runHostProcess,
4
+ type HostProcessOptions,
5
+ type HostProcessResult,
6
+ type Phase,
5
7
  } from "./process-lifecycle";
6
8
 
7
- function stopProcess(proc: Bun.Subprocess): void {
8
- if (process.platform !== "win32") {
9
- try {
10
- process.kill(-proc.pid, "SIGKILL");
11
- return;
12
- } catch {
13
- // The group may have exited before cancellation.
14
- }
15
- }
16
- try {
17
- proc.kill("SIGKILL");
18
- } catch {
19
- // The process already exited.
9
+ export class CodexProcessFailure extends HostProcessFailure {
10
+ constructor(phase: Phase, result: HostProcessResult) {
11
+ super("Codex", phase, result);
20
12
  }
21
13
  }
22
14
 
23
- export async function runCodexProcess(options: {
24
- argv: string[];
25
- cwd: string;
26
- env: Record<string, string>;
27
- input?: string;
28
- timeoutMs: number;
29
- signal?: AbortSignal;
30
- }): Promise<{ code: number; out: string }> {
31
- if (options.signal?.aborted) throw new Error("Codex run cancelled");
32
- const proc = Bun.spawn(options.argv, {
33
- cwd: options.cwd,
34
- env: options.env,
35
- detached: process.platform !== "win32",
36
- stdin: options.input === undefined ? "ignore" : "pipe",
37
- stdout: "pipe",
38
- stderr: "ignore",
39
- });
40
- const written = (async () => {
41
- if (options.input === undefined) return;
42
- if (!proc.stdin || typeof proc.stdin === "number")
43
- throw new Error("Codex input pipe unavailable");
44
- await proc.stdin.write(options.input);
45
- await proc.stdin.end();
46
- })();
47
- const completed = Promise.all([
48
- boundedProcessText(
49
- proc.stdout,
50
- MAX_EVENT_BYTES,
51
- "Codex event stream exceeds the size limit",
52
- ),
53
- proc.exited,
54
- written,
55
- ]).then(([out, code]) => ({ out, code }));
56
- return awaitHostProcess(proc, completed, {
57
- ...options,
58
- host: "Codex",
59
- stop: stopProcess,
60
- });
15
+ export function runCodexProcess(options: Omit<HostProcessOptions, "host">) {
16
+ return runHostProcess({ ...options, host: "Codex" });
61
17
  }
@@ -1,5 +1,8 @@
1
- import { isAbsolute, sep } from "node:path";
2
- import { insideRuntimeRoot, isRuntimeHomeRoot } from "../runtime-paths";
1
+ import { isAbsolute, join, sep } from "node:path";
2
+ import { minimalRoots as minimalProtectedRoots } from "../paths";
3
+ import { containsControl } from "../value-guards";
4
+ import { insideRuntimeRoot, isRuntimeHomeRoot } from "../runtime/paths";
5
+ import { workspaceBoundaryException } from "../fixtures/workspace-state";
3
6
  import {
4
7
  nativeStatePath,
5
8
  nativeStateProtectedRoots,
@@ -17,12 +20,7 @@ const LINUX_MINIMAL_READ_ROOTS = [
17
20
  ];
18
21
 
19
22
  function toml(value: string): string {
20
- if (
21
- Array.from(value).some(
22
- (character) =>
23
- character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f,
24
- )
25
- )
23
+ if (containsControl(value))
26
24
  throw new Error("Codex profile value contains a control character");
27
25
  return JSON.stringify(value);
28
26
  }
@@ -39,7 +37,9 @@ export function codexPermissionProfile(options: {
39
37
  commandEnvironment?: Record<string, string>;
40
38
  runtimeReadRoots?: string[];
41
39
  nativeReadRoots?: string[];
40
+ nativeWriteRoots?: string[];
42
41
  runtimeWriteRoot?: string;
42
+ localHttp?: boolean;
43
43
  }): string {
44
44
  if (!/^[a-z][a-z0-9_]*$/.test(options.id))
45
45
  throw new Error("invalid Codex permission profile ID");
@@ -50,8 +50,7 @@ export function codexPermissionProfile(options: {
50
50
  `default_permissions = ${toml(name)}`,
51
51
  'approval_policy = "never"',
52
52
  "allow_login_shell = false",
53
- "[features]",
54
- "hooks = false",
53
+ ...codexFeatureLines(options.localHttp),
55
54
  "",
56
55
  "[shell_environment_policy]",
57
56
  'inherit = "none"',
@@ -64,6 +63,7 @@ export function codexPermissionProfile(options: {
64
63
  ),
65
64
  `HOME = ${toml(options.commandHome)}`,
66
65
  `TMPDIR = ${toml(options.commandTemp)}`,
66
+ `TMPPREFIX = ${toml(join(options.commandTemp, sep))}`,
67
67
  "",
68
68
  `[permissions.${name}]`,
69
69
  'extends = ":workspace"',
@@ -78,30 +78,55 @@ export function codexPermissionProfile(options: {
78
78
  .sort()
79
79
  .map((root) => `${toml(root)} = "deny"`),
80
80
  ...readRootLines(options),
81
+ ...(options.nativeWriteRoots ?? []).map(
82
+ (root) => `${toml(root)} = "write"`,
83
+ ),
81
84
  `${toml(options.commandHome)} = "write"`,
82
85
  `${toml(options.commandTemp)} = "write"`,
83
86
  ...optionalRootLines(options.runtimeWriteRoot, "write"),
87
+ // Override Codex's metadata carveout only for this isolated fixture.
88
+ `${toml(join(options.workspace, ".git"))} = "write"`,
84
89
  "",
85
90
  `[permissions.${name}.filesystem.":workspace_roots"]`,
86
91
  '"." = "write"',
87
92
  "",
88
93
  `[permissions.${name}.network]`,
89
- "enabled = false",
94
+ ...networkProfile(options.localHttp),
90
95
  "",
91
96
  ].join("\n");
92
97
  }
93
98
 
99
+ function codexFeatureLines(localHttp: boolean | undefined): string[] {
100
+ return [
101
+ "[features]",
102
+ "hooks = false",
103
+ ...(localHttp ? ["network_proxy = true"] : []),
104
+ ];
105
+ }
106
+
107
+ function networkProfile(localHttp: boolean | undefined): string[] {
108
+ if (!localHttp) return ["enabled = false"];
109
+ return [
110
+ "enabled = true",
111
+ "allow_local_binding = true",
112
+ "allow_upstream_proxy = false",
113
+ 'domains = { "localhost" = "allow", "127.0.0.1" = "allow", "::1" = "allow" }',
114
+ ];
115
+ }
116
+
94
117
  function profileProtectedRoots(
95
118
  options: Parameters<typeof codexPermissionProfile>[0],
96
119
  ): string[] {
97
120
  const readRoots = profileReadRoots(options);
98
121
  const trustedRoots = [
99
122
  ...readRoots,
123
+ ...(options.nativeWriteRoots ?? []),
100
124
  ...(options.runtimeWriteRoot ? [options.runtimeWriteRoot] : []),
101
125
  ];
102
126
  const baselines = [
103
127
  ...LINUX_MINIMAL_READ_ROOTS,
104
128
  ...readRoots,
129
+ ...(options.nativeWriteRoots ?? []),
105
130
  options.workspace,
106
131
  options.commandHome,
107
132
  options.commandTemp,
@@ -111,7 +136,20 @@ function profileProtectedRoots(
111
136
  (root) =>
112
137
  (!implicitLinuxHomeBoundary(root, readRoots, baselines) ||
113
138
  process.platform !== "linux") &&
114
- !implicitNativeBoundary(root, trustedRoots, baselines),
139
+ !implicitNativeBoundary(root, trustedRoots, baselines) &&
140
+ !implicitWorkspaceBoundary(root, options.workspace, baselines),
141
+ );
142
+ }
143
+
144
+ function implicitWorkspaceBoundary(
145
+ root: string,
146
+ workspace: string,
147
+ baselines: string[],
148
+ ): boolean {
149
+ return (
150
+ process.platform === "linux" &&
151
+ workspaceBoundaryException(root, workspace).length > 0 &&
152
+ !hasBroaderBaseline(root, root, baselines)
115
153
  );
116
154
  }
117
155
 
@@ -178,15 +216,6 @@ function implicitLinuxHomeBoundary(
178
216
  );
179
217
  }
180
218
 
181
- function minimalProtectedRoots(roots: string[]): string[] {
182
- return [...new Set(roots)].filter(
183
- (root) =>
184
- !roots.some(
185
- (other) => other !== root && root.startsWith(`${other}${sep}`),
186
- ),
187
- );
188
- }
189
-
190
219
  function commandEnvironmentLines(
191
220
  environment: Record<string, string> = {},
192
221
  executableReadRoots: string[],
@@ -1,7 +1,7 @@
1
1
  import type { Dirent } from "node:fs";
2
2
  import { lstat, readFile, readdir, stat } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { isMissingFile } from "../fixture-tools";
4
+ import { isMissingFile } from "../fixtures/tools";
5
5
 
6
6
  const MAX_SESSION_BYTES = 8 * 1024 * 1024;
7
7
  const MAX_SESSION_FILES = 1024;
@@ -104,11 +104,3 @@ export async function readNativeSession(path: string): Promise<string | null> {
104
104
  if (bytes.byteLength > MAX_SESSION_BYTES) return null;
105
105
  return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
106
106
  }
107
-
108
- export async function readNativeSessionText(
109
- path: string,
110
- ): Promise<string | null> {
111
- if ((await stat(path)).size > MAX_SESSION_BYTES) return null;
112
- const text = await readFile(path, "utf8");
113
- return Buffer.byteLength(text) > MAX_SESSION_BYTES ? null : text;
114
- }