@tangle-network/agent-bench 0.1.0 → 0.3.5
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/CHANGELOG.md +17 -0
- package/HARNESS.md +302 -0
- package/README.md +26 -1
- package/fixtures/aec-bench.json +18 -0
- package/fixtures/agentbench-dbbench.json +22 -0
- package/fixtures/bfcl.json +45 -0
- package/fixtures/commit0.json +72 -0
- package/fixtures/crag.json +10 -0
- package/fixtures/dabstep.json +22 -0
- package/fixtures/enterpriseops-gym.json +103 -0
- package/fixtures/finresearchbench.json +21 -0
- package/fixtures/finsearchcomp.json +66 -0
- package/fixtures/frames.json +26 -0
- package/fixtures/hotpotqa.json +182 -0
- package/fixtures/nomiracl.json +26 -0
- package/fixtures/open-rag-bench.json +16 -0
- package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
- package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
- package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
- package/fixtures/pier-agent/no-model-task/task.toml +35 -0
- package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
- package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
- package/fixtures/programbench.json +17 -0
- package/fixtures/ragbench.json +21 -0
- package/fixtures/simpleqa.json +121 -0
- package/fixtures/t2-ragbench.json +13 -0
- package/fixtures/tau2-bench.json +16 -0
- package/fixtures/tau3-banking.json +16 -0
- package/fixtures/toollm.json +28 -0
- package/fixtures/webarena-verified.json +20 -0
- package/package.json +39 -15
- package/pier_agents/__init__.py +18 -0
- package/pier_agents/candidate_contract.py +755 -0
- package/pier_agents/process_boundary.py +321 -0
- package/pier_agents/tangle_candidate.py +907 -0
- package/pier_agents/workspace_boundary.py +368 -0
- package/scripts/appworld_driver.py +359 -0
- package/scripts/cadbench_prepare.py +22 -0
- package/scripts/cadgenbench_hard_parts.py +48 -0
- package/scripts/clbench_codebase_judge.py +73 -0
- package/scripts/commit0_judge.py +170 -0
- package/scripts/dabstep_judge.py +42 -0
- package/scripts/enterpriseops_gym_judge.py +281 -0
- package/scripts/programbench_judge.py +120 -0
- package/scripts/render-gate-chart.mjs +176 -0
- package/scripts/run-package-tests.mjs +56 -0
- package/scripts/terminate-pier-trial.mts +66 -0
- package/scripts/trata-hedge/README.md +56 -0
- package/scripts/trata-hedge/run.sh +60 -0
- package/scripts/trata-hedge/solve.py +83 -0
- package/scripts/verify-packed-consumer.mjs +224 -0
- package/scripts/verify-pier-agent.mts +715 -0
- package/scripts/verify-pier-pair.mts +74 -0
- package/scripts/verify-pier-recovery.mts +139 -0
- package/src/adapters.ts +26 -0
- package/src/benchmarks/_harness.test.mts +178 -0
- package/src/benchmarks/_harness.ts +239 -16
- package/src/benchmarks/agentbench.ts +163 -0
- package/src/benchmarks/appworld.test.mts +15 -9
- package/src/benchmarks/bfcl.ts +346 -0
- package/src/benchmarks/crag.ts +137 -0
- package/src/benchmarks/dabstep.test.mts +70 -0
- package/src/benchmarks/dabstep.ts +212 -0
- package/src/benchmarks/external-adapters.test.mts +150 -0
- package/src/benchmarks/finresearchbench.ts +269 -0
- package/src/benchmarks/humaneval.ts +20 -8
- package/src/benchmarks/nomiracl.ts +180 -0
- package/src/benchmarks/open-rag-bench.ts +153 -0
- package/src/benchmarks/rag-benchmarks.test.mts +138 -0
- package/src/benchmarks/rag-shared.ts +327 -0
- package/src/benchmarks/ragbench.ts +171 -0
- package/src/benchmarks/swe-bench.test.mts +61 -0
- package/src/benchmarks/swe-bench.ts +201 -19
- package/src/benchmarks/t2-ragbench.ts +166 -0
- package/src/benchmarks/tau-bench-shared.ts +214 -0
- package/src/benchmarks/tau2-bench.ts +30 -0
- package/src/benchmarks/tau3-banking.ts +29 -0
- package/src/benchmarks/terminal-bench.test.mts +33 -0
- package/src/benchmarks/terminal-bench.ts +23 -8
- package/src/benchmarks/toollm.ts +254 -0
- package/src/benchmarks/types.ts +42 -0
- package/src/benchmarks/webarena-verified.ts +200 -0
- package/src/commit0-prereqs.sh +0 -0
- package/src/coordination-mcp-container-reach.mts +181 -0
- package/src/decoder-live.mts +1 -1
- package/src/examples/README.md +103 -39
- package/src/examples/benchmark-matrix.mts +101 -0
- package/src/examples/lean-proof-gate.README.md +77 -0
- package/src/examples/lean-proof-gate.mts +162 -0
- package/src/examples/lean-verify.ts +95 -0
- package/src/examples/lean.Dockerfile +12 -0
- package/src/examples/math-demo.mts +9 -7
- package/src/examples/strategy-demo.mts +10 -12
- package/src/gate.ts +3 -2
- package/src/hev-eval.mts +69 -0
- package/src/hev-improve.mts +169 -0
- package/src/hev-structural.mts +688 -0
- package/src/index.ts +73 -0
- package/src/mbpp-structural.mts +662 -0
- package/src/pier-agent.test-fixtures.mts +19 -0
- package/src/pier-agent.test.mts +363 -0
- package/src/pier-agent.ts +657 -0
- package/src/pier-result-grader.mjs +30 -0
- package/src/pier-result-grader.test.mts +62 -0
- package/src/pier-result-grader.ts +108 -0
- package/src/pier-task-outcome.test.mts +117 -0
- package/src/pier-task-outcome.ts +240 -0
- package/src/pier-trial-controller.test.mts +412 -0
- package/src/pier-trial-controller.ts +858 -0
- package/src/pier-trial-supervisor.mjs +352 -0
- package/src/resolve-client.ts +25 -2
- package/src/run-benchmarks-cli.mts +72 -0
- package/src/run-benchmarks-report.ts +66 -0
- package/src/run-benchmarks.test.mts +231 -0
- package/src/run-benchmarks.ts +589 -0
- package/src/smoke-structural-rollout.mts +393 -0
- package/src/swe-bench-env.test.ts +207 -0
- package/src/swe-bench-env.ts +554 -0
- package/src/swe-jail.ts +293 -0
- package/src/swe-self-improve.mts +84 -0
- package/src/swe-structural-judge-policy.test.ts +117 -0
- package/src/swe-structural-judge-policy.ts +133 -0
- package/src/swe-structural-policy.test.ts +124 -0
- package/src/swe-structural-policy.ts +132 -0
- package/src/swe-structural-provenance.test.ts +93 -0
- package/src/swe-structural-provenance.ts +138 -0
- package/src/swe-structural.mts +1260 -0
- package/src/swe-temp.ts +14 -0
- package/src/tb-container-executor.mts +234 -0
- package/src/tb-container-executor.test.mts +99 -0
- package/src/tb-supervisor-sidecar.mts +222 -0
- package/src/trata-gepa.mts +1 -1
- package/steerers/eops-itsm-population.json +1 -0
- package/tb_agents/opencode_refine_agent.py +117 -0
- package/tb_agents/opencode_router_agent.py +406 -0
- package/tb_agents/opencode_supervisor_agent.py +239 -0
- package/tb_agents/script_agent.py +66 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import test from 'node:test'
|
|
5
|
+
|
|
6
|
+
import type { AgentCandidateArtifactRef } from '@tangle-network/agent-interface'
|
|
7
|
+
|
|
8
|
+
import { createPierResultGrader } from './pier-result-grader'
|
|
9
|
+
|
|
10
|
+
const digest = (bytes: Uint8Array): `sha256:${string}` =>
|
|
11
|
+
`sha256:${createHash('sha256').update(bytes).digest('hex')}`
|
|
12
|
+
|
|
13
|
+
const artifact: AgentCandidateArtifactRef = {
|
|
14
|
+
locator: { kind: 's3', bucket: 'pier-graders', key: 'pier-result-grader.mjs' },
|
|
15
|
+
sha256: `sha256:${'a'.repeat(64)}`,
|
|
16
|
+
byteLength: 1,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test('executes admitted Pier grader bytes over exact official result evidence', async () => {
|
|
20
|
+
const implementation = await readFile(new URL('./pier-result-grader.mjs', import.meta.url))
|
|
21
|
+
const resultBytes = Buffer.from(
|
|
22
|
+
JSON.stringify({ verifier_result: { rewards: { reward: 1, patch_applied: 1 } } }),
|
|
23
|
+
)
|
|
24
|
+
const grader = createPierResultGrader({ name: 'pier-result', version: '1', artifact })
|
|
25
|
+
const output = await grader.run({
|
|
26
|
+
executionId: 'execution-1',
|
|
27
|
+
termination: { kind: 'exit', exitCode: 0 },
|
|
28
|
+
outcome: { evidence: { digest: `sha256:${'b'.repeat(64)}` } } as never,
|
|
29
|
+
implementation: { byteLength: implementation.byteLength, bytes: implementation },
|
|
30
|
+
signal: new AbortController().signal,
|
|
31
|
+
officialResult: { value: JSON.parse(resultBytes.toString('utf8')), bytes: resultBytes },
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
assert.deepEqual(output.evaluation, {
|
|
35
|
+
score: 1,
|
|
36
|
+
passed: true,
|
|
37
|
+
dimensions: { patch_applied: 1, reward: 1 },
|
|
38
|
+
raw: {},
|
|
39
|
+
})
|
|
40
|
+
assert.deepEqual(Buffer.from(output.evidence), resultBytes)
|
|
41
|
+
assert.equal(output.binding.implementationDigest, digest(implementation))
|
|
42
|
+
assert.equal(output.binding.outputDigest, digest(resultBytes))
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('fails closed for missing official rewards and pre-aborted execution', async () => {
|
|
46
|
+
const implementation = await readFile(new URL('./pier-result-grader.mjs', import.meta.url))
|
|
47
|
+
const grader = createPierResultGrader({ name: 'pier-result', version: '1', artifact })
|
|
48
|
+
const base = {
|
|
49
|
+
executionId: 'execution-1',
|
|
50
|
+
termination: { kind: 'exit', exitCode: 0 } as const,
|
|
51
|
+
outcome: { evidence: { digest: `sha256:${'b'.repeat(64)}` } } as never,
|
|
52
|
+
implementation: { byteLength: implementation.byteLength, bytes: implementation },
|
|
53
|
+
officialResult: { value: {}, bytes: Buffer.from('{}') },
|
|
54
|
+
}
|
|
55
|
+
await assert.rejects(
|
|
56
|
+
grader.run({ ...base, signal: new AbortController().signal }),
|
|
57
|
+
/omitted verifier_result.rewards/,
|
|
58
|
+
)
|
|
59
|
+
const controller = new AbortController()
|
|
60
|
+
controller.abort(new Error('deadline'))
|
|
61
|
+
await assert.rejects(grader.run({ ...base, signal: controller.signal }), /deadline/)
|
|
62
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Execute the exact admitted Pier result parser bytes in a fresh Node process. */
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
import type { BenchmarkEvaluation } from '@tangle-network/agent-eval'
|
|
9
|
+
|
|
10
|
+
import type { PierCandidateGraderPort } from './pier-agent'
|
|
11
|
+
|
|
12
|
+
const sha256 = (bytes: Uint8Array): `sha256:${string}` =>
|
|
13
|
+
`sha256:${createHash('sha256').update(bytes).digest('hex')}`
|
|
14
|
+
|
|
15
|
+
export function createPierResultGrader(
|
|
16
|
+
descriptor: Pick<PierCandidateGraderPort, 'name' | 'version' | 'artifact'>,
|
|
17
|
+
): PierCandidateGraderPort {
|
|
18
|
+
return {
|
|
19
|
+
...descriptor,
|
|
20
|
+
run: async (input) => {
|
|
21
|
+
input.signal.throwIfAborted()
|
|
22
|
+
const implementation = Uint8Array.from(input.implementation.bytes)
|
|
23
|
+
if (implementation.byteLength !== input.implementation.byteLength) {
|
|
24
|
+
throw new Error('Pier grader implementation length changed before execution')
|
|
25
|
+
}
|
|
26
|
+
const evidence = Uint8Array.from(input.officialResult.bytes)
|
|
27
|
+
const evaluation = await executeGrader(implementation, evidence, input.signal)
|
|
28
|
+
input.signal.throwIfAborted()
|
|
29
|
+
return {
|
|
30
|
+
evaluation,
|
|
31
|
+
evidence,
|
|
32
|
+
binding: {
|
|
33
|
+
implementationDigest: sha256(implementation),
|
|
34
|
+
taskOutcomeDigest: input.outcome.evidence.digest,
|
|
35
|
+
outputDigest: sha256(evidence),
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function executeGrader(
|
|
43
|
+
implementation: Uint8Array,
|
|
44
|
+
resultBytes: Uint8Array,
|
|
45
|
+
signal: AbortSignal,
|
|
46
|
+
): Promise<BenchmarkEvaluation> {
|
|
47
|
+
const directory = await mkdtemp(join(tmpdir(), 'pier-result-grader-'))
|
|
48
|
+
const script = join(directory, 'grader.mjs')
|
|
49
|
+
try {
|
|
50
|
+
await writeFile(script, implementation, { flag: 'wx', mode: 0o600 })
|
|
51
|
+
await chmod(script, 0o500)
|
|
52
|
+
const stdout = await runNode(script, resultBytes, signal)
|
|
53
|
+
const parsed = JSON.parse(stdout.toString('utf8')) as BenchmarkEvaluation
|
|
54
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
55
|
+
throw new Error('Pier result grader returned a non-object evaluation')
|
|
56
|
+
}
|
|
57
|
+
return parsed
|
|
58
|
+
} finally {
|
|
59
|
+
await rm(directory, { recursive: true, force: true })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function runNode(
|
|
64
|
+
script: string,
|
|
65
|
+
input: Uint8Array,
|
|
66
|
+
signal: AbortSignal,
|
|
67
|
+
): Promise<Buffer> {
|
|
68
|
+
signal.throwIfAborted()
|
|
69
|
+
return await new Promise((resolveResult, reject) => {
|
|
70
|
+
const child = spawn(process.execPath, ['--no-warnings', script], {
|
|
71
|
+
env: {
|
|
72
|
+
PATH: process.env.PATH,
|
|
73
|
+
HOME: process.env.HOME,
|
|
74
|
+
LANG: 'C',
|
|
75
|
+
LC_ALL: 'C',
|
|
76
|
+
},
|
|
77
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
78
|
+
signal,
|
|
79
|
+
})
|
|
80
|
+
const stdout: Buffer[] = []
|
|
81
|
+
const stderr: Buffer[] = []
|
|
82
|
+
let bytes = 0
|
|
83
|
+
const collect = (target: Buffer[]) => (chunk: Buffer) => {
|
|
84
|
+
bytes += chunk.byteLength
|
|
85
|
+
if (bytes > 1024 * 1024) {
|
|
86
|
+
child.kill('SIGKILL')
|
|
87
|
+
reject(new Error('Pier result grader exceeded its output limit'))
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
target.push(Buffer.from(chunk))
|
|
91
|
+
}
|
|
92
|
+
child.stdout.on('data', collect(stdout))
|
|
93
|
+
child.stderr.on('data', collect(stderr))
|
|
94
|
+
child.once('error', reject)
|
|
95
|
+
child.once('close', (code, childSignal) => {
|
|
96
|
+
if (code !== 0) {
|
|
97
|
+
reject(
|
|
98
|
+
new Error(
|
|
99
|
+
`Pier result grader failed (${childSignal ?? code}): ${Buffer.concat(stderr).toString('utf8').trim()}`,
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
resolveResult(Buffer.concat(stdout))
|
|
105
|
+
})
|
|
106
|
+
child.stdin.end(input)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
3
|
+
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import test from 'node:test'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
captureAgentCandidateWorkspaceFiles,
|
|
10
|
+
createAgentCandidateWorkspacePort,
|
|
11
|
+
} from '@tangle-network/agent-runtime/candidate-execution'
|
|
12
|
+
|
|
13
|
+
import { capturePierTaskOutcome } from './pier-task-outcome'
|
|
14
|
+
|
|
15
|
+
test('Pier task outcome reconstructs the exact tree and deterministic archive from a patch', async () => {
|
|
16
|
+
const root = mkdtempSync(join(tmpdir(), 'pier-task-outcome-test-'))
|
|
17
|
+
try {
|
|
18
|
+
mkdirSync(join(root, 'src'))
|
|
19
|
+
writeFileSync(join(root, 'src/status.txt'), 'not-ready\n')
|
|
20
|
+
chmodSync(join(root, 'src/status.txt'), 0o644)
|
|
21
|
+
git(root, ['init', '-b', 'main'])
|
|
22
|
+
git(root, ['config', 'user.email', 'fixture@example.com'])
|
|
23
|
+
git(root, ['config', 'user.name', 'Fixture'])
|
|
24
|
+
git(root, ['add', '-A'])
|
|
25
|
+
git(root, ['commit', '-m', 'base'])
|
|
26
|
+
const baseCommit = git(root, ['rev-parse', 'HEAD'])
|
|
27
|
+
const baseTree = git(root, ['rev-parse', 'HEAD^{tree}'])
|
|
28
|
+
writeFileSync(join(root, 'src/status.txt'), 'ready\n')
|
|
29
|
+
const patch = execFileSync('git', ['-C', root, 'diff', '--binary'])
|
|
30
|
+
git(root, ['restore', '.'])
|
|
31
|
+
|
|
32
|
+
const captured = await capturePierTaskOutcome({ repositoryRoot: root, baseCommit, baseTree, patch })
|
|
33
|
+
assert.notEqual(captured.resultTree, baseTree)
|
|
34
|
+
assert.deepEqual(Buffer.from(captured.gitDiff), patch)
|
|
35
|
+
const expected = await captureAgentCandidateWorkspaceFiles([
|
|
36
|
+
{ path: 'src/status.txt', mode: 0o644, bytes: Buffer.from('ready\n') },
|
|
37
|
+
])
|
|
38
|
+
assert.deepEqual(captured.afterState, expected.snapshot.material)
|
|
39
|
+
assert.deepEqual(Buffer.from(captured.archive), Buffer.from(expected.archive))
|
|
40
|
+
const destination = join(root, 'materialized')
|
|
41
|
+
await createAgentCandidateWorkspacePort().materialize({
|
|
42
|
+
role: 'candidate',
|
|
43
|
+
archive: captured.archive,
|
|
44
|
+
snapshot: expected.snapshot,
|
|
45
|
+
destination,
|
|
46
|
+
})
|
|
47
|
+
assert.equal(readFileSync(join(destination, 'src/status.txt'), 'utf8'), 'ready\n')
|
|
48
|
+
} finally {
|
|
49
|
+
rmSync(root, { recursive: true, force: true })
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('Pier task outcome preserves the signed base for an empty patch', async () => {
|
|
54
|
+
const root = mkdtempSync(join(tmpdir(), 'pier-task-outcome-test-'))
|
|
55
|
+
try {
|
|
56
|
+
writeFileSync(join(root, 'README.md'), 'base\n')
|
|
57
|
+
git(root, ['init', '-b', 'main'])
|
|
58
|
+
git(root, ['config', 'user.email', 'fixture@example.com'])
|
|
59
|
+
git(root, ['config', 'user.name', 'Fixture'])
|
|
60
|
+
git(root, ['add', '-A'])
|
|
61
|
+
git(root, ['commit', '-m', 'base'])
|
|
62
|
+
const baseCommit = git(root, ['rev-parse', 'HEAD'])
|
|
63
|
+
const baseTree = git(root, ['rev-parse', 'HEAD^{tree}'])
|
|
64
|
+
|
|
65
|
+
const captured = await capturePierTaskOutcome({
|
|
66
|
+
repositoryRoot: root,
|
|
67
|
+
baseCommit,
|
|
68
|
+
baseTree,
|
|
69
|
+
patch: Buffer.alloc(0),
|
|
70
|
+
})
|
|
71
|
+
assert.equal(captured.resultTree, baseTree)
|
|
72
|
+
assert.equal(captured.gitDiff.byteLength, 0)
|
|
73
|
+
} finally {
|
|
74
|
+
rmSync(root, { recursive: true, force: true })
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('Pier task outcome returns large workspace bytes for the runtime capture path', async () => {
|
|
79
|
+
const root = mkdtempSync(join(tmpdir(), 'pier-task-outcome-test-'))
|
|
80
|
+
try {
|
|
81
|
+
const payload = Buffer.alloc(1024 * 1024 + 1, 7)
|
|
82
|
+
writeFileSync(join(root, 'large.bin'), payload)
|
|
83
|
+
git(root, ['init', '-b', 'main'])
|
|
84
|
+
git(root, ['config', 'user.email', 'fixture@example.com'])
|
|
85
|
+
git(root, ['config', 'user.name', 'Fixture'])
|
|
86
|
+
git(root, ['add', '-A'])
|
|
87
|
+
git(root, ['commit', '-m', 'base'])
|
|
88
|
+
const baseCommit = git(root, ['rev-parse', 'HEAD'])
|
|
89
|
+
const baseTree = git(root, ['rev-parse', 'HEAD^{tree}'])
|
|
90
|
+
const captured = await capturePierTaskOutcome({
|
|
91
|
+
repositoryRoot: root,
|
|
92
|
+
baseCommit,
|
|
93
|
+
baseTree,
|
|
94
|
+
patch: Buffer.alloc(0),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
assert.ok(captured.archive.byteLength > 1024 * 1024)
|
|
98
|
+
assert.equal(captured.afterState.files[0]?.byteLength, payload.byteLength)
|
|
99
|
+
} finally {
|
|
100
|
+
rmSync(root, { recursive: true, force: true })
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
function git(root: string, args: string[]): string {
|
|
105
|
+
return execFileSync('git', ['-C', root, ...args], {
|
|
106
|
+
encoding: 'utf8',
|
|
107
|
+
env: {
|
|
108
|
+
...process.env,
|
|
109
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
110
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
111
|
+
GIT_CONFIG_SYSTEM: '/dev/null',
|
|
112
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
113
|
+
GIT_NO_REPLACE_OBJECTS: '1',
|
|
114
|
+
LC_ALL: 'C',
|
|
115
|
+
},
|
|
116
|
+
}).trim()
|
|
117
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/** Reconstruct a runtime task outcome from Pier's evaluator-captured binary patch. */
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { mkdir, mkdtemp, realpath, rm } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join, resolve } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type AgentCandidateExecutorTaskOutcomeCapture,
|
|
9
|
+
captureAgentCandidateWorkspaceFiles,
|
|
10
|
+
} from '@tangle-network/agent-runtime/candidate-execution'
|
|
11
|
+
|
|
12
|
+
interface GitResult {
|
|
13
|
+
readonly stdout: Buffer
|
|
14
|
+
readonly stderr: Buffer
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Apply the official patch in an isolated Git object directory and capture its exact tree. */
|
|
18
|
+
export async function capturePierTaskOutcome(input: {
|
|
19
|
+
readonly repositoryRoot: string
|
|
20
|
+
readonly baseCommit: string
|
|
21
|
+
readonly baseTree: string
|
|
22
|
+
readonly patch: Uint8Array
|
|
23
|
+
readonly signal?: AbortSignal
|
|
24
|
+
}): Promise<Extract<AgentCandidateExecutorTaskOutcomeCapture, { kind: 'workspace' }>> {
|
|
25
|
+
input.signal?.throwIfAborted()
|
|
26
|
+
const repositoryRoot = resolve(input.repositoryRoot)
|
|
27
|
+
const head = (await git(repositoryRoot, ['rev-parse', 'HEAD'], undefined, {}, input.signal)).stdout
|
|
28
|
+
.toString('utf8')
|
|
29
|
+
.trim()
|
|
30
|
+
const headTree = (
|
|
31
|
+
await git(repositoryRoot, ['rev-parse', 'HEAD^{tree}'], undefined, {}, input.signal)
|
|
32
|
+
).stdout
|
|
33
|
+
.toString('utf8')
|
|
34
|
+
.trim()
|
|
35
|
+
if (head !== input.baseCommit || headTree !== input.baseTree) {
|
|
36
|
+
throw new Error('Pier task outcome repository drifted from its signed base')
|
|
37
|
+
}
|
|
38
|
+
const gitDir = resolve(
|
|
39
|
+
(
|
|
40
|
+
await git(repositoryRoot, ['rev-parse', '--absolute-git-dir'], undefined, {}, input.signal)
|
|
41
|
+
).stdout
|
|
42
|
+
.toString('utf8')
|
|
43
|
+
.trim(),
|
|
44
|
+
)
|
|
45
|
+
if ((await realpath(gitDir)) !== gitDir || gitDir.includes(':')) {
|
|
46
|
+
throw new Error('Pier task outcome Git directory is unsupported')
|
|
47
|
+
}
|
|
48
|
+
const replacements = (
|
|
49
|
+
await git(
|
|
50
|
+
repositoryRoot,
|
|
51
|
+
['for-each-ref', '--format=%(refname)', 'refs/replace'],
|
|
52
|
+
undefined,
|
|
53
|
+
{},
|
|
54
|
+
input.signal,
|
|
55
|
+
)
|
|
56
|
+
).stdout
|
|
57
|
+
.toString('utf8')
|
|
58
|
+
.trim()
|
|
59
|
+
if (replacements) throw new Error('Pier task outcome repository contains Git replace refs')
|
|
60
|
+
|
|
61
|
+
const temporary = await mkdtemp(join(tmpdir(), 'pier-task-outcome-'))
|
|
62
|
+
try {
|
|
63
|
+
const objectDirectory = join(temporary, 'objects')
|
|
64
|
+
const indexFile = join(temporary, 'index')
|
|
65
|
+
await mkdir(objectDirectory)
|
|
66
|
+
const environment = {
|
|
67
|
+
GIT_INDEX_FILE: indexFile,
|
|
68
|
+
GIT_OBJECT_DIRECTORY: objectDirectory,
|
|
69
|
+
GIT_ALTERNATE_OBJECT_DIRECTORIES: join(gitDir, 'objects'),
|
|
70
|
+
}
|
|
71
|
+
await git(repositoryRoot, ['read-tree', input.baseTree], undefined, environment, input.signal)
|
|
72
|
+
const patch = Uint8Array.from(input.patch)
|
|
73
|
+
if (patch.byteLength > 0) {
|
|
74
|
+
await git(
|
|
75
|
+
repositoryRoot,
|
|
76
|
+
['apply', '--cached', '--binary', '--whitespace=nowarn', '-'],
|
|
77
|
+
patch,
|
|
78
|
+
environment,
|
|
79
|
+
input.signal,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
const resultTree = (
|
|
83
|
+
await git(repositoryRoot, ['write-tree'], undefined, environment, input.signal)
|
|
84
|
+
).stdout
|
|
85
|
+
.toString('utf8')
|
|
86
|
+
.trim()
|
|
87
|
+
const entries = parseTreeEntries(
|
|
88
|
+
(
|
|
89
|
+
await git(
|
|
90
|
+
repositoryRoot,
|
|
91
|
+
['ls-tree', '-rz', '--full-tree', resultTree],
|
|
92
|
+
undefined,
|
|
93
|
+
environment,
|
|
94
|
+
input.signal,
|
|
95
|
+
)
|
|
96
|
+
).stdout,
|
|
97
|
+
)
|
|
98
|
+
if (entries.length === 0) throw new Error('Pier task outcome tree cannot be empty')
|
|
99
|
+
const files = await Promise.all(
|
|
100
|
+
entries.map(async (entry) => {
|
|
101
|
+
if (entry.type !== 'blob' || (entry.mode !== '100644' && entry.mode !== '100755')) {
|
|
102
|
+
throw new Error(`Pier task outcome contains a non-regular entry: ${entry.path}`)
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
path: safeGitPath(entry.path),
|
|
106
|
+
mode: entry.mode === '100755' ? (0o755 as const) : (0o644 as const),
|
|
107
|
+
bytes: (
|
|
108
|
+
await git(
|
|
109
|
+
repositoryRoot,
|
|
110
|
+
['cat-file', 'blob', entry.object],
|
|
111
|
+
undefined,
|
|
112
|
+
environment,
|
|
113
|
+
input.signal,
|
|
114
|
+
)
|
|
115
|
+
).stdout,
|
|
116
|
+
}
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
// The executor outcome must still return exact bytes; the runtime verifies
|
|
120
|
+
// and persists its final task references after this capture completes.
|
|
121
|
+
const captured = await captureAgentCandidateWorkspaceFiles(files, {
|
|
122
|
+
limits: { maxEmbeddedArtifactBytes: 512 * 1024 * 1024 },
|
|
123
|
+
})
|
|
124
|
+
return {
|
|
125
|
+
kind: 'workspace',
|
|
126
|
+
resultTree,
|
|
127
|
+
afterState: captured.snapshot.material,
|
|
128
|
+
archive: captured.archive,
|
|
129
|
+
gitDiff: patch,
|
|
130
|
+
}
|
|
131
|
+
} finally {
|
|
132
|
+
await rm(temporary, { recursive: true, force: true })
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseTreeEntries(bytes: Uint8Array): Array<{
|
|
137
|
+
readonly mode: string
|
|
138
|
+
readonly type: string
|
|
139
|
+
readonly object: string
|
|
140
|
+
readonly path: string
|
|
141
|
+
}> {
|
|
142
|
+
const raw = Buffer.from(bytes)
|
|
143
|
+
const decoded = raw.toString('utf8')
|
|
144
|
+
if (!Buffer.from(decoded, 'utf8').equals(raw)) {
|
|
145
|
+
throw new Error('Pier task outcome contains a non-UTF-8 path')
|
|
146
|
+
}
|
|
147
|
+
return decoded
|
|
148
|
+
.split('\0')
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.map((row) => {
|
|
151
|
+
const tab = row.indexOf('\t')
|
|
152
|
+
const [mode, type, object] = row.slice(0, tab).split(' ')
|
|
153
|
+
const path = row.slice(tab + 1)
|
|
154
|
+
if (tab < 1 || !mode || !type || !object || !path) {
|
|
155
|
+
throw new Error('Pier task outcome contains a malformed Git entry')
|
|
156
|
+
}
|
|
157
|
+
return { mode, type, object, path }
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function safeGitPath(path: string): string {
|
|
162
|
+
const parts = path.split('/')
|
|
163
|
+
if (
|
|
164
|
+
!path ||
|
|
165
|
+
path.startsWith('/') ||
|
|
166
|
+
path.includes('\\') ||
|
|
167
|
+
path.includes('\0') ||
|
|
168
|
+
parts.some((part) => !part || part === '.' || part === '..') ||
|
|
169
|
+
parts[0]?.toLowerCase() === '.git'
|
|
170
|
+
) {
|
|
171
|
+
throw new Error(`Pier task outcome contains an unsafe Git path: ${path}`)
|
|
172
|
+
}
|
|
173
|
+
return path
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function git(
|
|
177
|
+
repositoryRoot: string,
|
|
178
|
+
args: readonly string[],
|
|
179
|
+
input: Uint8Array | undefined,
|
|
180
|
+
extraEnvironment: Readonly<Record<string, string>>,
|
|
181
|
+
signal: AbortSignal | undefined,
|
|
182
|
+
): Promise<GitResult> {
|
|
183
|
+
signal?.throwIfAborted()
|
|
184
|
+
const environment = Object.fromEntries(
|
|
185
|
+
Object.entries(process.env).filter(([name]) => !name.startsWith('GIT_')),
|
|
186
|
+
) as Record<string, string>
|
|
187
|
+
Object.assign(environment, {
|
|
188
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
189
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
190
|
+
GIT_CONFIG_SYSTEM: '/dev/null',
|
|
191
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
192
|
+
GIT_NO_REPLACE_OBJECTS: '1',
|
|
193
|
+
LC_ALL: 'C',
|
|
194
|
+
...extraEnvironment,
|
|
195
|
+
})
|
|
196
|
+
return await new Promise((resolveResult, reject) => {
|
|
197
|
+
const child = spawn(
|
|
198
|
+
'git',
|
|
199
|
+
[
|
|
200
|
+
'-c',
|
|
201
|
+
'core.hooksPath=/dev/null',
|
|
202
|
+
'-c',
|
|
203
|
+
'protocol.file.allow=never',
|
|
204
|
+
'-C',
|
|
205
|
+
repositoryRoot,
|
|
206
|
+
...args,
|
|
207
|
+
],
|
|
208
|
+
{ env: environment, stdio: ['pipe', 'pipe', 'pipe'], signal },
|
|
209
|
+
)
|
|
210
|
+
const stdout: Buffer[] = []
|
|
211
|
+
const stderr: Buffer[] = []
|
|
212
|
+
let outputBytes = 0
|
|
213
|
+
const collect = (target: Buffer[]) => (chunk: Buffer) => {
|
|
214
|
+
outputBytes += chunk.byteLength
|
|
215
|
+
if (outputBytes > 64 * 1024 * 1024) {
|
|
216
|
+
child.kill('SIGKILL')
|
|
217
|
+
reject(new Error(`git ${args[0] ?? '<command>'} exceeded the output limit`))
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
target.push(Buffer.from(chunk))
|
|
221
|
+
}
|
|
222
|
+
child.stdout.on('data', collect(stdout))
|
|
223
|
+
child.stderr.on('data', collect(stderr))
|
|
224
|
+
child.once('error', reject)
|
|
225
|
+
child.once('close', (code, childSignal) => {
|
|
226
|
+
const out = Buffer.concat(stdout)
|
|
227
|
+
const err = Buffer.concat(stderr)
|
|
228
|
+
if (code !== 0) {
|
|
229
|
+
reject(
|
|
230
|
+
new Error(
|
|
231
|
+
`git ${args[0] ?? '<command>'} failed (${childSignal ?? code}): ${err.toString('utf8').trim()}`,
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
resolveResult({ stdout: out, stderr: err })
|
|
237
|
+
})
|
|
238
|
+
child.stdin.end(input)
|
|
239
|
+
})
|
|
240
|
+
}
|