@bjoernrochel/sevro 0.1.0-rc.15 → 0.1.0-rc.17
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/docs/extension-protocol-v1.md +10 -2
- package/docs/installing.md +2 -2
- package/docs/native-controls.md +47 -12
- package/docs/runtime-v1.md +28 -4
- package/package.json +1 -1
- package/src/candidate-artifacts.ts +11 -1
- package/src/evaluation/resources.ts +4 -1
- package/src/evaluation/trial.ts +35 -19
- package/src/fixtures/create.ts +10 -1
- package/src/fixtures/setup.ts +12 -2
- package/src/hosts/codex-exec-controls.ts +195 -55
- package/src/hosts/native-controls.ts +24 -2
|
@@ -106,8 +106,16 @@ trial in the Git fixture after source history and working-tree files are built,
|
|
|
106
106
|
before extension artifacts are mounted or the host starts. The command is never
|
|
107
107
|
interpreted through a shell by Sevro; an extension may explicitly select a
|
|
108
108
|
shell executable. `{{sevro.project}}` and `{{sevro.workspace}}` in environment
|
|
109
|
-
values resolve to the declared roots at execution time.
|
|
110
|
-
|
|
109
|
+
values resolve to the declared roots at execution time. Setup's `HOME` is the
|
|
110
|
+
candidate commands' private trial home, outside the Git fixture. Ordinary
|
|
111
|
+
`$HOME` paths can initialize directories and seed prior state before the candidate
|
|
112
|
+
starts, including without a runtime configuration file. This home is separate
|
|
113
|
+
from host authentication and native configuration, other trials, and grader homes.
|
|
114
|
+
Setup's `TMPDIR` remains disposable and is removed when setup finishes; the shared
|
|
115
|
+
home survives through candidate execution and grading, then is removed even on
|
|
116
|
+
setup failure, cancellation, or dry runs. Trusted injected hosts must use the
|
|
117
|
+
supplied `candidateRuntimeRoot` and runtime policy to preserve this boundary.
|
|
118
|
+
The symbolic values and command enter fixture and comparison identity, while retained configuration
|
|
111
119
|
records only their digest. Setup has a two-minute limit, inherits no credential
|
|
112
120
|
environment variables, and its output is discarded. Failure or interruption
|
|
113
121
|
stops the run before host execution; it cannot become a passing assessment.
|
package/docs/installing.md
CHANGED
|
@@ -23,10 +23,10 @@ Local installation records the selected version in the project lockfile.
|
|
|
23
23
|
|
|
24
24
|
## Current release
|
|
25
25
|
|
|
26
|
-
The current release is `0.1.0-rc.
|
|
26
|
+
The current release is `0.1.0-rc.17`. Install this exact version:
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
|
-
bun add --exact @bjoernrochel/sevro@0.1.0-rc.
|
|
29
|
+
bun add --exact @bjoernrochel/sevro@0.1.0-rc.17
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
<!-- sevro-current-release:end -->
|
package/docs/native-controls.md
CHANGED
|
@@ -5,7 +5,8 @@ retain an observation with that ID. It exposes host facts for extension policy:
|
|
|
5
5
|
|
|
6
6
|
- `method: native_control_calls`;
|
|
7
7
|
- ordered `calls` containing only `ordinal`, `namespace`, and `name`;
|
|
8
|
-
- `acceptedAgentCount`, a verified Codex acceptance count
|
|
8
|
+
- `acceptedAgentCount`, a verified direct native Codex acceptance count when
|
|
9
|
+
overall coverage is complete, otherwise `null` (also `null` on Claude);
|
|
9
10
|
- `submittedExecCalls`, the Codex submitted-code count or `null` on Claude; and
|
|
10
11
|
- `truncated`, which is `false` for a complete record and may be unknown (`null`)
|
|
11
12
|
for an incomplete Codex record.
|
|
@@ -16,6 +17,18 @@ invocation attempt, not success. Extensions decide which calls are prohibited.
|
|
|
16
17
|
An unknown acceptance count cannot establish agent acceptance.
|
|
17
18
|
|
|
18
19
|
Codex projects labels and counts from its bound native-session observation.
|
|
20
|
+
It additionally retains `directAcceptedAgentCount`, the number of positively
|
|
21
|
+
correlated direct native spawn acceptances in that source, independently of exec
|
|
22
|
+
inspection. This is an observed count, not a total across unknown launch paths.
|
|
23
|
+
`coverage.nativeCalls` records native-session completeness;
|
|
24
|
+
`coverage.execInspection` records whether all submitted exec sources were
|
|
25
|
+
understood and matched to the direct submission inventory;
|
|
26
|
+
`coverage.acceptedAgents: direct_native_calls` names the scope of both acceptance
|
|
27
|
+
counts. Accepted launches through exec are not included in these direct counts.
|
|
28
|
+
When native capture is incomplete, even a zero observed count cannot establish
|
|
29
|
+
absence. Native labels remain usable as positive attempt evidence regardless
|
|
30
|
+
of an unrelated exec inspection gap. A rejected attempt is never an acceptance.
|
|
31
|
+
|
|
19
32
|
When submitted code is present, both observations also retain `execCalls`:
|
|
20
33
|
one receipt per native `exec` submission, with its native `ordinal`, inspection
|
|
21
34
|
`completeness`, and ordered `calls` containing `index`, `namespace`, and `name`.
|
|
@@ -27,25 +40,46 @@ programs without executing them. It accepts literal tool calls, constant result
|
|
|
27
40
|
bindings, template interpolation and `+` composition of inspected expressions,
|
|
28
41
|
direct `text`/`store` output, and literal `Promise.all`/`allSettled` batches.
|
|
29
42
|
Batch results may use flat constant array bindings or result-only
|
|
30
|
-
`forEach(text)` or `for ... of` output
|
|
43
|
+
`forEach(text)` or `for ... of` output, including blocks and conditional
|
|
44
|
+
result-field selection. `JSON.stringify`, `String`, object spread/shorthand,
|
|
45
|
+
and nested result fields are supported. Numeric output loops must use the
|
|
46
|
+
bounded shape `for (let i = 0; i < results.length; i++)`, indexing the known
|
|
47
|
+
batch with that loop variable. Output loops and conditional expressions cannot
|
|
48
|
+
invoke tools. Destructuring defaults, rest elements,
|
|
31
49
|
holes, nested bindings, duplicate names, and shadowed globals remain unsupported.
|
|
32
|
-
|
|
33
|
-
unique, later native completion for the same call ID with an intact successful
|
|
34
|
-
script envelope. Tool argument strings, comments and printed tool-looking text
|
|
50
|
+
Tool argument strings, comments and printed tool-looking text
|
|
35
51
|
do not become call labels. A rejected tool request returned by a completed
|
|
36
52
|
script still appears in its submitted-call inventory. This inventory proves
|
|
37
53
|
submission of those requests, not successful effects or acceptance; the existing
|
|
38
54
|
acceptance count describes independently correlated direct native spawns.
|
|
39
55
|
|
|
40
56
|
Dynamic lookup, aliases, shadowed globals, callbacks that invoke tools,
|
|
41
|
-
conditional
|
|
42
|
-
|
|
57
|
+
conditional tool execution, and nested executors keep source inspection partial.
|
|
58
|
+
Each receipt's `inspectionReason` is `null` on success, or `unsupported_source`,
|
|
59
|
+
`malformed_source`, `source_limit`, or `foreign_namespace`. Unsupported syntax
|
|
60
|
+
and expression/call-budget exhaustion use `unsupported_source`.
|
|
61
|
+
Source is bounded at
|
|
43
62
|
64 KiB, traversed expressions at 2,048, and calls at 128 per submission; tool
|
|
44
63
|
labels are bounded at 64 characters. Private source and output bytes are discarded.
|
|
45
64
|
The portable observation becomes partial if any submission is uninspected or
|
|
46
|
-
receipt counts/ordinals disagree with the native list.
|
|
47
|
-
`null
|
|
48
|
-
|
|
65
|
+
receipt counts/ordinals disagree with the native list. `acceptedAgentCount`
|
|
66
|
+
remains `null` in that case, while native coverage, direct acceptance facts and
|
|
67
|
+
the native `submittedExecCalls` count remain independently available.
|
|
68
|
+
`submittedExecCalls` and `truncated` depend on native capture, not exec output.
|
|
69
|
+
|
|
70
|
+
Receipt `completeness` describes source inspection, independently of execution
|
|
71
|
+
or displayed output. `execution` is `completed`, `failed`, or `incomplete`;
|
|
72
|
+
`output` is `complete`, `truncated`, or `unavailable`. Output evidence requires
|
|
73
|
+
a unique later output bound to the same native call ID, with recognized text
|
|
74
|
+
blocks. `complete` describes the displayed receipt, not the whole program's
|
|
75
|
+
execution. Execution status additionally requires a successful or failed script
|
|
76
|
+
header. Missing, ambiguous or
|
|
77
|
+
malformed output yields `incomplete`/`unavailable`; a yielded or unrecognized
|
|
78
|
+
header stays `incomplete`. Displayed truncation does not change source coverage.
|
|
79
|
+
A fully inspected source with no launch call proves that absence even if its
|
|
80
|
+
shell output was truncated or the exec cell failed. Source inspection cannot
|
|
81
|
+
prove execution or success. Missing fields in historical receipts are unknown;
|
|
82
|
+
historical artifacts retain their original results and meanings.
|
|
49
83
|
|
|
50
84
|
Claude records every tool-use label from the bounded stream, with namespace
|
|
51
85
|
`claude`, without filtering to Skill or Agent. It requires a completed successful
|
|
@@ -55,11 +89,12 @@ Tool-looking text in arguments or output does not become a native call.
|
|
|
55
89
|
|
|
56
90
|
Consumers must verify the source, completeness, shape, ordering, and counts
|
|
57
91
|
before using an empty call list to establish absence. Missing or partial
|
|
58
|
-
|
|
92
|
+
source coverage cannot establish a negative assertion. For Codex, consumers must
|
|
59
93
|
inspect every `execCalls` receipt in addition to the direct `calls` list,
|
|
60
94
|
validate receipt ordinals against the direct `exec` submissions, and require
|
|
61
95
|
all receipts to be complete with counts matching `submittedExecCalls`.
|
|
62
|
-
They
|
|
96
|
+
They can use positively observed native attempt labels before requiring complete
|
|
97
|
+
coverage for an absence pass. They must examine submitted labels for prohibited controls even when
|
|
63
98
|
`acceptedAgentCount` is zero. Older consumers that require
|
|
64
99
|
`submittedExecCalls === 0` remain conservative but cannot use this new evidence
|
|
65
100
|
until they adopt receipt validation.
|
package/docs/runtime-v1.md
CHANGED
|
@@ -260,7 +260,10 @@ so zsh here-documents use their existing private temporary storage while shared
|
|
|
260
260
|
host temporary directories remain denied. `TMPPREFIX` is runner-owned and cannot
|
|
261
261
|
be inherited or set through runtime configuration.
|
|
262
262
|
Sevro supplies private `HOME` and temporary storage and sets `UV_OFFLINE=1`
|
|
263
|
-
and `PYTHONDONTWRITEBYTECODE=1`.
|
|
263
|
+
and `PYTHONDONTWRITEBYTECODE=1`. Fixture setup uses the candidate commands'
|
|
264
|
+
private `HOME`, so `$HOME` paths initialize the same state the candidate will
|
|
265
|
+
use. Its temporary directory stays disposable.
|
|
266
|
+
Candidate, hook, checker, semantic, and advisory
|
|
264
267
|
state are separate. With a runtime policy, native candidate, semantic, and
|
|
265
268
|
advisory roots live in a unique runner-owned host directory outside the Git
|
|
266
269
|
fixture. The root contains `home`, `tmp`, and declared seed slots; plugins can
|
|
@@ -270,7 +273,8 @@ configuration, and live transcript sources. Checker and hook caches also live
|
|
|
270
273
|
outside candidate-controlled workspace state. Private roots survive native
|
|
271
274
|
continuation turns and children, then are removed with their owning trial or
|
|
272
275
|
host state after owned hooks terminate. Opted-in candidate artifact runtimes
|
|
273
|
-
remain alive through grading as described below. Runs
|
|
276
|
+
remain alive through grading as described below. Runs with neither fixture setup
|
|
277
|
+
nor a selected runtime policy
|
|
274
278
|
retain their previous writable-state locations.
|
|
275
279
|
|
|
276
280
|
Claude refuses managed workspace or runtime namespaces overlapping its
|
|
@@ -297,7 +301,8 @@ candidate's private home after execution.
|
|
|
297
301
|
}
|
|
298
302
|
```
|
|
299
303
|
|
|
300
|
-
The example permits only the
|
|
304
|
+
The example permits only the `~/.darrow/reviews` subtree, which may be initialized
|
|
305
|
+
by trusted fixture setup or created by the candidate.
|
|
301
306
|
Its original absolute path reaches isolated shell checks and native
|
|
302
307
|
semantic/advisory commands as `SEVRO_CANDIDATE_ARTIFACT_REVIEWS`. Graders keep
|
|
303
308
|
their own private `HOME`; use the supplied variable to read candidate evidence.
|
|
@@ -313,12 +318,31 @@ to 16,384 entries, including its root. Missing directories, symlinked ancestors,
|
|
|
313
318
|
descendant symlinks, hardlinks and special files make grading an error; missing
|
|
314
319
|
evidence cannot produce a pass.
|
|
315
320
|
|
|
321
|
+
Initialize an empty directory or seed prior Review state in the extension's
|
|
322
|
+
trusted `fixtureSetup` command before the candidate starts. For example:
|
|
323
|
+
|
|
324
|
+
```sh
|
|
325
|
+
mkdir -p "$HOME/.darrow/reviews"
|
|
326
|
+
printf '%s\n' '{"verdict":"passed"}' > "$HOME/.darrow/reviews/prior.json"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Setup and candidate commands share this private trial home, including when no
|
|
330
|
+
artifact grants or runtime file are selected. Graders keep their own homes and
|
|
331
|
+
receive only declared read-only artifact grants. To test missing reports or
|
|
332
|
+
absence of new artifacts, initialize the directory without writing a report;
|
|
333
|
+
an empty safe directory reaches ordinary task checks. Missing required reports
|
|
334
|
+
can then fail those checks, while negative cases can assert the directory is
|
|
335
|
+
empty. Sevro does not automatically create declared artifact directories: if
|
|
336
|
+
setup and candidate both leave one absent, or replace it with an unsafe path,
|
|
337
|
+
grading still errors. Setup state is temporary and is removed with its trial,
|
|
338
|
+
including setup failure, cancellation, and dry runs.
|
|
339
|
+
|
|
316
340
|
The grant permits reads and denies writes, creation, deletion and renaming.
|
|
317
341
|
Credentials, other home contents and peer role/trial state stay protected.
|
|
318
342
|
Ordinary `filesystem.readOnlyRoots` cannot select this private-state exception.
|
|
319
343
|
Artifact grants and native transcript access are independent opt-ins.
|
|
320
344
|
|
|
321
|
-
The trial allocates the retained runtime before
|
|
345
|
+
The trial allocates the retained runtime before fixture setup and
|
|
322
346
|
removes it after grading, advisory review and evidence persistence, including
|
|
323
347
|
failure and cancellation paths. Review state remains outside the Git fixture.
|
|
324
348
|
These artifacts are temporary; declaring access does not copy their bytes into
|
package/package.json
CHANGED
|
@@ -65,10 +65,12 @@ function validArtifactPath(path: string): boolean {
|
|
|
65
65
|
/** Allocate before host execution so exceptions cannot lose cleanup ownership. */
|
|
66
66
|
export async function allocateCandidateRuntime(
|
|
67
67
|
policy: RuntimePolicy | undefined,
|
|
68
|
+
retainHome = false,
|
|
68
69
|
): Promise<string | undefined> {
|
|
69
70
|
const directories = policy?.artifactDirectories ?? [];
|
|
70
71
|
const codexState = codexStateEnabled(policy);
|
|
71
|
-
if (!directories
|
|
72
|
+
if (!candidateRuntimeRequired(directories, codexState, retainHome))
|
|
73
|
+
return undefined;
|
|
72
74
|
requireArtifactDirectories(directories);
|
|
73
75
|
const root = await allocateNativeState("candidate-runtime-");
|
|
74
76
|
leases.set(root, directories);
|
|
@@ -76,6 +78,14 @@ export async function allocateCandidateRuntime(
|
|
|
76
78
|
return root;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
function candidateRuntimeRequired(
|
|
82
|
+
directories: RuntimeArtifactDirectory[],
|
|
83
|
+
codexState: boolean,
|
|
84
|
+
retainHome: boolean,
|
|
85
|
+
): boolean {
|
|
86
|
+
return directories.length > 0 || codexState || retainHome;
|
|
87
|
+
}
|
|
88
|
+
|
|
79
89
|
export async function requireCandidateRuntime(
|
|
80
90
|
root: string | undefined,
|
|
81
91
|
role?: RuntimeRole,
|
|
@@ -181,6 +181,7 @@ export async function prepareTrialWorkspace(
|
|
|
181
181
|
invocation: PreparedInvocation,
|
|
182
182
|
workspaces: RunWorkspaceReservations,
|
|
183
183
|
trial: number,
|
|
184
|
+
candidateRuntimeRoot?: string,
|
|
184
185
|
) {
|
|
185
186
|
const reservation = workspaces.candidate(trial);
|
|
186
187
|
workspaces.retain(reservation);
|
|
@@ -195,6 +196,7 @@ export async function prepareTrialWorkspace(
|
|
|
195
196
|
fixture.projectRoot,
|
|
196
197
|
options.signal,
|
|
197
198
|
reservation,
|
|
199
|
+
candidateRuntimeRoot,
|
|
198
200
|
).catch((error: unknown) => {
|
|
199
201
|
if (
|
|
200
202
|
!options.signal?.aborted ||
|
|
@@ -203,12 +205,13 @@ export async function prepareTrialWorkspace(
|
|
|
203
205
|
throw error;
|
|
204
206
|
return null;
|
|
205
207
|
});
|
|
206
|
-
|
|
208
|
+
const details = await trialWorkspaceDetails(
|
|
207
209
|
options.case,
|
|
208
210
|
invocation,
|
|
209
211
|
reservation,
|
|
210
212
|
workspace,
|
|
211
213
|
);
|
|
214
|
+
return { ...details, candidateRuntimeRoot };
|
|
212
215
|
}
|
|
213
216
|
|
|
214
217
|
async function trialWorkspaceDetails(
|
package/src/evaluation/trial.ts
CHANGED
|
@@ -231,6 +231,7 @@ class EvaluationTrial {
|
|
|
231
231
|
private readonly fixture: Awaited<ReturnType<typeof prepareTrialWorkspace>>,
|
|
232
232
|
) {
|
|
233
233
|
this.state = initialTrialState(options.dry);
|
|
234
|
+
this.state.candidateRuntimeRoot = fixture.candidateRuntimeRoot;
|
|
234
235
|
this.evidence = new TrialEvidenceWriter({
|
|
235
236
|
runDir: context.resources.runDir,
|
|
236
237
|
trial,
|
|
@@ -296,7 +297,7 @@ class EvaluationTrial {
|
|
|
296
297
|
workspace: this.fixture.workspace,
|
|
297
298
|
condition: this.options.condition,
|
|
298
299
|
fixtureBinDir: this.fixture.fixtureBinDir,
|
|
299
|
-
runtimePolicy: this.
|
|
300
|
+
runtimePolicy: this.candidateRuntimePolicy(),
|
|
300
301
|
instrumentation: snapshotInstrumentation(
|
|
301
302
|
this.context.prepared.requestedInstrumentation,
|
|
302
303
|
),
|
|
@@ -313,6 +314,16 @@ class EvaluationTrial {
|
|
|
313
314
|
};
|
|
314
315
|
}
|
|
315
316
|
|
|
317
|
+
private candidateRuntimePolicy() {
|
|
318
|
+
if (this.options.runtimePolicy) return this.options.runtimePolicy;
|
|
319
|
+
if (!this.context.prepared.fixture.fixtureSetup) return undefined;
|
|
320
|
+
return {
|
|
321
|
+
format: "sevro.runtime.v1" as const,
|
|
322
|
+
environment: {},
|
|
323
|
+
readOnlyRoots: [],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
316
327
|
private candidateReservedIds(): Set<string> {
|
|
317
328
|
return new Set([
|
|
318
329
|
"sevro.observation.final-message",
|
|
@@ -387,9 +398,6 @@ class EvaluationTrial {
|
|
|
387
398
|
const started = performance.now();
|
|
388
399
|
try {
|
|
389
400
|
ensureNotCancelled(this.options.signal);
|
|
390
|
-
this.state.candidateRuntimeRoot = await allocateCandidateRuntime(
|
|
391
|
-
this.options.runtimePolicy,
|
|
392
|
-
);
|
|
393
401
|
this.state.hostResult = await this.options.host.run(
|
|
394
402
|
this.candidateRequest(),
|
|
395
403
|
);
|
|
@@ -781,7 +789,6 @@ class EvaluationTrial {
|
|
|
781
789
|
evaluationProgress(this.options, stage, status, this.trial);
|
|
782
790
|
}
|
|
783
791
|
private async cleanup(): Promise<void> {
|
|
784
|
-
await releaseCandidateRuntime(this.state.candidateRuntimeRoot);
|
|
785
792
|
if (this.state.candidateTranscriptRoot)
|
|
786
793
|
await rm(this.state.candidateTranscriptRoot, {
|
|
787
794
|
recursive: true,
|
|
@@ -819,21 +826,30 @@ export async function executeEvaluationTrials(
|
|
|
819
826
|
signal: options.signal,
|
|
820
827
|
},
|
|
821
828
|
async (trial, stopAdmission) => {
|
|
822
|
-
const
|
|
823
|
-
options,
|
|
824
|
-
context.prepared.fixture,
|
|
825
|
-
context.prepared.invocation,
|
|
826
|
-
context.resources.workspaces,
|
|
827
|
-
trial,
|
|
829
|
+
const candidateRuntimeRoot = await allocateCandidateRuntime(
|
|
830
|
+
options.runtimePolicy,
|
|
831
|
+
Boolean(context.prepared.fixture.fixtureSetup),
|
|
828
832
|
);
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
833
|
+
try {
|
|
834
|
+
const fixture = await prepareTrialWorkspace(
|
|
835
|
+
options,
|
|
836
|
+
context.prepared.fixture,
|
|
837
|
+
context.prepared.invocation,
|
|
838
|
+
context.resources.workspaces,
|
|
839
|
+
trial,
|
|
840
|
+
candidateRuntimeRoot,
|
|
841
|
+
);
|
|
842
|
+
evaluationProgress(options, "preparation", "completed", trial);
|
|
843
|
+
return await new EvaluationTrial(
|
|
844
|
+
options,
|
|
845
|
+
context,
|
|
846
|
+
trial,
|
|
847
|
+
stopAdmission,
|
|
848
|
+
fixture,
|
|
849
|
+
).run();
|
|
850
|
+
} finally {
|
|
851
|
+
await releaseCandidateRuntime(candidateRuntimeRoot);
|
|
852
|
+
}
|
|
837
853
|
},
|
|
838
854
|
);
|
|
839
855
|
}
|
package/src/fixtures/create.ts
CHANGED
|
@@ -36,6 +36,7 @@ export async function createFixture(
|
|
|
36
36
|
projectRoot: string,
|
|
37
37
|
signal?: AbortSignal,
|
|
38
38
|
reservedWorkspace?: string,
|
|
39
|
+
candidateRuntimeRoot?: string,
|
|
39
40
|
): Promise<string> {
|
|
40
41
|
const paths = inlineFixturePaths(fixture, generated, repository);
|
|
41
42
|
const workspace = reservedWorkspace ?? (await allocateWorkspace());
|
|
@@ -53,7 +54,13 @@ export async function createFixture(
|
|
|
53
54
|
workspace,
|
|
54
55
|
);
|
|
55
56
|
await writeInlineFixtureFiles(paths, workspace);
|
|
56
|
-
await setupFixture(
|
|
57
|
+
await setupFixture(
|
|
58
|
+
setup,
|
|
59
|
+
workspace,
|
|
60
|
+
projectRoot,
|
|
61
|
+
signal,
|
|
62
|
+
candidateRuntimeRoot,
|
|
63
|
+
);
|
|
57
64
|
await mountPreparationArtifacts(artifacts, workspace);
|
|
58
65
|
await excludePreparationArtifacts(artifacts, workspace);
|
|
59
66
|
await fixtureBinDirectory(workspace);
|
|
@@ -131,6 +138,7 @@ async function setupFixture(
|
|
|
131
138
|
workspace: string,
|
|
132
139
|
projectRoot: string,
|
|
133
140
|
signal?: AbortSignal,
|
|
141
|
+
candidateRuntimeRoot?: string,
|
|
134
142
|
): Promise<void> {
|
|
135
143
|
if (!setup) return;
|
|
136
144
|
await runFixtureSetup(setup, {
|
|
@@ -138,6 +146,7 @@ async function setupFixture(
|
|
|
138
146
|
projectRoot,
|
|
139
147
|
fixtureBinDir: (await fixtureBinDirectory(workspace)) ?? undefined,
|
|
140
148
|
signal,
|
|
149
|
+
candidateRuntimeRoot,
|
|
141
150
|
});
|
|
142
151
|
}
|
|
143
152
|
|
package/src/fixtures/setup.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
1
|
+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { delimiter, isAbsolute, join } from "node:path";
|
|
4
4
|
import { isRecord } from "../value-guards";
|
|
5
|
+
import { requireCandidateRuntime } from "../candidate-artifacts";
|
|
5
6
|
import {
|
|
6
7
|
ProcessDeadline,
|
|
7
8
|
terminateOwnedProcessGroup,
|
|
@@ -112,6 +113,7 @@ interface FixtureSetupOptions {
|
|
|
112
113
|
projectRoot: string;
|
|
113
114
|
fixtureBinDir?: string;
|
|
114
115
|
signal?: AbortSignal;
|
|
116
|
+
candidateRuntimeRoot?: string;
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
function setupPath(directory: string | undefined): string {
|
|
@@ -134,7 +136,9 @@ function executionEnvironment(
|
|
|
134
136
|
);
|
|
135
137
|
return {
|
|
136
138
|
PATH: setupPath(options.fixtureBinDir),
|
|
137
|
-
HOME:
|
|
139
|
+
HOME: options.candidateRuntimeRoot
|
|
140
|
+
? join(options.candidateRuntimeRoot, "home")
|
|
141
|
+
: home,
|
|
138
142
|
TMPDIR: home,
|
|
139
143
|
LANG: process.env.LANG ?? "C",
|
|
140
144
|
GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null",
|
|
@@ -201,6 +205,12 @@ export async function runFixtureSetup(
|
|
|
201
205
|
options: FixtureSetupOptions,
|
|
202
206
|
): Promise<void> {
|
|
203
207
|
if (options.signal?.aborted) throw new Error("fixture setup cancelled");
|
|
208
|
+
await requireCandidateRuntime(options.candidateRuntimeRoot);
|
|
209
|
+
if (options.candidateRuntimeRoot)
|
|
210
|
+
await mkdir(join(options.candidateRuntimeRoot, "home"), {
|
|
211
|
+
recursive: true,
|
|
212
|
+
mode: 0o700,
|
|
213
|
+
});
|
|
204
214
|
const home = await mkdtemp(join(tmpdir(), "sevro-setup-"));
|
|
205
215
|
const execution = new FixtureSetupExecution(setup, options, home);
|
|
206
216
|
try {
|
|
@@ -21,6 +21,14 @@ export interface ExecCallReceipt {
|
|
|
21
21
|
ordinal: number;
|
|
22
22
|
completeness: "complete" | "partial";
|
|
23
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";
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
function toolNamespace(name: string): SubmittedCall["namespace"] {
|
|
@@ -77,7 +85,15 @@ function member(node: ts.Expression, owner: string, names: string[]): boolean {
|
|
|
77
85
|
function localName(node: ts.BindingName): string | undefined {
|
|
78
86
|
const ts = typescript();
|
|
79
87
|
if (!ts.isIdentifier(node)) return undefined;
|
|
80
|
-
return [
|
|
88
|
+
return [
|
|
89
|
+
"tools",
|
|
90
|
+
"text",
|
|
91
|
+
"store",
|
|
92
|
+
"Promise",
|
|
93
|
+
"load",
|
|
94
|
+
"JSON",
|
|
95
|
+
"String",
|
|
96
|
+
].includes(node.text)
|
|
81
97
|
? undefined
|
|
82
98
|
: node.text;
|
|
83
99
|
}
|
|
@@ -85,6 +101,7 @@ class Submission {
|
|
|
85
101
|
readonly calls: SubmittedCall[] = [];
|
|
86
102
|
private readonly locals = new Set<string>();
|
|
87
103
|
private readonly collections = new Set<string>();
|
|
104
|
+
private readonly indices = new Set<string>();
|
|
88
105
|
private nodes = 0;
|
|
89
106
|
expression(node: ts.Expression): boolean {
|
|
90
107
|
const ts = typescript();
|
|
@@ -110,17 +127,42 @@ class Submission {
|
|
|
110
127
|
return this.localProperty(node);
|
|
111
128
|
case ts.isCallExpression(node):
|
|
112
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
|
+
);
|
|
113
140
|
default:
|
|
114
141
|
return false;
|
|
115
142
|
}
|
|
116
143
|
}
|
|
117
144
|
private property(node: ts.ObjectLiteralElementLike): boolean {
|
|
118
145
|
const ts = typescript();
|
|
146
|
+
if (ts.isSpreadAssignment(node))
|
|
147
|
+
return this.outputExpression(node.expression);
|
|
148
|
+
if (ts.isShorthandPropertyAssignment(node)) return this.shorthand(node);
|
|
119
149
|
if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name))
|
|
120
150
|
return false;
|
|
121
151
|
return this.expression(node.initializer);
|
|
122
152
|
}
|
|
153
|
+
private shorthand(node: ts.ShorthandPropertyAssignment): boolean {
|
|
154
|
+
return !node.objectAssignmentInitializer && this.expression(node.name);
|
|
155
|
+
}
|
|
123
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
|
+
);
|
|
124
166
|
return (
|
|
125
167
|
node.operatorToken.kind === typescript().SyntaxKind.PlusToken &&
|
|
126
168
|
this.expression(node.left) &&
|
|
@@ -148,13 +190,12 @@ class Submission {
|
|
|
148
190
|
if (ts.isVariableStatement(node))
|
|
149
191
|
return this.declarations(node.declarationList);
|
|
150
192
|
if (ts.isForOfStatement(node)) return this.outputLoop(node);
|
|
193
|
+
if (ts.isForStatement(node)) return this.numericOutputLoop(node);
|
|
151
194
|
return ts.isExpressionStatement(node) && this.expression(node.expression);
|
|
152
195
|
}
|
|
153
196
|
private localProperty(node: ts.PropertyAccessExpression): boolean {
|
|
154
|
-
const ts = typescript();
|
|
155
197
|
return (
|
|
156
|
-
|
|
157
|
-
this.locals.has(node.expression.text) &&
|
|
198
|
+
this.expression(node.expression) &&
|
|
158
199
|
!["constructor", "__proto__", "prototype"].includes(node.name.text)
|
|
159
200
|
);
|
|
160
201
|
}
|
|
@@ -231,10 +272,43 @@ class Submission {
|
|
|
231
272
|
if (!this.collection(node.expression)) return false;
|
|
232
273
|
if (!ts.isVariableDeclarationList(node.initializer)) return false;
|
|
233
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);
|
|
234
303
|
return (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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))
|
|
238
312
|
);
|
|
239
313
|
}
|
|
240
314
|
private collection(node: ts.Expression): boolean {
|
|
@@ -244,9 +318,12 @@ class Submission {
|
|
|
244
318
|
}
|
|
245
319
|
function helperCall(node: ts.Expression): boolean {
|
|
246
320
|
const ts = typescript();
|
|
247
|
-
if (ts.isIdentifier(node) && ["text", "store"].includes(node.text))
|
|
321
|
+
if (ts.isIdentifier(node) && ["text", "store", "String"].includes(node.text))
|
|
248
322
|
return true;
|
|
249
|
-
return
|
|
323
|
+
return (
|
|
324
|
+
member(node, "Promise", ["all", "allSettled"]) ||
|
|
325
|
+
member(node, "JSON", ["stringify"])
|
|
326
|
+
);
|
|
250
327
|
}
|
|
251
328
|
function namedIdentifier(
|
|
252
329
|
node: ts.Expression | undefined,
|
|
@@ -271,22 +348,69 @@ function loopVariable(list: ts.VariableDeclarationList): string | undefined {
|
|
|
271
348
|
? localName(declaration.name)
|
|
272
349
|
: undefined;
|
|
273
350
|
}
|
|
274
|
-
function
|
|
351
|
+
function numericLoopVariable(node: ts.ForStatement): string | undefined {
|
|
275
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 {
|
|
276
359
|
if (
|
|
277
|
-
!
|
|
278
|
-
|
|
360
|
+
!(list.flags & typescript().NodeFlags.Let) ||
|
|
361
|
+
list.declarations.length !== 1
|
|
279
362
|
)
|
|
280
|
-
return
|
|
281
|
-
const
|
|
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;
|
|
282
380
|
return (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
namedIdentifier(call.arguments[0], name)
|
|
381
|
+
numericCondition(condition, name) &&
|
|
382
|
+
numericIncrement(node.incrementor, name)
|
|
286
383
|
);
|
|
287
384
|
}
|
|
288
|
-
function
|
|
289
|
-
|
|
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" };
|
|
290
414
|
const ts = typescript();
|
|
291
415
|
const source = ts.createSourceFile(
|
|
292
416
|
"submitted.js",
|
|
@@ -298,33 +422,26 @@ function inspectedSource(code: unknown): Submission | undefined {
|
|
|
298
422
|
const diagnostics = (
|
|
299
423
|
source as ts.SourceFile & { parseDiagnostics: readonly ts.Diagnostic[] }
|
|
300
424
|
).parseDiagnostics;
|
|
301
|
-
if (diagnostics.length !== 0) return
|
|
425
|
+
if (diagnostics.length !== 0) return { reason: "malformed_source" };
|
|
302
426
|
const submission = new Submission();
|
|
303
427
|
return source.statements.every((statement) => submission.statement(statement))
|
|
304
|
-
? submission
|
|
305
|
-
:
|
|
428
|
+
? { submission, reason: null }
|
|
429
|
+
: { reason: "unsupported_source" };
|
|
306
430
|
}
|
|
307
|
-
function
|
|
308
|
-
if (isUnknownArray(output)) return
|
|
309
|
-
if (typeof output !== "string") return
|
|
431
|
+
function outputBlocks(output: unknown): unknown[] {
|
|
432
|
+
if (isUnknownArray(output)) return output;
|
|
433
|
+
if (typeof output !== "string") return [];
|
|
310
434
|
try {
|
|
311
435
|
const blocks: unknown = JSON.parse(output);
|
|
312
|
-
|
|
313
|
-
return completedBlocks(blocks);
|
|
436
|
+
return isUnknownArray(blocks) ? blocks : [];
|
|
314
437
|
} catch {
|
|
315
|
-
return
|
|
438
|
+
return [];
|
|
316
439
|
}
|
|
317
440
|
}
|
|
318
|
-
function completedBlocks(blocks: unknown[]): boolean {
|
|
319
|
-
return blocks.every(completeTextBlock) && completedHeader(blocks[0]);
|
|
320
|
-
}
|
|
321
441
|
function completeTextBlock(block: unknown): boolean {
|
|
322
442
|
if (!isRecord(block) || !["text", "input_text"].includes(String(block.type)))
|
|
323
443
|
return false;
|
|
324
|
-
return
|
|
325
|
-
typeof block.text === "string" &&
|
|
326
|
-
!block.text.includes("Warning: truncated output")
|
|
327
|
-
);
|
|
444
|
+
return typeof block.text === "string";
|
|
328
445
|
}
|
|
329
446
|
function completedHeader(block: unknown): boolean {
|
|
330
447
|
return (
|
|
@@ -335,23 +452,35 @@ function completedHeader(block: unknown): boolean {
|
|
|
335
452
|
)
|
|
336
453
|
);
|
|
337
454
|
}
|
|
338
|
-
function
|
|
339
|
-
if (!nativeExecNamespace(entry.payload.namespace)) return false;
|
|
455
|
+
function submissionOutput(entry: Entry, entries: Entry[]): unknown[] {
|
|
340
456
|
const id = entry.payload.call_id;
|
|
341
|
-
if (typeof id !== "string") return
|
|
457
|
+
if (typeof id !== "string") return [];
|
|
342
458
|
const matches = entries.filter((other) => other.payload.call_id === id);
|
|
343
|
-
if (matches.length !== 2) return
|
|
459
|
+
if (matches.length !== 2) return [];
|
|
344
460
|
const result = matches.find(
|
|
345
461
|
(other) => other.payload.type === "custom_tool_call_output",
|
|
346
462
|
);
|
|
347
|
-
return
|
|
463
|
+
return result && result.ordinal > entry.ordinal
|
|
464
|
+
? outputBlocks(result.payload.output)
|
|
465
|
+
: [];
|
|
348
466
|
}
|
|
349
|
-
function
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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"),
|
|
354
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";
|
|
355
484
|
}
|
|
356
485
|
function nativeExecNamespace(namespace: unknown): boolean {
|
|
357
486
|
return namespace === undefined || namespace === "functions";
|
|
@@ -363,14 +492,25 @@ export function codexExecCallReceipts(entries: Entry[]): ExecCallReceipt[] {
|
|
|
363
492
|
({ payload }) =>
|
|
364
493
|
payload.type === "custom_tool_call" && payload.name === "exec",
|
|
365
494
|
)
|
|
366
|
-
.map((entry) => {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
+
};
|
|
376
516
|
}
|
|
@@ -14,6 +14,12 @@ export interface NativeControlObservation {
|
|
|
14
14
|
method: "native_control_calls";
|
|
15
15
|
calls: Call[];
|
|
16
16
|
acceptedAgentCount: number | null;
|
|
17
|
+
directAcceptedAgentCount?: number;
|
|
18
|
+
coverage?: {
|
|
19
|
+
nativeCalls: NativeControlObservation["completeness"];
|
|
20
|
+
execInspection: "complete" | "partial";
|
|
21
|
+
acceptedAgents: "direct_native_calls";
|
|
22
|
+
};
|
|
17
23
|
submittedExecCalls: number | null;
|
|
18
24
|
execCalls?: ExecCallReceipt[];
|
|
19
25
|
truncated: boolean | null;
|
|
@@ -47,12 +53,28 @@ export function codexNativeControls(
|
|
|
47
53
|
acceptedAgentCount: complete
|
|
48
54
|
? observation.data.acceptedSpawns.length
|
|
49
55
|
: null,
|
|
50
|
-
|
|
56
|
+
directAcceptedAgentCount: observation.data.acceptedSpawns.length,
|
|
57
|
+
coverage: {
|
|
58
|
+
nativeCalls: observation.completeness,
|
|
59
|
+
execInspection: inspectedExecCalls(observation)
|
|
60
|
+
? "complete"
|
|
61
|
+
: "partial",
|
|
62
|
+
acceptedAgents: "direct_native_calls",
|
|
63
|
+
},
|
|
64
|
+
...nativeCounts(observation),
|
|
51
65
|
...execEvidence(observation.data.execCalls),
|
|
52
|
-
truncated: complete ? false : null,
|
|
53
66
|
},
|
|
54
67
|
};
|
|
55
68
|
}
|
|
69
|
+
function nativeCounts(
|
|
70
|
+
observation: Awaited<ReturnType<typeof codexNativeCallObservation>>,
|
|
71
|
+
) {
|
|
72
|
+
const complete = observation.completeness === "complete";
|
|
73
|
+
return {
|
|
74
|
+
submittedExecCalls: complete ? observation.data.submittedExecCalls : null,
|
|
75
|
+
truncated: complete ? false : null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
56
78
|
function controlCompleteness(
|
|
57
79
|
source: NativeControlObservation["completeness"],
|
|
58
80
|
complete: boolean,
|