@tangle-network/agent-bench 0.4.0 → 0.4.1
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 +6 -0
- package/dist/adapters.js +23 -23
- package/dist/benchmarks/_harness.d.ts +4 -1
- package/dist/benchmarks/_harness.js +3 -1
- package/dist/benchmarks/aec-bench.js +2 -2
- package/dist/benchmarks/agentbench.js +2 -2
- package/dist/benchmarks/appworld.js +2 -2
- package/dist/benchmarks/bfcl.js +2 -2
- package/dist/benchmarks/commit0.js +2 -2
- package/dist/benchmarks/crag.js +2 -2
- package/dist/benchmarks/dabstep.js +2 -2
- package/dist/benchmarks/enterpriseops-gym.js +2 -2
- package/dist/benchmarks/finresearchbench.js +2 -2
- package/dist/benchmarks/nomiracl.js +2 -2
- package/dist/benchmarks/open-rag-bench.js +2 -2
- package/dist/benchmarks/programbench.js +2 -2
- package/dist/benchmarks/ragbench.js +2 -2
- package/dist/benchmarks/swe-bench.js +2 -2
- package/dist/benchmarks/t2-ragbench.js +2 -2
- package/dist/benchmarks/tau-bench-shared.js +2 -2
- package/dist/benchmarks/tau2-bench.js +3 -3
- package/dist/benchmarks/tau3-banking.js +3 -3
- package/dist/benchmarks/terminal-bench.js +2 -2
- package/dist/benchmarks/toollm.js +2 -2
- package/dist/benchmarks/webarena-verified.js +2 -2
- package/dist/{chunk-YSMEKBTD.js → chunk-5FEQDSCT.js} +2 -2
- package/dist/{chunk-PB64GYIG.js → chunk-67ACKDCX.js} +2 -2
- package/dist/{chunk-NQG5XDSB.js → chunk-7FKBWOQT.js} +2 -2
- package/dist/{chunk-K3BQGZCT.js → chunk-BEN6IF2X.js} +2 -2
- package/dist/{chunk-SHYIRB7I.js → chunk-BZY5QARD.js} +2 -2
- package/dist/{chunk-KP5KD6EN.js → chunk-CLIKAXKH.js} +2 -2
- package/dist/{chunk-XKEFIFIC.js → chunk-CWIOBFSP.js} +2 -2
- package/dist/{chunk-IA2FBTWC.js → chunk-CXDUTWQE.js} +2 -2
- package/dist/{chunk-RH5F53JT.js → chunk-DWALFME7.js} +2 -2
- package/dist/{chunk-V7AEBY6U.js → chunk-EEOC6QPJ.js} +21 -21
- package/dist/{chunk-WSKWVEQB.js → chunk-EIETHPD5.js} +7 -3
- package/dist/chunk-EIETHPD5.js.map +1 -0
- package/dist/{chunk-SHM6MRRF.js → chunk-GC2EPS6L.js} +2 -2
- package/dist/{chunk-IZ5M6OAC.js → chunk-GCHL6XPM.js} +2 -2
- package/dist/{chunk-IFVINJ4B.js → chunk-HQ5HCCKF.js} +2 -2
- package/dist/{chunk-7GRVHU22.js → chunk-HVW25KSX.js} +2 -2
- package/dist/{chunk-3U5TXJZS.js → chunk-J6BU3NTM.js} +2 -2
- package/dist/{chunk-XYA4XSNU.js → chunk-JSQOUKXS.js} +4 -3
- package/dist/chunk-JSQOUKXS.js.map +1 -0
- package/dist/{chunk-SVR2LKYI.js → chunk-NRMGT25X.js} +2 -2
- package/dist/{chunk-MQMRLGOG.js → chunk-QZZEAHWJ.js} +2 -2
- package/dist/{chunk-RCYQEFNX.js → chunk-WG7TM7UV.js} +3 -3
- package/dist/{chunk-HWST3SED.js → chunk-XXFF3RRD.js} +2 -2
- package/dist/{chunk-SFLA7OH3.js → chunk-ZFNOM7WR.js} +3 -3
- package/dist/{chunk-Z4TZ76N7.js → chunk-ZNCCYTFG.js} +2 -2
- package/dist/index.js +23 -23
- package/package.json +1 -1
- package/pier_agents/candidate_contract.py +238 -24
- package/pier_agents/tangle_candidate.py +75 -5
- package/scripts/verify-packed-consumer.mjs +65 -1
- package/scripts/verify-pier-agent.mts +6 -4
- package/src/benchmarks/_harness.test.mts +16 -1
- package/src/benchmarks/_harness.ts +9 -2
- package/src/benchmarks/terminal-bench.ts +2 -1
- package/dist/chunk-WSKWVEQB.js.map +0 -1
- package/dist/chunk-XYA4XSNU.js.map +0 -1
- /package/dist/{chunk-YSMEKBTD.js.map → chunk-5FEQDSCT.js.map} +0 -0
- /package/dist/{chunk-PB64GYIG.js.map → chunk-67ACKDCX.js.map} +0 -0
- /package/dist/{chunk-NQG5XDSB.js.map → chunk-7FKBWOQT.js.map} +0 -0
- /package/dist/{chunk-K3BQGZCT.js.map → chunk-BEN6IF2X.js.map} +0 -0
- /package/dist/{chunk-SHYIRB7I.js.map → chunk-BZY5QARD.js.map} +0 -0
- /package/dist/{chunk-KP5KD6EN.js.map → chunk-CLIKAXKH.js.map} +0 -0
- /package/dist/{chunk-XKEFIFIC.js.map → chunk-CWIOBFSP.js.map} +0 -0
- /package/dist/{chunk-IA2FBTWC.js.map → chunk-CXDUTWQE.js.map} +0 -0
- /package/dist/{chunk-RH5F53JT.js.map → chunk-DWALFME7.js.map} +0 -0
- /package/dist/{chunk-V7AEBY6U.js.map → chunk-EEOC6QPJ.js.map} +0 -0
- /package/dist/{chunk-SHM6MRRF.js.map → chunk-GC2EPS6L.js.map} +0 -0
- /package/dist/{chunk-IZ5M6OAC.js.map → chunk-GCHL6XPM.js.map} +0 -0
- /package/dist/{chunk-IFVINJ4B.js.map → chunk-HQ5HCCKF.js.map} +0 -0
- /package/dist/{chunk-7GRVHU22.js.map → chunk-HVW25KSX.js.map} +0 -0
- /package/dist/{chunk-3U5TXJZS.js.map → chunk-J6BU3NTM.js.map} +0 -0
- /package/dist/{chunk-SVR2LKYI.js.map → chunk-NRMGT25X.js.map} +0 -0
- /package/dist/{chunk-MQMRLGOG.js.map → chunk-QZZEAHWJ.js.map} +0 -0
- /package/dist/{chunk-RCYQEFNX.js.map → chunk-WG7TM7UV.js.map} +0 -0
- /package/dist/{chunk-HWST3SED.js.map → chunk-XXFF3RRD.js.map} +0 -0
- /package/dist/{chunk-SFLA7OH3.js.map → chunk-ZFNOM7WR.js.map} +0 -0
- /package/dist/{chunk-Z4TZ76N7.js.map → chunk-ZNCCYTFG.js.map} +0 -0
|
@@ -34,8 +34,13 @@ _RESERVED_PATH_PARTS = {".git", ".sidecar"}
|
|
|
34
34
|
_PLAN_KIND = "agent-candidate-execution-plan-material"
|
|
35
35
|
_RECEIPT_KIND = "agent-candidate-materialization"
|
|
36
36
|
_PROFILE_PLAN_KIND = "agent-profile-workspace-plan"
|
|
37
|
+
_PROFILE_ACTIVATION_KIND = "agent-candidate-profile-activation"
|
|
37
38
|
_EXECUTION_EVIDENCE_KIND = "agent-candidate-execution-plan"
|
|
39
|
+
_RUN_CELL_KIND = "agent-candidate-run-cell"
|
|
40
|
+
_BENCHMARK_SUITE_KIND = "agent-candidate-benchmark-suite"
|
|
41
|
+
_BENCHMARK_TASK_KIND = "agent-candidate-benchmark-task"
|
|
38
42
|
_STDIN_TASK_PATH = "/tangle/input/stdin.txt"
|
|
43
|
+
_MAX_SAFE_INTEGER = 9_007_199_254_740_991
|
|
39
44
|
|
|
40
45
|
|
|
41
46
|
class CandidateContractError(ValueError):
|
|
@@ -130,6 +135,30 @@ def _integer(value: Any, label: str, *, minimum: int = 0) -> int:
|
|
|
130
135
|
return value
|
|
131
136
|
|
|
132
137
|
|
|
138
|
+
def _safe_integer(value: Any, label: str) -> int:
|
|
139
|
+
if (
|
|
140
|
+
isinstance(value, bool)
|
|
141
|
+
or not isinstance(value, int)
|
|
142
|
+
or value < -_MAX_SAFE_INTEGER
|
|
143
|
+
or value > _MAX_SAFE_INTEGER
|
|
144
|
+
):
|
|
145
|
+
raise CandidateContractError(f"{label} must be a safe integer")
|
|
146
|
+
return value
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _canonical_bytes(value: Any, label: str) -> bytes:
|
|
150
|
+
try:
|
|
151
|
+
return json.dumps(
|
|
152
|
+
value,
|
|
153
|
+
ensure_ascii=False,
|
|
154
|
+
allow_nan=False,
|
|
155
|
+
separators=(",", ":"),
|
|
156
|
+
sort_keys=True,
|
|
157
|
+
).encode("utf-8")
|
|
158
|
+
except (TypeError, ValueError, UnicodeEncodeError) as exc:
|
|
159
|
+
raise CandidateContractError(f"{label} is not canonical JSON") from exc
|
|
160
|
+
|
|
161
|
+
|
|
133
162
|
def _number(value: Any, label: str, *, minimum: float = 0) -> float:
|
|
134
163
|
if (
|
|
135
164
|
isinstance(value, bool)
|
|
@@ -246,6 +275,21 @@ def _embedded_bytes(value: Any, label: str) -> bytes:
|
|
|
246
275
|
return raw
|
|
247
276
|
|
|
248
277
|
|
|
278
|
+
def _embedded_contract(
|
|
279
|
+
value: Any, label: str, *, kind: str
|
|
280
|
+
) -> tuple[dict[str, Any], bytes, str]:
|
|
281
|
+
evidence = _object(value, label)
|
|
282
|
+
digest = _digest(evidence.get("digest"), f"{label}.digest")
|
|
283
|
+
raw = _embedded_bytes(evidence.get("material"), f"{label}.material")
|
|
284
|
+
if sha256_bytes(raw) != digest:
|
|
285
|
+
raise CandidateContractError(f"{label} material does not match its digest")
|
|
286
|
+
try:
|
|
287
|
+
material = json.loads(raw)
|
|
288
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
289
|
+
raise CandidateContractError(f"{label} material is not UTF-8 JSON") from exc
|
|
290
|
+
return _contract_object(material, f"{label}.material", kind=kind), raw, digest
|
|
291
|
+
|
|
292
|
+
|
|
249
293
|
def _workspace_files(value: Any, label: str) -> tuple[WorkspaceFile, ...]:
|
|
250
294
|
snapshot = _contract_object(
|
|
251
295
|
value, label, kind="agent-candidate-workspace-snapshot"
|
|
@@ -318,11 +362,10 @@ def _profile_files(value: Any) -> tuple[ProfileFile, ...]:
|
|
|
318
362
|
return tuple(files)
|
|
319
363
|
|
|
320
364
|
|
|
321
|
-
def _instruction(value: Any) -> InstructionEvidence:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
delivery = _object(obj.get("delivery"), "task.instruction.delivery")
|
|
365
|
+
def _instruction(value: Any, delivery_value: Any) -> InstructionEvidence:
|
|
366
|
+
text = _string(value, "benchmark task instruction")
|
|
367
|
+
raw = text.encode("utf-8")
|
|
368
|
+
delivery = _object(delivery_value, "plan.instructionDelivery")
|
|
326
369
|
kind = delivery.get("kind")
|
|
327
370
|
if kind == "argv-append" or kind == "stdin-utf8":
|
|
328
371
|
if set(delivery) != {"kind"}:
|
|
@@ -342,10 +385,8 @@ def _instruction(value: Any) -> InstructionEvidence:
|
|
|
342
385
|
else:
|
|
343
386
|
raise CandidateContractError("unsupported task instruction delivery")
|
|
344
387
|
return InstructionEvidence(
|
|
345
|
-
sha256=
|
|
346
|
-
byte_length=
|
|
347
|
-
obj.get("byteLength"), "task.instruction.byteLength", minimum=1
|
|
348
|
-
),
|
|
388
|
+
sha256=sha256_bytes(raw),
|
|
389
|
+
byte_length=len(raw),
|
|
349
390
|
delivery_kind=kind,
|
|
350
391
|
delivery_env=env,
|
|
351
392
|
delivery_path=path,
|
|
@@ -400,12 +441,87 @@ def load_prepared_candidate_contract(
|
|
|
400
441
|
"receipt execution-plan artifact differs from the executed bytes"
|
|
401
442
|
)
|
|
402
443
|
|
|
403
|
-
|
|
444
|
+
run_cell = _contract_object(
|
|
445
|
+
plan.get("runCell"), "plan.runCell", kind=_RUN_CELL_KIND
|
|
446
|
+
)
|
|
447
|
+
_digest(run_cell.get("experimentDigest"), "plan.runCell.experimentDigest")
|
|
448
|
+
run_cell_digest = _digest(run_cell.get("digest"), "plan.runCell.digest")
|
|
449
|
+
if run_cell.get("arm") not in {"baseline", "candidate"}:
|
|
450
|
+
raise CandidateContractError("plan.runCell.arm is unsupported")
|
|
451
|
+
run_cell_seed = _safe_integer(run_cell.get("seed"), "plan.runCell.seed")
|
|
452
|
+
if sha256_bytes(
|
|
453
|
+
_canonical_bytes(
|
|
454
|
+
{key: value for key, value in run_cell.items() if key != "digest"},
|
|
455
|
+
"plan.runCell",
|
|
456
|
+
)
|
|
457
|
+
) != run_cell_digest:
|
|
458
|
+
raise CandidateContractError("plan.runCell digest does not match")
|
|
459
|
+
bundle_digest = _digest(
|
|
460
|
+
run_cell.get("bundleDigest"), "plan.runCell.bundleDigest"
|
|
461
|
+
)
|
|
404
462
|
if receipt.get("bundleDigest") != bundle_digest:
|
|
405
463
|
raise CandidateContractError("receipt and plan bundle digests differ")
|
|
406
464
|
execution_id = _string(plan.get("executionId"), "plan.executionId")
|
|
407
465
|
|
|
408
|
-
|
|
466
|
+
benchmark = _object(receipt.get("benchmark"), "receipt.benchmark")
|
|
467
|
+
suite, _, suite_digest = _embedded_contract(
|
|
468
|
+
benchmark.get("suite"),
|
|
469
|
+
"receipt.benchmark.suite",
|
|
470
|
+
kind=_BENCHMARK_SUITE_KIND,
|
|
471
|
+
)
|
|
472
|
+
task, _, task_digest = _embedded_contract(
|
|
473
|
+
benchmark.get("task"),
|
|
474
|
+
"receipt.benchmark.task",
|
|
475
|
+
kind=_BENCHMARK_TASK_KIND,
|
|
476
|
+
)
|
|
477
|
+
if (
|
|
478
|
+
run_cell.get("suiteDigest") != suite_digest
|
|
479
|
+
or run_cell.get("taskDigest") != task_digest
|
|
480
|
+
):
|
|
481
|
+
raise CandidateContractError(
|
|
482
|
+
"run cell does not bind the receipt benchmark suite and task"
|
|
483
|
+
)
|
|
484
|
+
task_index = _integer(run_cell.get("taskIndex"), "plan.runCell.taskIndex")
|
|
485
|
+
repetition = _integer(run_cell.get("repetition"), "plan.runCell.repetition")
|
|
486
|
+
if suite.get("digestAlgorithm") != "rfc8785-sha256":
|
|
487
|
+
raise CandidateContractError(
|
|
488
|
+
"benchmark suite digestAlgorithm must equal rfc8785-sha256"
|
|
489
|
+
)
|
|
490
|
+
reps = _integer(suite.get("reps"), "benchmark suite reps", minimum=1)
|
|
491
|
+
task_digests = suite.get("taskDigests")
|
|
492
|
+
seeds = suite.get("seeds")
|
|
493
|
+
if (
|
|
494
|
+
not isinstance(task_digests, list)
|
|
495
|
+
or not task_digests
|
|
496
|
+
or len(set(task_digests)) != len(task_digests)
|
|
497
|
+
):
|
|
498
|
+
raise CandidateContractError(
|
|
499
|
+
"benchmark suite taskDigests must be a non-empty unique array"
|
|
500
|
+
)
|
|
501
|
+
for index, value in enumerate(task_digests):
|
|
502
|
+
_digest(value, f"benchmark suite taskDigests[{index}]")
|
|
503
|
+
if (
|
|
504
|
+
not isinstance(seeds, list)
|
|
505
|
+
or len(seeds) != len(task_digests) * reps
|
|
506
|
+
):
|
|
507
|
+
raise CandidateContractError(
|
|
508
|
+
"benchmark suite must provide one seed per task repetition"
|
|
509
|
+
)
|
|
510
|
+
for index, value in enumerate(seeds):
|
|
511
|
+
_safe_integer(value, f"benchmark suite seeds[{index}]")
|
|
512
|
+
if (
|
|
513
|
+
task_index >= len(task_digests)
|
|
514
|
+
or task_digests[task_index] != task_digest
|
|
515
|
+
or repetition >= reps
|
|
516
|
+
or seeds[task_index * reps + repetition] != run_cell_seed
|
|
517
|
+
):
|
|
518
|
+
raise CandidateContractError(
|
|
519
|
+
"run cell coordinates do not match the receipt benchmark suite"
|
|
520
|
+
)
|
|
521
|
+
if task.get("digestAlgorithm") != "rfc8785-sha256":
|
|
522
|
+
raise CandidateContractError(
|
|
523
|
+
"benchmark task digestAlgorithm must equal rfc8785-sha256"
|
|
524
|
+
)
|
|
409
525
|
outcome = _object(task.get("outcome"), "plan.task.outcome")
|
|
410
526
|
if outcome.get("kind") != "workspace":
|
|
411
527
|
raise CandidateContractError("Pier requires a workspace task outcome")
|
|
@@ -418,7 +534,9 @@ def load_prepared_candidate_contract(
|
|
|
418
534
|
)
|
|
419
535
|
if len(base_commit) != len(base_tree):
|
|
420
536
|
raise CandidateContractError("task Git objects use different hash formats")
|
|
421
|
-
instruction = _instruction(
|
|
537
|
+
instruction = _instruction(
|
|
538
|
+
task.get("instruction"), plan.get("instructionDelivery")
|
|
539
|
+
)
|
|
422
540
|
task_files = _workspace_files(task.get("workspace"), "plan.task.workspace")
|
|
423
541
|
if not task_files:
|
|
424
542
|
raise CandidateContractError("task workspace cannot be empty")
|
|
@@ -480,16 +598,40 @@ def load_prepared_candidate_contract(
|
|
|
480
598
|
if receipt.get("candidateWorkspace") != candidate_snapshot:
|
|
481
599
|
raise CandidateContractError("receipt and plan candidate workspaces differ")
|
|
482
600
|
|
|
601
|
+
profile_activation = _contract_object(
|
|
602
|
+
receipt.get("profileActivation"),
|
|
603
|
+
"receipt.profileActivation",
|
|
604
|
+
kind=_PROFILE_ACTIVATION_KIND,
|
|
605
|
+
)
|
|
606
|
+
profile_activation_digest = _digest(
|
|
607
|
+
profile_activation.get("digest"),
|
|
608
|
+
"receipt.profileActivation.digest",
|
|
609
|
+
)
|
|
610
|
+
if sha256_bytes(
|
|
611
|
+
_canonical_bytes(
|
|
612
|
+
{
|
|
613
|
+
key: value
|
|
614
|
+
for key, value in profile_activation.items()
|
|
615
|
+
if key != "digest"
|
|
616
|
+
},
|
|
617
|
+
"receipt.profileActivation",
|
|
618
|
+
)
|
|
619
|
+
) != profile_activation_digest:
|
|
620
|
+
raise CandidateContractError(
|
|
621
|
+
"profile activation digest does not match"
|
|
622
|
+
)
|
|
483
623
|
profile_evidence = _contract_object(
|
|
484
|
-
|
|
485
|
-
"receipt.profilePlan",
|
|
624
|
+
profile_activation.get("profilePlan"),
|
|
625
|
+
"receipt.profileActivation.profilePlan",
|
|
486
626
|
kind=_PROFILE_PLAN_KIND,
|
|
487
627
|
)
|
|
488
628
|
profile_digest = _digest(
|
|
489
|
-
profile_evidence.get("digest"),
|
|
629
|
+
profile_evidence.get("digest"),
|
|
630
|
+
"receipt.profileActivation.profilePlan.digest",
|
|
490
631
|
)
|
|
491
632
|
profile_raw = _embedded_bytes(
|
|
492
|
-
profile_evidence.get("artifact"),
|
|
633
|
+
profile_evidence.get("artifact"),
|
|
634
|
+
"receipt.profileActivation.profilePlan.artifact",
|
|
493
635
|
)
|
|
494
636
|
if sha256_bytes(profile_raw) != profile_digest:
|
|
495
637
|
raise CandidateContractError("profile artifact does not match its digest")
|
|
@@ -498,11 +640,50 @@ def load_prepared_candidate_contract(
|
|
|
498
640
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
499
641
|
raise CandidateContractError("profile artifact is not UTF-8 JSON") from exc
|
|
500
642
|
profile_material = _contract_object(
|
|
501
|
-
profile_evidence.get("material"),
|
|
643
|
+
profile_evidence.get("material"),
|
|
644
|
+
"receipt.profileActivation.profilePlan.material",
|
|
502
645
|
)
|
|
503
646
|
if profile_artifact_material != profile_material:
|
|
504
647
|
raise CandidateContractError("profile artifact and material differ")
|
|
648
|
+
if profile_material.get("harness") != plan.get("harness"):
|
|
649
|
+
raise CandidateContractError(
|
|
650
|
+
"profile plan harness differs from the execution plan"
|
|
651
|
+
)
|
|
505
652
|
profile_files = _profile_files(profile_material)
|
|
653
|
+
activation_files = profile_activation.get("files")
|
|
654
|
+
if not isinstance(activation_files, list):
|
|
655
|
+
raise CandidateContractError(
|
|
656
|
+
"receipt.profileActivation.files must be an array"
|
|
657
|
+
)
|
|
658
|
+
activated: list[ProfileFile] = []
|
|
659
|
+
for index, value in enumerate(activation_files):
|
|
660
|
+
item = _object(value, f"receipt.profileActivation.files[{index}]")
|
|
661
|
+
path = _safe_relative(
|
|
662
|
+
item.get("path"), f"receipt.profileActivation.files[{index}].path"
|
|
663
|
+
)
|
|
664
|
+
mode = _integer(
|
|
665
|
+
item.get("mode"), f"receipt.profileActivation.files[{index}].mode"
|
|
666
|
+
)
|
|
667
|
+
content = item.get("content")
|
|
668
|
+
if (
|
|
669
|
+
mode > 0o777
|
|
670
|
+
or not isinstance(content, str)
|
|
671
|
+
or "\0" in content
|
|
672
|
+
):
|
|
673
|
+
raise CandidateContractError(
|
|
674
|
+
f"receipt.profileActivation.files[{index}] is invalid"
|
|
675
|
+
)
|
|
676
|
+
activated.append(
|
|
677
|
+
ProfileFile(
|
|
678
|
+
path=path,
|
|
679
|
+
mode=mode,
|
|
680
|
+
sha256=sha256_bytes(content.encode("utf-8")),
|
|
681
|
+
)
|
|
682
|
+
)
|
|
683
|
+
if activated != list(profile_files):
|
|
684
|
+
raise CandidateContractError(
|
|
685
|
+
"profile activation files differ from the signed profile plan"
|
|
686
|
+
)
|
|
506
687
|
profile_application = _object(plan.get("profile"), "plan.profile")
|
|
507
688
|
if profile_application.get("planDigest") != profile_digest:
|
|
508
689
|
raise CandidateContractError("plan does not bind the profile digest")
|
|
@@ -563,6 +744,10 @@ def load_prepared_candidate_contract(
|
|
|
563
744
|
raise CandidateContractError("launch cwd names an unavailable workspace")
|
|
564
745
|
cwd_path = _safe_relative(cwd.get("path"), "plan.launch.cwd.path", allow_dot=True)
|
|
565
746
|
limits = _object(plan.get("limits"), "plan.limits")
|
|
747
|
+
if limits != task.get("limits"):
|
|
748
|
+
raise CandidateContractError(
|
|
749
|
+
"execution plan limits differ from the benchmark task"
|
|
750
|
+
)
|
|
566
751
|
timeout_ms = _integer(limits.get("timeoutMs"), "plan.limits.timeoutMs", minimum=1)
|
|
567
752
|
_integer(limits.get("maxSteps"), "plan.limits.maxSteps", minimum=1)
|
|
568
753
|
max_model_calls = _integer(
|
|
@@ -571,11 +756,27 @@ def load_prepared_candidate_contract(
|
|
|
571
756
|
_integer(limits.get("maxInputTokens"), "plan.limits.maxInputTokens")
|
|
572
757
|
_integer(limits.get("maxOutputTokens"), "plan.limits.maxOutputTokens")
|
|
573
758
|
_number(limits.get("maxCostUsd"), "plan.limits.maxCostUsd")
|
|
574
|
-
attempt = _object(
|
|
575
|
-
_integer(
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
759
|
+
attempt = _object(task.get("attempt"), "benchmark task attempt")
|
|
760
|
+
attempt_number = _integer(
|
|
761
|
+
run_cell.get("attempt"), "plan.runCell.attempt", minimum=1
|
|
762
|
+
)
|
|
763
|
+
max_attempts = _integer(
|
|
764
|
+
attempt.get("maxAttempts"), "benchmark task attempt.maxAttempts", minimum=1
|
|
765
|
+
)
|
|
766
|
+
if attempt_number > max_attempts:
|
|
767
|
+
raise CandidateContractError(
|
|
768
|
+
"run cell attempt exceeds the benchmark task maximum"
|
|
769
|
+
)
|
|
770
|
+
retry_policy = attempt.get("retryPolicy")
|
|
771
|
+
if retry_policy not in {
|
|
772
|
+
"none",
|
|
773
|
+
"pre-model-infrastructure-only",
|
|
774
|
+
}:
|
|
775
|
+
raise CandidateContractError("benchmark task retry policy is unsupported")
|
|
776
|
+
if retry_policy == "none" and max_attempts != 1:
|
|
777
|
+
raise CandidateContractError(
|
|
778
|
+
"a no-retry benchmark task must allow exactly one attempt"
|
|
779
|
+
)
|
|
579
780
|
container = _object(plan.get("container"), "plan.container")
|
|
580
781
|
if receipt.get("container") != container:
|
|
581
782
|
raise CandidateContractError("receipt and plan container identities differ")
|
|
@@ -586,11 +787,21 @@ def load_prepared_candidate_contract(
|
|
|
586
787
|
raise CandidateContractError(
|
|
587
788
|
"container image must be an unpinned OCI reference without credentials"
|
|
588
789
|
)
|
|
589
|
-
|
|
790
|
+
container_source = container.get("source")
|
|
791
|
+
if container_source not in {
|
|
590
792
|
"pinned-container",
|
|
591
793
|
"evaluator-task-container",
|
|
592
794
|
}:
|
|
593
795
|
raise CandidateContractError("plan container source is unsupported")
|
|
796
|
+
task_container = task.get("evaluatorTaskContainer")
|
|
797
|
+
if container_source == "evaluator-task-container" and task_container != container:
|
|
798
|
+
raise CandidateContractError(
|
|
799
|
+
"execution plan container differs from the benchmark task container"
|
|
800
|
+
)
|
|
801
|
+
if container_source == "pinned-container" and task_container is not None:
|
|
802
|
+
raise CandidateContractError(
|
|
803
|
+
"pinned execution cannot override a benchmark task container"
|
|
804
|
+
)
|
|
594
805
|
platform = _object(container.get("platform"), "plan.container.platform")
|
|
595
806
|
if platform.get("os") != "linux" or platform.get("architecture") not in {
|
|
596
807
|
"amd64",
|
|
@@ -605,7 +816,10 @@ def load_prepared_candidate_contract(
|
|
|
605
816
|
_string(resolved_model.get("provider"), "plan.model.resolved.provider")
|
|
606
817
|
_string(resolved_model.get("model"), "plan.model.resolved.model")
|
|
607
818
|
_string(resolved_model.get("snapshot"), "plan.model.resolved.snapshot")
|
|
608
|
-
if
|
|
819
|
+
if (
|
|
820
|
+
receipt.get("resolvedModel") != resolved_model
|
|
821
|
+
or task.get("model") != resolved_model
|
|
822
|
+
):
|
|
609
823
|
raise CandidateContractError("receipt and plan resolved models differ")
|
|
610
824
|
model_access = _object(model.get("access"), "plan.model.access")
|
|
611
825
|
if set(model_access) != {"kind", "grantDigest", "network"}:
|
|
@@ -55,6 +55,7 @@ _CANDIDATE_HOME = "/tangle/home"
|
|
|
55
55
|
_CANDIDATE_TMP = "/tangle/tmp"
|
|
56
56
|
_CONTROL_ROOT = "/tangle/control"
|
|
57
57
|
_FIXED_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
58
|
+
_FIXED_PATH_ENTRIES = frozenset(_FIXED_PATH.split(":"))
|
|
58
59
|
_RESERVED_PROCESS_ENV = {
|
|
59
60
|
"GIT_CONFIG_COUNT",
|
|
60
61
|
"GIT_CONFIG_KEY_0",
|
|
@@ -124,6 +125,33 @@ def _trace_env(
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
|
|
128
|
+
def _evaluator_git(root: str) -> str:
|
|
129
|
+
return shlex.join(
|
|
130
|
+
[
|
|
131
|
+
"git",
|
|
132
|
+
"-c",
|
|
133
|
+
"core.hooksPath=/dev/null",
|
|
134
|
+
"-c",
|
|
135
|
+
"core.fsmonitor=false",
|
|
136
|
+
"-c",
|
|
137
|
+
"core.untrackedCache=false",
|
|
138
|
+
"-c",
|
|
139
|
+
"protocol.file.allow=never",
|
|
140
|
+
"-C",
|
|
141
|
+
root,
|
|
142
|
+
]
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _evaluator_git_config(repository_base_commit: str) -> bytes:
|
|
147
|
+
if len(repository_base_commit) == 64:
|
|
148
|
+
return (
|
|
149
|
+
b"[core]\n\trepositoryformatversion = 1\n"
|
|
150
|
+
b"[extensions]\n\tobjectformat = sha256\n"
|
|
151
|
+
)
|
|
152
|
+
return b"[core]\n\trepositoryformatversion = 0\n"
|
|
153
|
+
|
|
154
|
+
|
|
127
155
|
class TangleCandidateAgent(BaseAgent):
|
|
128
156
|
"""Pier adapter for a branded ``PreparedAgentCandidateExecution``."""
|
|
129
157
|
|
|
@@ -241,12 +269,25 @@ class TangleCandidateAgent(BaseAgent):
|
|
|
241
269
|
) -> dict[str, str]:
|
|
242
270
|
protected: dict[str, str] = {}
|
|
243
271
|
public_names = set(self._contract.env)
|
|
244
|
-
reserved_public = sorted(
|
|
272
|
+
reserved_public = sorted(
|
|
273
|
+
(public_names & _RESERVED_PROCESS_ENV) - {"PATH"}
|
|
274
|
+
)
|
|
245
275
|
if reserved_public:
|
|
246
276
|
raise PierCandidateError(
|
|
247
277
|
"signed public env collides with evaluator Git safety fields: "
|
|
248
278
|
+ ", ".join(reserved_public)
|
|
249
279
|
)
|
|
280
|
+
public_path = self._contract.env.get("PATH")
|
|
281
|
+
if public_path is not None:
|
|
282
|
+
entries = public_path.split(":")
|
|
283
|
+
if (
|
|
284
|
+
not entries
|
|
285
|
+
or len(entries) != len(set(entries))
|
|
286
|
+
or any(entry not in _FIXED_PATH_ENTRIES for entry in entries)
|
|
287
|
+
):
|
|
288
|
+
raise PierCandidateError(
|
|
289
|
+
"signed public PATH contains an empty, duplicate, or untrusted directory"
|
|
290
|
+
)
|
|
250
291
|
expected_trace = _trace_env(self._contract, trace_run_id)
|
|
251
292
|
for name, expected in expected_trace.items():
|
|
252
293
|
if values.get(name) != expected:
|
|
@@ -455,7 +496,7 @@ class TangleCandidateAgent(BaseAgent):
|
|
|
455
496
|
return ()
|
|
456
497
|
originals: list[OriginalProfileFile] = []
|
|
457
498
|
root = self._contract.task_root
|
|
458
|
-
git =
|
|
499
|
+
git = _evaluator_git(root)
|
|
459
500
|
for file in self._contract.profile_files:
|
|
460
501
|
target = f"{root}/{file.path}"
|
|
461
502
|
quoted = shlex.quote(target)
|
|
@@ -674,14 +715,38 @@ class TangleCandidateAgent(BaseAgent):
|
|
|
674
715
|
user="root",
|
|
675
716
|
)
|
|
676
717
|
|
|
718
|
+
async def _install_evaluator_git_config(
|
|
719
|
+
self, environment: BaseEnvironment
|
|
720
|
+
) -> None:
|
|
721
|
+
config = _evaluator_git_config(self._contract.repository_base_commit)
|
|
722
|
+
target = f"{self._contract.task_root}/.git/config"
|
|
723
|
+
with tempfile.NamedTemporaryFile(delete=False) as handle:
|
|
724
|
+
handle.write(config)
|
|
725
|
+
local_path = Path(handle.name)
|
|
726
|
+
try:
|
|
727
|
+
await self._write_verified_file(
|
|
728
|
+
environment,
|
|
729
|
+
local_path,
|
|
730
|
+
target,
|
|
731
|
+
anchor=self._contract.task_root,
|
|
732
|
+
mode=0o600,
|
|
733
|
+
digest=sha256_bytes(config),
|
|
734
|
+
byte_length=len(config),
|
|
735
|
+
)
|
|
736
|
+
finally:
|
|
737
|
+
local_path.unlink(missing_ok=True)
|
|
738
|
+
|
|
677
739
|
async def _restore_profile_and_capture_solution(
|
|
678
740
|
self, environment: BaseEnvironment
|
|
679
741
|
) -> None:
|
|
680
742
|
root = self._contract.task_root
|
|
681
|
-
|
|
682
|
-
git = f"git -c core.hooksPath=/dev/null -C {quoted_root}"
|
|
743
|
+
git = _evaluator_git(root)
|
|
683
744
|
git_env = candidate_git_env(root)
|
|
684
745
|
|
|
746
|
+
# Repository-local config is candidate-owned after execution. Replace it
|
|
747
|
+
# before root invokes Git so arbitrary filters and other programs cannot run.
|
|
748
|
+
await self._install_evaluator_git_config(environment)
|
|
749
|
+
|
|
685
750
|
# Ignore candidate-authored commits and derive one evaluator-owned final
|
|
686
751
|
# tree from the actual working files relative to the signed base commit.
|
|
687
752
|
await self._exec(
|
|
@@ -718,7 +783,12 @@ class TangleCandidateAgent(BaseAgent):
|
|
|
718
783
|
await self._ensure_real_directory(environment, parent, anchor=root)
|
|
719
784
|
await self._exec(environment, f"rm -rf -- {quoted}", user="root")
|
|
720
785
|
|
|
721
|
-
await self._exec(
|
|
786
|
+
await self._exec(
|
|
787
|
+
environment,
|
|
788
|
+
f"{git} add -A -- . {shlex.quote(':(exclude).sidecar')}",
|
|
789
|
+
env=git_env,
|
|
790
|
+
user="root",
|
|
791
|
+
)
|
|
722
792
|
for original in self._original_profile_files:
|
|
723
793
|
if original.tracked:
|
|
724
794
|
continue
|
|
@@ -54,9 +54,54 @@ try {
|
|
|
54
54
|
const packDir = path.join(scratch, 'pack')
|
|
55
55
|
const runtimePackDir = path.join(scratch, 'runtime-pack')
|
|
56
56
|
const consumerDir = path.join(scratch, 'consumer')
|
|
57
|
+
const terminalBenchVenv = path.join(scratch, 'terminal-bench-venv')
|
|
58
|
+
const terminalBenchBinDir = path.join(terminalBenchVenv, 'bin')
|
|
57
59
|
await mkdir(packDir)
|
|
58
60
|
await mkdir(runtimePackDir)
|
|
59
61
|
await mkdir(consumerDir)
|
|
62
|
+
await mkdir(terminalBenchBinDir, { recursive: true })
|
|
63
|
+
await writeFile(path.join(terminalBenchVenv, 'package.json'), '{"type":"module"}\n')
|
|
64
|
+
await writeFile(
|
|
65
|
+
path.join(terminalBenchBinDir, 'python'),
|
|
66
|
+
String.raw`#!/usr/bin/env node
|
|
67
|
+
const rows = [{
|
|
68
|
+
id: 'installed-absolute-venv',
|
|
69
|
+
instruction: 'prove the installed path',
|
|
70
|
+
task_dir: '/tmp/terminal-bench-task',
|
|
71
|
+
solution: 'echo ok\n',
|
|
72
|
+
}]
|
|
73
|
+
process.stdout.write(JSON.stringify(rows) + '\n')
|
|
74
|
+
`,
|
|
75
|
+
{ mode: 0o755 },
|
|
76
|
+
)
|
|
77
|
+
await writeFile(
|
|
78
|
+
path.join(terminalBenchBinDir, 'tb'),
|
|
79
|
+
String.raw`#!/usr/bin/env node
|
|
80
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
81
|
+
import { join } from 'node:path'
|
|
82
|
+
|
|
83
|
+
const args = process.argv.slice(2)
|
|
84
|
+
function value(flag) {
|
|
85
|
+
const index = args.indexOf(flag)
|
|
86
|
+
if (index === -1 || index + 1 >= args.length) throw new Error('missing ' + flag)
|
|
87
|
+
return args[index + 1]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const outputPath = value('--output-path')
|
|
91
|
+
const runId = value('--run-id')
|
|
92
|
+
const taskId = value('-t')
|
|
93
|
+
const reportDir = join(outputPath, runId)
|
|
94
|
+
mkdirSync(reportDir, { recursive: true })
|
|
95
|
+
writeFileSync(
|
|
96
|
+
join(reportDir, 'results.json'),
|
|
97
|
+
JSON.stringify({
|
|
98
|
+
resolved_ids: [taskId],
|
|
99
|
+
results: [{ task_id: taskId, is_resolved: true, parser_results: { installedVenv: true } }],
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
`,
|
|
103
|
+
{ mode: 0o755 },
|
|
104
|
+
)
|
|
60
105
|
|
|
61
106
|
// Build explicitly so verification cannot inherit a machine-level
|
|
62
107
|
// ignore-scripts setting and accidentally pack stale or missing output.
|
|
@@ -122,6 +167,21 @@ try {
|
|
|
122
167
|
path.join(consumerDir, 'index.mjs'),
|
|
123
168
|
"import { resolveAdapter, runBenchmarks } from '@tangle-network/agent-bench'\nimport { ADAPTERS } from '@tangle-network/agent-bench/adapters'\nimport { createCragAdapter } from '@tangle-network/agent-bench/benchmarks/crag'\n\nif (typeof resolveAdapter !== 'function' || typeof runBenchmarks !== 'function') throw new Error('root exports are not executable')\nif (typeof ADAPTERS !== 'object' || typeof createCragAdapter !== 'function') throw new Error('subpath exports are not executable')\nif (resolveAdapter('crag').name !== 'crag') throw new Error('compiled adapter registry returned the wrong adapter')\nprocess.env.TOOLLM_FIXTURES = '1'\nconst toolLlmTasks = await resolveAdapter('toollm').loadTasks({ limit: 1 })\nif (toolLlmTasks.length !== 1 || toolLlmTasks[0]?.id !== '1') throw new Error('packed ToolLLM fixture loading failed')\n",
|
|
124
169
|
)
|
|
170
|
+
await writeFile(
|
|
171
|
+
path.join(consumerDir, 'terminal-bench-absolute-venv.mjs'),
|
|
172
|
+
`import { createTerminalBenchAdapter } from '@tangle-network/agent-bench/benchmarks/terminal-bench'
|
|
173
|
+
|
|
174
|
+
const adapter = createTerminalBenchAdapter()
|
|
175
|
+
const tasks = await adapter.loadTasks({ ids: ['installed-absolute-venv'] })
|
|
176
|
+
if (tasks.length !== 1 || tasks[0]?.id !== 'installed-absolute-venv') {
|
|
177
|
+
throw new Error('packed Terminal-Bench adapter did not load through the absolute venv')
|
|
178
|
+
}
|
|
179
|
+
const score = await adapter.judge(tasks[0], 'echo installed-package-path')
|
|
180
|
+
if (!score.resolved || score.score !== 1) {
|
|
181
|
+
throw new Error(\`packed Terminal-Bench adapter did not judge through the absolute venv: \${JSON.stringify(score)}\`)
|
|
182
|
+
}
|
|
183
|
+
`,
|
|
184
|
+
)
|
|
125
185
|
await writeFile(
|
|
126
186
|
path.join(consumerDir, 'tsconfig.json'),
|
|
127
187
|
`${JSON.stringify(
|
|
@@ -165,6 +225,10 @@ for name in sorted(expected):
|
|
|
165
225
|
consumerDir,
|
|
166
226
|
)
|
|
167
227
|
await run('node', ['index.mjs'], consumerDir)
|
|
228
|
+
await run('node', ['terminal-bench-absolute-venv.mjs'], consumerDir, {
|
|
229
|
+
...process.env,
|
|
230
|
+
TERMINAL_BENCH_VENV: terminalBenchVenv,
|
|
231
|
+
})
|
|
168
232
|
await run('npm', ['exec', '--', 'tsc', '-p', 'tsconfig.json'], consumerDir)
|
|
169
233
|
const typescript5 = await run('npm', ['exec', '--', 'tsc', '--version'], consumerDir)
|
|
170
234
|
if (typescript5.stdout.trim() !== `Version ${TYPESCRIPT_5}`) {
|
|
@@ -228,7 +292,7 @@ for name in sorted(expected):
|
|
|
228
292
|
})
|
|
229
293
|
}
|
|
230
294
|
console.log(
|
|
231
|
-
`packed consumer verified: ${manifest.name}@${manifest.version} with @tangle-network/agent-runtime@${runtimeManifest.version}, TypeScript ${TYPESCRIPT_5} and ${TYPESCRIPT_6}; prepared ${prepareProof.executionPlanDigest}`,
|
|
295
|
+
`packed consumer verified: ${manifest.name}@${manifest.version} with @tangle-network/agent-runtime@${runtimeManifest.version}, TypeScript ${TYPESCRIPT_5} and ${TYPESCRIPT_6}, absolute Terminal-Bench venv; prepared ${prepareProof.executionPlanDigest}`,
|
|
232
296
|
)
|
|
233
297
|
} finally {
|
|
234
298
|
await rm(scratch, { recursive: true, force: true })
|
|
@@ -435,7 +435,7 @@ ${proofArm === 'success' ? "(task / 'src/status.txt').write_text('ready\\nowner=
|
|
|
435
435
|
workspace: taskWorkspace,
|
|
436
436
|
evaluatorTaskContainer: container,
|
|
437
437
|
limits: {
|
|
438
|
-
timeoutMs:
|
|
438
|
+
timeoutMs: 180_000,
|
|
439
439
|
maxSteps: 8,
|
|
440
440
|
maxModelCalls: 0,
|
|
441
441
|
maxInputTokens: 0,
|
|
@@ -671,12 +671,14 @@ ${proofArm === 'success' ? "(task / 'src/status.txt').write_text('ready\\nowner=
|
|
|
671
671
|
)
|
|
672
672
|
}
|
|
673
673
|
assertTreeOmits(scratch, 'zero-model-proof')
|
|
674
|
-
const usage = finalized.receipt.value.usage
|
|
674
|
+
const usage = finalized.receipt.value.modelSettlement.material.usage
|
|
675
675
|
if (
|
|
676
676
|
usage.modelCalls !== 0 ||
|
|
677
677
|
usage.inputTokens !== 0 ||
|
|
678
678
|
usage.outputTokens !== 0 ||
|
|
679
|
-
usage.
|
|
679
|
+
usage.cachedInputTokens !== 0 ||
|
|
680
|
+
usage.reasoningTokens !== 0 ||
|
|
681
|
+
usage.costUsdNanos !== 0 ||
|
|
680
682
|
finalized.receipt.value.trace.modelCallCount !== 0
|
|
681
683
|
) {
|
|
682
684
|
throw new Error(`runtime minted nonzero usage for a zero-model run: ${JSON.stringify(usage)}`)
|
|
@@ -691,7 +693,7 @@ ${proofArm === 'success' ? "(task / 'src/status.txt').write_text('ready\\nowner=
|
|
|
691
693
|
modelCalls: usage.modelCalls,
|
|
692
694
|
inputTokens: usage.inputTokens,
|
|
693
695
|
outputTokens: usage.outputTokens,
|
|
694
|
-
costUsd: usage.
|
|
696
|
+
costUsd: usage.costUsdNanos / 1_000_000_000,
|
|
695
697
|
pierContextUsage: null,
|
|
696
698
|
profileExcludedByVerifier: true,
|
|
697
699
|
container: {
|
|
@@ -4,7 +4,13 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
|
4
4
|
import test from 'node:test'
|
|
5
5
|
import { tmpdir } from 'node:os'
|
|
6
6
|
import { join } from 'node:path'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
benchRoot,
|
|
9
|
+
resolveBenchPython,
|
|
10
|
+
runStagedJudge,
|
|
11
|
+
venvBin,
|
|
12
|
+
venvBinAt,
|
|
13
|
+
} from './_harness'
|
|
8
14
|
|
|
9
15
|
const digest = (bytes: Uint8Array): `sha256:${string}` =>
|
|
10
16
|
`sha256:${createHash('sha256').update(bytes).digest('hex')}`
|
|
@@ -31,6 +37,15 @@ test('resolveBenchPython rejects relative and empty interpreter paths', () => {
|
|
|
31
37
|
)
|
|
32
38
|
})
|
|
33
39
|
|
|
40
|
+
test('venv executable paths support package-owned and external environments', () => {
|
|
41
|
+
assert.equal(venvBinAt('/srv/terminal-bench', 'tb'), join('/srv/terminal-bench', 'bin', 'tb'))
|
|
42
|
+
assert.equal(
|
|
43
|
+
venvBinAt('.venv-commit0', 'python'),
|
|
44
|
+
join(benchRoot, '.venv-commit0', 'bin', 'python'),
|
|
45
|
+
)
|
|
46
|
+
assert.equal(venvBin('python'), venvBinAt('.venv', 'python'))
|
|
47
|
+
})
|
|
48
|
+
|
|
34
49
|
test('runStagedJudge terminates a hung evaluator at its explicit timeout', async () => {
|
|
35
50
|
const started = Date.now()
|
|
36
51
|
await assert.rejects(
|