@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.
- package/CONTRIBUTING.md +170 -0
- package/LICENSE +63 -136
- package/README.md +83 -66
- package/docs/README.md +44 -0
- package/docs/acknowledgements.md +14 -0
- package/docs/architecture.md +146 -0
- package/docs/assets/README.md +27 -0
- package/docs/assets/sevro-logo.png +0 -0
- package/docs/assets/sevro-symbol.svg +1 -0
- package/docs/assets/sevro-wordmark.svg +1 -0
- package/docs/builtin-graders-v1.md +41 -13
- package/docs/codex-host.md +184 -52
- package/docs/contracts.md +19 -0
- package/docs/creating-cases.md +139 -0
- package/docs/development-cli.md +190 -30
- package/docs/documentation-quality.md +61 -0
- package/docs/evaluation-workflow.md +110 -0
- package/docs/evidence/code-mode-controls-71.md +64 -0
- package/docs/evidence/code-mode-programs-85.md +64 -0
- package/docs/evidence/codex-state-73/README.md +204 -0
- package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
- package/docs/evidence/codex-state-73/results.json +661 -0
- package/docs/evidence/local-http-74.md +104 -0
- package/docs/evidence/runtime-startup-30/README.md +185 -0
- package/docs/evidence/runtime-startup-30/after.json +319 -0
- package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
- package/docs/evidence/runtime-startup-30/before.json +403 -0
- package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
- package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
- package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
- package/docs/evidence/skill-reads-70/results.json +823 -0
- package/docs/evidence.md +21 -0
- package/docs/extension-protocol-v1.md +50 -16
- package/docs/fixture-cleanup-validation.md +3 -0
- package/docs/getting-started.md +102 -0
- package/docs/guide-evaluation.md +89 -0
- package/docs/guide-verification.json +239 -0
- package/docs/identity-v1.md +35 -1
- package/docs/installing.md +88 -0
- package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
- package/docs/licensing.md +42 -0
- package/docs/native-controls.md +76 -5
- package/docs/native-hosts.md +228 -0
- package/docs/reading-results.md +141 -0
- package/docs/recipes.md +489 -0
- package/docs/releases.md +81 -40
- package/docs/report-v1.md +29 -1
- package/docs/results-v1.md +180 -0
- package/docs/run-settings-v1.md +147 -0
- package/docs/running-evaluations.md +31 -0
- package/docs/runtime-v1.md +614 -0
- package/docs/specs/repository-guide.md +58 -0
- package/docs/troubleshooting.md +30 -0
- package/docs/typescript-quality.md +234 -0
- package/docs/usability-study.md +114 -0
- package/examples/basic/host.ts +2 -2
- package/examples/recipes/candidate.ts +26 -0
- package/examples/recipes/cases/coding-change.json +21 -0
- package/examples/recipes/cases/host-tools.json +21 -0
- package/examples/recipes/cases/mise.json +21 -0
- package/examples/recipes/cases/node.json +21 -0
- package/examples/recipes/cases/python-uv.json +21 -0
- package/examples/recipes/cases/semantic.json +17 -0
- package/examples/recipes/coding-change/check.ts +23 -0
- package/examples/recipes/extension-artifacts.ts +65 -0
- package/examples/recipes/extension-methods.ts +174 -0
- package/examples/recipes/extension.ts +37 -0
- package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/README.md +3 -0
- package/examples/recipes/run-inputs.ts +188 -0
- package/examples/recipes/run-options.ts +131 -0
- package/examples/recipes/run.ts +63 -0
- package/examples/recipes/runtime/empty.json +1 -0
- package/examples/recipes/runtime/goals.json +1 -0
- package/examples/recipes/runtime/mise.json +6 -0
- package/examples/recipes/runtime/path.json +1 -0
- package/examples/recipes/runtime/plugins.json +4 -0
- package/examples/recipes/runtime/python-uv.json +10 -0
- package/examples/recipes/semantic-judge.ts +29 -0
- package/examples/templates/coding/adapter.ts +17 -0
- package/examples/templates/coding/case.json +15 -0
- package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
- package/examples/templates/repository-skill/extension-data.ts +64 -0
- package/examples/templates/repository-skill/extension.ts +31 -0
- package/examples/templates/response/adapter.ts +12 -0
- package/examples/templates/response/case.json +13 -0
- package/package.json +39 -6
- package/schemas/report-v1.schema.json +9 -0
- package/schemas/run-evidence-v1.schema.json +2 -1
- package/schemas/runtime-v1.schema.json +193 -0
- package/src/advisory.ts +10 -5
- package/src/candidate-artifacts.ts +208 -0
- package/src/candidate-codex-state.ts +185 -0
- package/src/case-validation.ts +105 -0
- package/src/cli/cancellation.ts +20 -0
- package/src/cli/command.ts +81 -0
- package/src/cli/completion.ts +15 -0
- package/src/cli/demo.ts +59 -0
- package/src/cli/evaluation.ts +55 -0
- package/src/cli/extension.ts +79 -0
- package/src/cli/help.ts +222 -0
- package/src/cli/hosts.ts +59 -0
- package/src/cli/init-settings.ts +30 -0
- package/src/cli/init.ts +111 -0
- package/src/cli/invocation.ts +627 -0
- package/src/cli/loading.ts +151 -0
- package/src/cli/preparation.ts +163 -0
- package/src/cli/presentation.ts +91 -0
- package/src/cli/progress.ts +6 -0
- package/src/cli/run-manifest.ts +133 -0
- package/src/cli/run-options.ts +101 -0
- package/src/cli/run-overrides.ts +83 -0
- package/src/cli/settings.ts +160 -0
- package/src/cli/summary.ts +43 -0
- package/src/cli.ts +13 -843
- package/src/contract-types.ts +94 -0
- package/src/engine.ts +19 -2373
- package/src/evaluation/advisory.ts +150 -0
- package/src/evaluation/check-plan.ts +510 -0
- package/src/evaluation/collection.ts +88 -0
- package/src/evaluation/context.ts +10 -0
- package/src/evaluation/grading-host.ts +39 -0
- package/src/evaluation/grading.ts +258 -0
- package/src/evaluation/invocation-plan.ts +548 -0
- package/src/evaluation/plan.ts +736 -0
- package/src/evaluation/progress.ts +19 -0
- package/src/evaluation/resources.ts +241 -0
- package/src/evaluation/run.ts +206 -0
- package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
- package/src/evaluation/semantic.ts +312 -0
- package/src/evaluation/trial.ts +866 -0
- package/src/evaluation/types.ts +207 -0
- package/src/evidence/host-results.ts +92 -0
- package/src/evidence/retained-artifacts.ts +18 -0
- package/src/evidence/trial-writer.ts +203 -0
- package/src/evidence/usage.ts +17 -0
- package/src/extension-client.ts +228 -148
- package/src/extension-session.ts +326 -195
- package/src/fixtures/advisory.ts +338 -0
- package/src/fixtures/bin.ts +57 -0
- package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
- package/src/fixtures/create.ts +187 -0
- package/src/fixtures/generated.ts +300 -0
- package/src/fixtures/git-hooks.ts +24 -0
- package/src/fixtures/preparation.ts +270 -0
- package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
- package/src/fixtures/setup.ts +224 -0
- package/src/fixtures/tools.ts +88 -0
- package/src/fixtures/workspace-state.ts +212 -0
- package/src/generated/cli-result.cjs +2 -0
- package/src/generated/cli-result.d.cts +3 -0
- package/src/generated/extension.cjs +2 -0
- package/src/generated/extension.d.cts +3 -0
- package/src/generated/report.cjs +2 -0
- package/src/generated/report.d.cts +3 -0
- package/src/generated/run-evidence.cjs +2 -0
- package/src/generated/run-evidence.d.cts +3 -0
- package/src/generated/runtime.cjs +2 -0
- package/src/generated/runtime.d.cts +3 -0
- package/src/graders/artifact.ts +56 -23
- package/src/graders/git-head.ts +79 -75
- package/src/graders/output.ts +222 -140
- package/src/graders/semantic.ts +102 -43
- package/src/graders/shell.ts +379 -207
- package/src/host-diagnostic-redaction.ts +52 -0
- package/src/host-diagnostics.ts +319 -0
- package/src/hosts/claude-continuation.ts +63 -18
- package/src/hosts/claude-credential.ts +43 -62
- package/src/hosts/claude-events.ts +86 -51
- package/src/hosts/claude-keychain.ts +59 -0
- package/src/hosts/claude-native-goal.ts +57 -33
- package/src/hosts/claude-process.ts +111 -0
- package/src/hosts/claude-repository-invocation.ts +208 -117
- package/src/hosts/claude-settings.ts +240 -21
- package/src/hosts/claude-skill-activations.ts +166 -0
- package/src/hosts/claude-state.ts +403 -0
- package/src/hosts/claude-tool-calls.ts +171 -106
- package/src/hosts/claude.ts +561 -451
- package/src/hosts/codex-app-server.ts +326 -178
- package/src/hosts/codex-configuration.ts +44 -18
- package/src/hosts/codex-events.ts +97 -60
- package/src/hosts/codex-exec-controls.ts +516 -0
- package/src/hosts/codex-invocation.ts +70 -0
- package/src/hosts/codex-marketplace.ts +283 -0
- package/src/hosts/codex-native-calls.ts +546 -500
- package/src/hosts/codex-process.ts +202 -0
- package/src/hosts/codex-profile.ts +257 -28
- package/src/hosts/codex-session-files.ts +106 -0
- package/src/hosts/codex-state.ts +492 -0
- package/src/hosts/codex.ts +783 -832
- package/src/hosts/isolation-common.ts +94 -0
- package/src/hosts/isolation-roots.ts +54 -27
- package/src/hosts/isolation.ts +10 -0
- package/src/hosts/linux-sandbox.ts +159 -0
- package/src/hosts/mac-sandbox.ts +187 -61
- package/src/hosts/native-controls.ts +152 -62
- package/src/hosts/process-lifecycle.ts +37 -0
- package/src/hosts/protected-worktrees.ts +51 -19
- package/src/hosts/skill-hook-catalog.ts +131 -0
- package/src/hosts/skill-hook-handler.ts +374 -0
- package/src/hosts/skill-hooks.ts +580 -0
- package/src/hosts/workspace-fingerprint.ts +55 -35
- package/src/identity.ts +102 -48
- package/src/instrumentation.ts +109 -48
- package/src/native-transcript-state.ts +131 -0
- package/src/native-transcript-view.ts +90 -0
- package/src/native-transcripts.ts +286 -0
- package/src/process-lifecycle.ts +150 -0
- package/src/provenance.ts +161 -81
- package/src/prune.ts +215 -0
- package/src/report.ts +172 -140
- package/src/resolved-case.ts +33 -0
- package/src/results-lock.ts +29 -0
- package/src/results.ts +133 -86
- package/src/run-owner.ts +154 -63
- package/src/runtime/config.ts +456 -0
- package/src/runtime/discovery-apple.ts +90 -0
- package/src/runtime/discovery-providers.ts +63 -0
- package/src/runtime/discovery.ts +70 -0
- package/src/runtime/hook-processes.ts +97 -0
- package/src/runtime/hooks.ts +476 -0
- package/src/runtime/paths.ts +313 -0
- package/src/runtime/query.ts +85 -0
- package/src/runtime/seeds.ts +134 -0
- package/src/runtime/shared-cache.ts +143 -0
- package/src/runtime/state.ts +143 -0
- package/src/schema-types.ts +198 -0
- package/src/schema.ts +15 -19
- package/src/transcript-retention.ts +146 -0
- package/src/value-guards.ts +20 -0
- package/src/advisory-fixture.ts +0 -282
- package/src/fixture-bin.ts +0 -87
- package/src/fixture-setup.ts +0 -163
- package/src/generated-fixture.ts +0 -220
- package/src/git-hooks.ts +0 -63
- package/src/hosts/claude-nested-skills.ts +0 -218
- package/src/hosts/codex-command-output.ts +0 -399
- package/src/hosts/codex-executor-command.ts +0 -195
- package/src/hosts/codex-skill-reads.ts +0 -391
- 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
|
+
}
|