@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
@@ -0,0 +1,516 @@
1
+ import { createRequire } from "node:module";
2
+ import type ts from "typescript";
3
+ import { isRecord, isUnknownArray } from "../value-guards";
4
+
5
+ const require = createRequire(import.meta.url);
6
+ let parser: typeof ts | undefined;
7
+ function typescript(): typeof ts {
8
+ return (parser ??= require("typescript") as typeof ts);
9
+ }
10
+
11
+ interface Entry {
12
+ ordinal: number;
13
+ payload: Record<string, unknown>;
14
+ }
15
+ interface SubmittedCall {
16
+ index: number;
17
+ namespace: "functions" | "collaboration" | "other";
18
+ name: string;
19
+ }
20
+ export interface ExecCallReceipt {
21
+ ordinal: number;
22
+ completeness: "complete" | "partial";
23
+ calls: SubmittedCall[];
24
+ inspectionReason?:
25
+ | "unsupported_source"
26
+ | "malformed_source"
27
+ | "source_limit"
28
+ | "foreign_namespace"
29
+ | null;
30
+ execution?: "completed" | "failed" | "incomplete";
31
+ output?: "complete" | "truncated" | "unavailable";
32
+ }
33
+
34
+ function toolNamespace(name: string): SubmittedCall["namespace"] {
35
+ if (
36
+ [
37
+ "spawn_agent",
38
+ "list_agents",
39
+ "wait_agent",
40
+ "followup_task",
41
+ "send_message",
42
+ "interrupt_agent",
43
+ ].includes(name)
44
+ )
45
+ return "collaboration";
46
+ if (
47
+ [
48
+ "create_goal",
49
+ "get_goal",
50
+ "update_goal",
51
+ "exec_command",
52
+ "write_stdin",
53
+ ].includes(name)
54
+ )
55
+ return "functions";
56
+ return "other";
57
+ }
58
+ function toolName(node: ts.Expression): string | undefined {
59
+ const ts = typescript();
60
+ if (!ts.isPropertyAccessExpression(node)) return undefined;
61
+ if (!ts.isIdentifier(node.expression) || node.expression.text !== "tools")
62
+ return undefined;
63
+ return /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(node.name.text)
64
+ ? node.name.text
65
+ : undefined;
66
+ }
67
+ function literal(node: ts.Expression): boolean {
68
+ const ts = typescript();
69
+ if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) return true;
70
+ return [
71
+ ts.SyntaxKind.TrueKeyword,
72
+ ts.SyntaxKind.FalseKeyword,
73
+ ts.SyntaxKind.NullKeyword,
74
+ ].includes(node.kind);
75
+ }
76
+ function member(node: ts.Expression, owner: string, names: string[]): boolean {
77
+ const ts = typescript();
78
+ return (
79
+ ts.isPropertyAccessExpression(node) &&
80
+ ts.isIdentifier(node.expression) &&
81
+ node.expression.text === owner &&
82
+ names.includes(node.name.text)
83
+ );
84
+ }
85
+ function localName(node: ts.BindingName): string | undefined {
86
+ const ts = typescript();
87
+ if (!ts.isIdentifier(node)) return undefined;
88
+ return [
89
+ "tools",
90
+ "text",
91
+ "store",
92
+ "Promise",
93
+ "load",
94
+ "JSON",
95
+ "String",
96
+ ].includes(node.text)
97
+ ? undefined
98
+ : node.text;
99
+ }
100
+ class Submission {
101
+ readonly calls: SubmittedCall[] = [];
102
+ private readonly locals = new Set<string>();
103
+ private readonly collections = new Set<string>();
104
+ private readonly indices = new Set<string>();
105
+ private nodes = 0;
106
+ expression(node: ts.Expression): boolean {
107
+ const ts = typescript();
108
+ if (++this.nodes > 2048) return false;
109
+ switch (true) {
110
+ case literal(node):
111
+ return true;
112
+ case ts.isTemplateExpression(node):
113
+ return node.templateSpans.every((span) =>
114
+ this.expression(span.expression),
115
+ );
116
+ case ts.isBinaryExpression(node):
117
+ return this.composition(node);
118
+ case ts.isAwaitExpression(node):
119
+ return this.expression(node.expression);
120
+ case ts.isObjectLiteralExpression(node):
121
+ return node.properties.every((property) => this.property(property));
122
+ case ts.isArrayLiteralExpression(node):
123
+ return node.elements.every((element) => this.expression(element));
124
+ case ts.isIdentifier(node):
125
+ return this.locals.has(node.text);
126
+ case ts.isPropertyAccessExpression(node):
127
+ return this.localProperty(node);
128
+ case ts.isCallExpression(node):
129
+ return this.call(node);
130
+ case ts.isConditionalExpression(node):
131
+ return [node.condition, node.whenTrue, node.whenFalse].every((part) =>
132
+ this.outputExpression(part),
133
+ );
134
+ case ts.isElementAccessExpression(node):
135
+ return (
136
+ this.collection(node.expression) &&
137
+ typescript().isIdentifier(node.argumentExpression) &&
138
+ this.indices.has(node.argumentExpression.text)
139
+ );
140
+ default:
141
+ return false;
142
+ }
143
+ }
144
+ private property(node: ts.ObjectLiteralElementLike): boolean {
145
+ const ts = typescript();
146
+ if (ts.isSpreadAssignment(node))
147
+ return this.outputExpression(node.expression);
148
+ if (ts.isShorthandPropertyAssignment(node)) return this.shorthand(node);
149
+ if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name))
150
+ return false;
151
+ return this.expression(node.initializer);
152
+ }
153
+ private shorthand(node: ts.ShorthandPropertyAssignment): boolean {
154
+ return !node.objectAssignmentInitializer && this.expression(node.name);
155
+ }
156
+ private composition(node: ts.BinaryExpression): boolean {
157
+ if (
158
+ [
159
+ typescript().SyntaxKind.EqualsEqualsEqualsToken,
160
+ typescript().SyntaxKind.ExclamationEqualsEqualsToken,
161
+ ].includes(node.operatorToken.kind)
162
+ )
163
+ return (
164
+ this.outputExpression(node.left) && this.outputExpression(node.right)
165
+ );
166
+ return (
167
+ node.operatorToken.kind === typescript().SyntaxKind.PlusToken &&
168
+ this.expression(node.left) &&
169
+ this.expression(node.right)
170
+ );
171
+ }
172
+ private call(node: ts.CallExpression): boolean {
173
+ if (this.outputEach(node)) return true;
174
+ if (!node.arguments.every((argument) => this.expression(argument)))
175
+ return false;
176
+ return helperCall(node.expression) || this.retainToolCall(node.expression);
177
+ }
178
+ private retainToolCall(expression: ts.Expression): boolean {
179
+ const name = toolName(expression);
180
+ if (!name || this.calls.length >= 128) return false;
181
+ this.calls.push({
182
+ index: this.calls.length,
183
+ namespace: toolNamespace(name),
184
+ name,
185
+ });
186
+ return name !== "exec";
187
+ }
188
+ statement(node: ts.Statement): boolean {
189
+ const ts = typescript();
190
+ if (ts.isVariableStatement(node))
191
+ return this.declarations(node.declarationList);
192
+ if (ts.isForOfStatement(node)) return this.outputLoop(node);
193
+ if (ts.isForStatement(node)) return this.numericOutputLoop(node);
194
+ return ts.isExpressionStatement(node) && this.expression(node.expression);
195
+ }
196
+ private localProperty(node: ts.PropertyAccessExpression): boolean {
197
+ return (
198
+ this.expression(node.expression) &&
199
+ !["constructor", "__proto__", "prototype"].includes(node.name.text)
200
+ );
201
+ }
202
+ private declarations(list: ts.VariableDeclarationList): boolean {
203
+ const ts = typescript();
204
+ if (!(list.flags & ts.NodeFlags.Const)) return false;
205
+ return list.declarations.every((declaration) =>
206
+ this.declaration(declaration),
207
+ );
208
+ }
209
+ private declaration(node: ts.VariableDeclaration): boolean {
210
+ if (!node.initializer) return false;
211
+ const names = this.bindingNames(node.name, node.initializer);
212
+ if (!names) return false;
213
+ if (!this.expression(node.initializer)) return false;
214
+ names.forEach((name) => this.locals.add(name));
215
+ this.retainCollection(node.name, node.initializer, names);
216
+ return true;
217
+ }
218
+ private retainCollection(
219
+ name: ts.BindingName,
220
+ initializer: ts.Expression,
221
+ names: string[],
222
+ ): void {
223
+ if (localName(name) && collectionExpression(initializer))
224
+ names.forEach((name) => this.collections.add(name));
225
+ }
226
+ private bindingNames(
227
+ node: ts.BindingName,
228
+ initializer: ts.Expression,
229
+ ): string[] | undefined {
230
+ const ts = typescript();
231
+ if (ts.isIdentifier(node)) {
232
+ const name = this.declaredName(node);
233
+ return name ? [name] : undefined;
234
+ }
235
+ if (!ts.isArrayBindingPattern(node) || !collectionExpression(initializer))
236
+ return undefined;
237
+ return this.batchNames(node);
238
+ }
239
+ private batchNames(node: ts.ArrayBindingPattern): string[] | undefined {
240
+ const names = node.elements.map((element) => this.batchBinding(element));
241
+ if (!names.every((name): name is string => name !== undefined))
242
+ return undefined;
243
+ return new Set(names).size === names.length ? names : undefined;
244
+ }
245
+ private batchBinding(node: ts.ArrayBindingElement): string | undefined {
246
+ const ts = typescript();
247
+ if (!ts.isBindingElement(node)) return undefined;
248
+ if (node.dotDotDotToken || node.initializer || node.propertyName)
249
+ return undefined;
250
+ return this.declaredName(node.name);
251
+ }
252
+ private declaredName(node: ts.BindingName): string | undefined {
253
+ const name = localName(node);
254
+ return name && !this.locals.has(name) ? name : undefined;
255
+ }
256
+ private outputEach(node: ts.CallExpression): boolean {
257
+ const ts = typescript();
258
+ const [callback] = node.arguments;
259
+ if (
260
+ node.arguments.length !== 1 ||
261
+ !callback ||
262
+ !ts.isIdentifier(callback) ||
263
+ callback.text !== "text"
264
+ )
265
+ return false;
266
+ return [...this.collections].some((name) =>
267
+ member(node.expression, name, ["forEach"]),
268
+ );
269
+ }
270
+ private outputLoop(node: ts.ForOfStatement): boolean {
271
+ const ts = typescript();
272
+ if (!this.collection(node.expression)) return false;
273
+ if (!ts.isVariableDeclarationList(node.initializer)) return false;
274
+ const name = loopVariable(node.initializer);
275
+ if (!name || this.locals.has(name)) return false;
276
+ this.locals.add(name);
277
+ const valid = this.outputBody(node.statement);
278
+ this.locals.delete(name);
279
+ return valid;
280
+ }
281
+ private outputExpression(node: ts.Expression): boolean {
282
+ const count = this.calls.length;
283
+ return this.expression(node) && this.calls.length === count;
284
+ }
285
+ private numericOutputLoop(node: ts.ForStatement): boolean {
286
+ const name = numericLoopVariable(node);
287
+ if (!name || this.locals.has(name)) return false;
288
+ const condition = node.condition as ts.BinaryExpression;
289
+ const bound = condition.right as ts.PropertyAccessExpression;
290
+ if (!this.collection(bound.expression)) return false;
291
+ this.locals.add(name);
292
+ this.indices.add(name);
293
+ const valid = this.outputBody(node.statement);
294
+ this.locals.delete(name);
295
+ this.indices.delete(name);
296
+ return valid;
297
+ }
298
+ private outputBody(node: ts.Statement): boolean {
299
+ const ts = typescript();
300
+ if (ts.isBlock(node))
301
+ return node.statements.every((statement) => this.outputBody(statement));
302
+ if (ts.isIfStatement(node)) return this.outputConditional(node);
303
+ return (
304
+ ts.isExpressionStatement(node) && this.outputExpression(node.expression)
305
+ );
306
+ }
307
+ private outputConditional(node: ts.IfStatement): boolean {
308
+ return (
309
+ this.outputExpression(node.expression) &&
310
+ this.outputBody(node.thenStatement) &&
311
+ (!node.elseStatement || this.outputBody(node.elseStatement))
312
+ );
313
+ }
314
+ private collection(node: ts.Expression): boolean {
315
+ const ts = typescript();
316
+ return ts.isIdentifier(node) && this.collections.has(node.text);
317
+ }
318
+ }
319
+ function helperCall(node: ts.Expression): boolean {
320
+ const ts = typescript();
321
+ if (ts.isIdentifier(node) && ["text", "store", "String"].includes(node.text))
322
+ return true;
323
+ return (
324
+ member(node, "Promise", ["all", "allSettled"]) ||
325
+ member(node, "JSON", ["stringify"])
326
+ );
327
+ }
328
+ function namedIdentifier(
329
+ node: ts.Expression | undefined,
330
+ name: string,
331
+ ): boolean {
332
+ return (
333
+ node !== undefined && typescript().isIdentifier(node) && node.text === name
334
+ );
335
+ }
336
+ function collectionExpression(node: ts.Expression): boolean {
337
+ const ts = typescript();
338
+ if (!ts.isAwaitExpression(node) || !ts.isCallExpression(node.expression))
339
+ return false;
340
+ return member(node.expression.expression, "Promise", ["all", "allSettled"]);
341
+ }
342
+ function loopVariable(list: ts.VariableDeclarationList): string | undefined {
343
+ const ts = typescript();
344
+ if (!(list.flags & ts.NodeFlags.Const) || list.declarations.length !== 1)
345
+ return undefined;
346
+ const declaration = list.declarations[0];
347
+ return declaration && !declaration.initializer
348
+ ? localName(declaration.name)
349
+ : undefined;
350
+ }
351
+ function numericLoopVariable(node: ts.ForStatement): string | undefined {
352
+ const ts = typescript();
353
+ if (!node.initializer || !ts.isVariableDeclarationList(node.initializer))
354
+ return undefined;
355
+ const name = numericBinding(node.initializer);
356
+ return name && numericLoopBounds(node, name) ? name : undefined;
357
+ }
358
+ function numericBinding(list: ts.VariableDeclarationList): string | undefined {
359
+ if (
360
+ !(list.flags & typescript().NodeFlags.Let) ||
361
+ list.declarations.length !== 1
362
+ )
363
+ return undefined;
364
+ const declaration = list.declarations[0];
365
+ return declaration && zeroInitializer(declaration.initializer)
366
+ ? localName(declaration.name)
367
+ : undefined;
368
+ }
369
+ function zeroInitializer(node: ts.Expression | undefined): boolean {
370
+ return (
371
+ node !== undefined &&
372
+ typescript().isNumericLiteral(node) &&
373
+ node.text === "0"
374
+ );
375
+ }
376
+ function numericLoopBounds(node: ts.ForStatement, name: string): boolean {
377
+ const ts = typescript();
378
+ const condition = node.condition;
379
+ if (!condition || !ts.isBinaryExpression(condition)) return false;
380
+ return (
381
+ numericCondition(condition, name) &&
382
+ numericIncrement(node.incrementor, name)
383
+ );
384
+ }
385
+ function numericCondition(node: ts.BinaryExpression, name: string): boolean {
386
+ return (
387
+ node.operatorToken.kind === typescript().SyntaxKind.LessThanToken &&
388
+ namedIdentifier(node.left, name) &&
389
+ lengthProperty(node.right)
390
+ );
391
+ }
392
+ function lengthProperty(node: ts.Expression): boolean {
393
+ return (
394
+ typescript().isPropertyAccessExpression(node) && node.name.text === "length"
395
+ );
396
+ }
397
+ function numericIncrement(
398
+ node: ts.Expression | undefined,
399
+ name: string,
400
+ ): boolean {
401
+ return (
402
+ node !== undefined &&
403
+ typescript().isPostfixUnaryExpression(node) &&
404
+ node.operator === typescript().SyntaxKind.PlusPlusToken &&
405
+ namedIdentifier(node.operand, name)
406
+ );
407
+ }
408
+ function inspectedSource(code: unknown): {
409
+ submission?: Submission;
410
+ reason: ExecCallReceipt["inspectionReason"];
411
+ } {
412
+ if (typeof code !== "string") return { reason: "unsupported_source" };
413
+ if (code.length > 65536) return { reason: "source_limit" };
414
+ const ts = typescript();
415
+ const source = ts.createSourceFile(
416
+ "submitted.js",
417
+ code,
418
+ ts.ScriptTarget.Latest,
419
+ false,
420
+ ts.ScriptKind.JS,
421
+ );
422
+ const diagnostics = (
423
+ source as ts.SourceFile & { parseDiagnostics: readonly ts.Diagnostic[] }
424
+ ).parseDiagnostics;
425
+ if (diagnostics.length !== 0) return { reason: "malformed_source" };
426
+ const submission = new Submission();
427
+ return source.statements.every((statement) => submission.statement(statement))
428
+ ? { submission, reason: null }
429
+ : { reason: "unsupported_source" };
430
+ }
431
+ function outputBlocks(output: unknown): unknown[] {
432
+ if (isUnknownArray(output)) return output;
433
+ if (typeof output !== "string") return [];
434
+ try {
435
+ const blocks: unknown = JSON.parse(output);
436
+ return isUnknownArray(blocks) ? blocks : [];
437
+ } catch {
438
+ return [];
439
+ }
440
+ }
441
+ function completeTextBlock(block: unknown): boolean {
442
+ if (!isRecord(block) || !["text", "input_text"].includes(String(block.type)))
443
+ return false;
444
+ return typeof block.text === "string";
445
+ }
446
+ function completedHeader(block: unknown): boolean {
447
+ return (
448
+ isRecord(block) &&
449
+ typeof block.text === "string" &&
450
+ /^Script completed\nWall time [0-9]+(?:\.[0-9]+)? seconds\nOutput:\n$/.test(
451
+ block.text,
452
+ )
453
+ );
454
+ }
455
+ function submissionOutput(entry: Entry, entries: Entry[]): unknown[] {
456
+ const id = entry.payload.call_id;
457
+ if (typeof id !== "string") return [];
458
+ const matches = entries.filter((other) => other.payload.call_id === id);
459
+ if (matches.length !== 2) return [];
460
+ const result = matches.find(
461
+ (other) => other.payload.type === "custom_tool_call_output",
462
+ );
463
+ return result && result.ordinal > entry.ordinal
464
+ ? outputBlocks(result.payload.output)
465
+ : [];
466
+ }
467
+ function outputEvidence(
468
+ blocks: unknown[],
469
+ ): Pick<ExecCallReceipt, "execution" | "output"> {
470
+ if (!blocks.length || !blocks.every(completeTextBlock))
471
+ return { execution: "incomplete", output: "unavailable" };
472
+ const header = blocks[0] as { text: string };
473
+ const execution = executionStatus(header);
474
+ const truncated = blocks.some((block) =>
475
+ (block as { text: string }).text.includes("Warning: truncated output"),
476
+ );
477
+ return { execution, output: truncated ? "truncated" : "complete" };
478
+ }
479
+ function executionStatus(header: {
480
+ text: string;
481
+ }): ExecCallReceipt["execution"] {
482
+ if (completedHeader(header)) return "completed";
483
+ return /^Script failed\b/.test(header.text) ? "failed" : "incomplete";
484
+ }
485
+ function nativeExecNamespace(namespace: unknown): boolean {
486
+ return namespace === undefined || namespace === "functions";
487
+ }
488
+ /** Static inspection only: submitted code is never executed by the observer. */
489
+ export function codexExecCallReceipts(entries: Entry[]): ExecCallReceipt[] {
490
+ return entries
491
+ .filter(
492
+ ({ payload }) =>
493
+ payload.type === "custom_tool_call" && payload.name === "exec",
494
+ )
495
+ .map((entry) => ({
496
+ ordinal: entry.ordinal,
497
+ ...inspectionEvidence(entry),
498
+ ...outputEvidence(submissionOutput(entry, entries)),
499
+ }));
500
+ }
501
+ function inspectionEvidence(
502
+ entry: Entry,
503
+ ): Pick<ExecCallReceipt, "completeness" | "calls" | "inspectionReason"> {
504
+ if (!nativeExecNamespace(entry.payload.namespace))
505
+ return {
506
+ completeness: "partial",
507
+ calls: [],
508
+ inspectionReason: "foreign_namespace",
509
+ };
510
+ const inspected = inspectedSource(entry.payload.input);
511
+ return {
512
+ completeness: inspected.submission ? "complete" : "partial",
513
+ calls: inspected.submission?.calls ?? [],
514
+ inspectionReason: inspected.reason,
515
+ };
516
+ }
@@ -0,0 +1,70 @@
1
+ import type { Stats } from "node:fs";
2
+ import { lstat } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import type { HostAdapter } from "../engine";
5
+ type Request = Parameters<HostAdapter["run"]>[0];
6
+ type Invocation = NonNullable<Request["explicitSkillInvocation"]>;
7
+
8
+ function invocationTokenCount(request: Request, token: string): number {
9
+ return (
10
+ request.prompt.split(token).length -
11
+ 1 +
12
+ (request.followUpPrompt?.split(token).length ?? 1) -
13
+ 1
14
+ );
15
+ }
16
+ function validSkillName(name: string): boolean {
17
+ return /^[A-Za-z0-9._-]+$/.test(name) && ![".", ".."].includes(name);
18
+ }
19
+ export async function verifyCodexInvocation(request: Request): Promise<void> {
20
+ const selected = request.explicitSkillInvocation;
21
+ if (!selected) return;
22
+ if (
23
+ !validSkillName(selected.skillName) ||
24
+ invocationTokenCount(request, selected.token) !== 1
25
+ )
26
+ throw new Error("invalid Codex explicit skill invocation");
27
+ if (selected.scope === "repository") {
28
+ await verifyRepositorySkill(request, selected);
29
+ return;
30
+ }
31
+ requirePluginInvocation(request, selected);
32
+ }
33
+ async function verifyRepositorySkill(
34
+ request: Request,
35
+ selected: Invocation,
36
+ ): Promise<void> {
37
+ if (selected.token !== `$${selected.skillName}`)
38
+ throw new Error("invalid Codex explicit skill invocation");
39
+ let path = request.workspace;
40
+ for (const [index, part] of [
41
+ ".agents",
42
+ "skills",
43
+ selected.skillName,
44
+ "SKILL.md",
45
+ ].entries()) {
46
+ path = join(path, part);
47
+ const entry = await lstat(path).catch(() => null);
48
+ if (!safeRepositoryEntry(entry, index))
49
+ throw new Error(
50
+ "invoked Codex repository skill is unavailable or unsafe",
51
+ );
52
+ }
53
+ }
54
+ function safeRepositoryEntry(entry: Stats | null, index: number): boolean {
55
+ if (!entry || entry.isSymbolicLink()) return false;
56
+ return index === 3
57
+ ? entry.isFile() && entry.size <= 1024 * 1024
58
+ : entry.isDirectory();
59
+ }
60
+ function requirePluginInvocation(
61
+ request: Request,
62
+ selected: Invocation & { scope?: "plugin" },
63
+ ): void {
64
+ if (
65
+ !request.codexMarketplace?.pluginNames.includes(selected.pluginName) ||
66
+ !/^[a-z][a-z0-9-]*$/.test(selected.pluginName) ||
67
+ selected.token !== `$${selected.pluginName}:${selected.skillName}`
68
+ )
69
+ throw new Error("invalid Codex explicit skill invocation");
70
+ }