Skip to content

Commit 4fa0f52

Browse files
committed
feat: refine hosted runner, kernel, and timeout/reproducibility services
1 parent 2374ca1 commit 4fa0f52

21 files changed

Lines changed: 3110 additions & 137 deletions

‎services/hosted-runner/src/server.test.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ async function start(
3333
authorizeToken: HostedRunnerServerOptions["authorizeToken"] = (token) =>
3434
Promise.resolve(token === "scoped-token"),
3535
onJobSettled?: HostedRunnerServerOptions["onJobSettled"],
36+
releaseIdentity?: HostedRunnerServerOptions["releaseIdentity"],
3637
) {
3738
const server = createHostedRunnerServer({
3839
authorizeToken,
3940
processJob,
4041
...(onJobSettled === undefined ? {} : { onJobSettled }),
42+
...(releaseIdentity === undefined ? {} : { releaseIdentity }),
4143
});
4244
servers.push(server);
4345
server.listen(0, "127.0.0.1");
@@ -103,13 +105,20 @@ describe("hosted runner HTTP service", () => {
103105

104106
it("accepts one authenticated job and reports readiness without exposing inputs", async () => {
105107
const processJob = vi.fn(async () => undefined);
106-
const baseUrl = await start(processJob);
108+
const runnerSourceCommit = "b".repeat(40);
109+
const runnerImageDigest = `sha256:${"c".repeat(64)}`;
110+
const baseUrl = await start(processJob, undefined, undefined, {
111+
runnerSourceCommit,
112+
runnerImageDigest,
113+
});
107114

108115
const ready = await fetch(`${baseUrl}/ready`);
109116
expect(ready.status).toBe(200);
110117
await expect(ready.json()).resolves.toEqual({
111118
status: "ready",
112119
service: "counterlab-hosted-runner",
120+
runnerSourceCommit,
121+
runnerImageDigest,
113122
});
114123

115124
const accepted = await fetch(`${baseUrl}/jobs`, {

‎services/hosted-runner/src/server.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ const DispatchSchema = z
3030
.strict();
3131

3232
export type HostedRunnerServerOptions = {
33+
releaseIdentity?: {
34+
runnerSourceCommit: string;
35+
runnerImageDigest: string;
36+
};
3337
authorizeToken(
3438
token: string,
3539
jobId: string,
@@ -100,6 +104,7 @@ export function createHostedRunnerServer(options: HostedRunnerServerOptions) {
100104
respond(response, 200, {
101105
status: "ready",
102106
service: "counterlab-hosted-runner",
107+
...(options.releaseIdentity ?? {}),
103108
});
104109
return;
105110
}
@@ -217,6 +222,10 @@ async function startProductionServer(): Promise<void> {
217222
const authJson = process.env.CODEX_AUTH_JSON;
218223
const runnerVerifyingPublicKey =
219224
process.env.COUNTERLAB_RUNNER_VERIFYING_PUBLIC_KEY;
225+
const runnerSourceCommit =
226+
process.env.COUNTERLAB_RUNNER_SOURCE_COMMIT?.trim() ?? "";
227+
const runnerImageDigest =
228+
process.env.COUNTERLAB_RUNNER_IMAGE_DIGEST?.trim() ?? "";
220229
delete process.env.CODEX_AUTH_JSON;
221230
delete process.env.COUNTERLAB_RUNNER_VERIFYING_PUBLIC_KEY;
222231
if (authJson === undefined || authJson.trim().length === 0) {
@@ -230,6 +239,17 @@ async function startProductionServer(): Promise<void> {
230239
"COUNTERLAB_RUNNER_VERIFYING_PUBLIC_KEY is required by the hosted runner",
231240
);
232241
}
242+
const releaseIdentityMissing =
243+
runnerSourceCommit.length === 0 && runnerImageDigest.length === 0;
244+
if (
245+
!releaseIdentityMissing &&
246+
(!/^[a-f0-9]{40}$/u.test(runnerSourceCommit) ||
247+
!/^sha256:[a-f0-9]{64}$/u.test(runnerImageDigest))
248+
) {
249+
throw new Error(
250+
"Runner release identity must contain an exact source commit and image digest",
251+
);
252+
}
233253
const workspaceRoot = process.env.COUNTERLAB_RUNNER_WORK_ROOT ?? "/work/jobs";
234254
const codexHomeRoot =
235255
process.env.COUNTERLAB_CODEX_HOME_ROOT ?? "/run/counterlab-codex";
@@ -260,6 +280,9 @@ async function startProductionServer(): Promise<void> {
260280
current?: ReturnType<typeof createHostedRunnerServer>;
261281
} = {};
262282
const server = createHostedRunnerServer({
283+
...(releaseIdentityMissing
284+
? {}
285+
: { releaseIdentity: { runnerSourceCommit, runnerImageDigest } }),
263286
async authorizeToken(scopedToken, jobId, purpose, controlPlaneOrigin) {
264287
try {
265288
await verifyRunnerJobToken(scopedToken, runnerVerifyingPublicKey, {

‎services/kernel/src/counterlab_kernel/hosted_patch.py‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,88 @@
166166
"resultHash",
167167
})
168168
_V5_TRANSFER_CHECK_KEYS = frozenset({"invariant", "passed", "evidence"})
169+
_V5_TRANSFER_POLICIES: dict[str, dict[str, Any]] = {
170+
"entity_leakage": {
171+
"taskId": "forecasting-future-leakage-01",
172+
"evaluatorVersion": "counterlab-transfer-v1",
173+
"selectedStrategy": "time_ordered_holdout",
174+
"identifiedRisks": ["centered_window_reads_future"],
175+
"requiredEvidence": frozenset(
176+
{"center_true_uses_later_targets", "random_split_mixes_dates"}
177+
),
178+
"allowedEvidence": frozenset(
179+
{
180+
"center_true_uses_later_targets",
181+
"random_split_mixes_dates",
182+
"metric_is_mae",
183+
}
184+
),
185+
"checks": [
186+
{
187+
"invariant": "TIME_AWARE_EVALUATION",
188+
"passed": True,
189+
"evidence": (
190+
"A deployment-aligned forecast trains on earlier dates and "
191+
"evaluates later dates."
192+
),
193+
},
194+
{
195+
"invariant": "FUTURE_INFORMATION_RISK",
196+
"passed": True,
197+
"evidence": (
198+
"A centered target window reads observations that do not "
199+
"exist at prediction time."
200+
),
201+
},
202+
{
203+
"invariant": "EVIDENCE_GROUNDED",
204+
"passed": True,
205+
"evidence": (
206+
"Both center=True and the shuffled date split must be "
207+
"selected as code evidence."
208+
),
209+
},
210+
],
211+
},
212+
"class_imbalance": {
213+
"taskId": "manufacturing-defect-transfer-01",
214+
"evaluatorVersion": "counterlab-imbalance-transfer-v1",
215+
"selectedStrategy": "reject_accuracy_only",
216+
"identifiedRisks": ["recall_and_pr_auc"],
217+
"requiredEvidence": frozenset(
218+
{"zero_true_positives", "rare_base_rate"}
219+
),
220+
"allowedEvidence": frozenset(
221+
{"zero_true_positives", "rare_base_rate", "many_true_negatives"}
222+
),
223+
"checks": [
224+
{
225+
"invariant": "ACCURACY_CLAIM_REJECTED",
226+
"passed": True,
227+
"evidence": (
228+
"Overall accuracy cannot support deployment when the "
229+
"classifier misses every rare defective part."
230+
),
231+
},
232+
{
233+
"invariant": "MINORITY_METRICS_SELECTED",
234+
"passed": True,
235+
"evidence": (
236+
"Defect recall and PR-AUC expose performance on the rare "
237+
"class and relative to its base rate."
238+
),
239+
},
240+
{
241+
"invariant": "EVIDENCE_GROUNDED",
242+
"passed": True,
243+
"evidence": (
244+
"Use both the zero true-positive count and the 1% base rate "
245+
"as evidence."
246+
),
247+
},
248+
],
249+
},
250+
}
169251

170252

171253
class HostedPatchError(ValueError):
@@ -200,6 +282,56 @@ def _strict_fields(
200282
)
201283

202284

285+
def _validate_v5_transfer_semantics(
286+
concept: str, transfer: Mapping[str, Any]
287+
) -> None:
288+
policy = _V5_TRANSFER_POLICIES.get(concept)
289+
if policy is None:
290+
raise HostedPatchError("v5 patch transfer Subject Pack is not registered")
291+
evidence = _sequence(
292+
transfer.get("evidenceChoices"), "transferResult.evidenceChoices"
293+
)
294+
evidence_set = set(evidence)
295+
required_evidence = policy["requiredEvidence"]
296+
allowed_evidence = policy["allowedEvidence"]
297+
checks = [
298+
dict(_mapping(check, "transfer check"))
299+
for check in _sequence(transfer.get("checks"), "transferResult.checks")
300+
]
301+
if (
302+
transfer.get("taskId") != policy["taskId"]
303+
or transfer.get("evaluatorVersion") != policy["evaluatorVersion"]
304+
or transfer.get("selectedStrategy") != policy["selectedStrategy"]
305+
or transfer.get("identifiedRisks") != policy["identifiedRisks"]
306+
or len(evidence) != len(evidence_set)
307+
or evidence != sorted(evidence)
308+
or not required_evidence.issubset(evidence_set)
309+
or not evidence_set.issubset(allowed_evidence)
310+
or any(
311+
not isinstance(check.get("evidence"), str)
312+
or not check["evidence"].strip()
313+
for check in checks
314+
)
315+
or [
316+
{
317+
"invariant": check.get("invariant"),
318+
"passed": check.get("passed"),
319+
}
320+
for check in checks
321+
]
322+
!= [
323+
{
324+
"invariant": check["invariant"],
325+
"passed": check["passed"],
326+
}
327+
for check in policy["checks"]
328+
]
329+
):
330+
raise HostedPatchError(
331+
"v5 patch transfer does not match the fixed evaluator semantics"
332+
)
333+
334+
203335
def _validate_v5_patch_bundle(bundle: Mapping[str, Any]) -> None:
204336
_strict_fields(bundle, _V5_PATCH_BUNDLE_KEYS, "v5 patch bundle")
205337
manifest = _mapping(bundle.get("artifactManifest"), "artifactManifest")
@@ -267,6 +399,7 @@ def _validate_v5_patch_bundle(bundle: Mapping[str, Any]) -> None:
267399
concept = belief.get("concept")
268400
if concept not in _PATCH_OPERATIONS:
269401
raise HostedPatchError("v5 patch Subject Pack is not registered")
402+
_validate_v5_transfer_semantics(str(concept), transfer)
270403
if (
271404
manifest.get("support", {}).get("status") != "SUPPORTED"
272405
or belief.get("supportState") != "SUPPORTED"

‎services/kernel/src/counterlab_kernel/hosted_run.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,7 @@ def _execute_v5_interactive(bundle: Mapping[str, Any]) -> dict[str, Any]:
647647
"scientific-candidate-verifier-v1",
648648
"scientific-candidate-verifier-v2",
649649
"scientific-candidate-verifier-v3",
650+
"scientific-candidate-verifier-v4",
650651
}
651652
or compile_authority.get("projectionAdapterVersion")
652653
!= "experiment-ir-v5-to-plan-v2-v1"
@@ -1094,6 +1095,7 @@ def _execute_v5(bundle: Mapping[str, Any]) -> dict[str, Any]:
10941095
"scientific-candidate-verifier-v1",
10951096
"scientific-candidate-verifier-v2",
10961097
"scientific-candidate-verifier-v3",
1098+
"scientific-candidate-verifier-v4",
10971099
}
10981100
or provenance.get("scorerVersion") != selection.get("scorerVersion")
10991101
or provenance.get("projectionAdapterVersion")

‎services/kernel/src/counterlab_kernel/imbalance_transfer.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
_TASK: dict[str, Any] = {
1212
"schemaVersion": "1",
13-
"id": "manufacturing-defect-imbalance-01",
13+
"id": "manufacturing-defect-transfer-01",
1414
"concept": "class_imbalance_transfer",
1515
"title": "Would this inspection model catch rare defects?",
1616
"scenario": (
@@ -108,6 +108,8 @@ def evaluate_manufacturing_transfer(
108108
raise ValueError(f"unknown decision choice: {decision_choice}")
109109
if metric_choice not in _choice_ids("metricChoices"):
110110
raise ValueError(f"unknown metric choice: {metric_choice}")
111+
if len(set(evidence_choices)) != len(evidence_choices):
112+
raise ValueError("duplicate evidence choice")
111113
unknown_evidence = sorted(
112114
set(evidence_choices).difference(_choice_ids("evidenceChips"))
113115
)
@@ -129,8 +131,8 @@ def evaluate_manufacturing_transfer(
129131
"id": "MINORITY_METRICS_SELECTED",
130132
"passed": metric_choice == _CORRECT_METRIC,
131133
"feedback": (
132-
"Defect recall and PR-AUC expose performance on the rare class and "
133-
"relative to its base rate."
134+
"Defect recall and PR-AUC expose performance on the rare class "
135+
"and relative to its base rate."
134136
),
135137
},
136138
{

‎services/kernel/src/counterlab_kernel/transfer.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ def evaluate_forecasting_transfer(
124124
raise ValueError(f"unknown strategy choice: {strategy_choice}")
125125
if risk_choice not in risk_ids:
126126
raise ValueError(f"unknown risk choice: {risk_choice}")
127+
if len(set(evidence_choices)) != len(evidence_choices):
128+
raise ValueError("duplicate evidence choice")
127129
unknown_evidence = sorted(set(evidence_choices).difference(evidence_ids))
128130
if unknown_evidence:
129131
raise ValueError(
@@ -178,4 +180,3 @@ def evaluate_forecasting_transfer(
178180
}
179181
result["resultHash"] = sha256_json(result)
180182
return result
181-
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from __future__ import annotations
2+
3+
from hashlib import sha256
4+
import json
5+
from pathlib import Path
6+
7+
from counterlab_kernel.canonical import sha256_json
8+
from counterlab_kernel.patching import (
9+
compile_sample_notebook_patch,
10+
verify_sample_notebook_patch,
11+
)
12+
from counterlab_kernel.transfer import evaluate_forecasting_transfer
13+
14+
15+
ROOT = Path(__file__).resolve().parents[3]
16+
EVIDENCE = ROOT / "fixtures/public/leakage_sample_patch_v1"
17+
ORIGINAL = ROOT / "fixtures/notebooks/customer_churn_leakage.ipynb"
18+
FIXTURE = ROOT / "fixtures/public/customer_churn.csv"
19+
20+
21+
def read_json(path: Path) -> dict[str, object]:
22+
value = json.loads(path.read_text(encoding="utf-8"))
23+
assert isinstance(value, dict)
24+
return value
25+
26+
27+
def file_sha256(path: Path) -> str:
28+
return sha256(path.read_bytes()).hexdigest()
29+
30+
31+
def passing_transfer() -> dict[str, object]:
32+
return evaluate_forecasting_transfer(
33+
strategy_choice="time_ordered_holdout",
34+
risk_choice="centered_window_reads_future",
35+
evidence_choices=[
36+
"center_true_uses_later_targets",
37+
"random_split_mixes_dates",
38+
],
39+
)
40+
41+
42+
def test_checked_in_current_patch_is_source_and_byte_bound() -> None:
43+
patch = read_json(EVIDENCE / "patch-kernel-result.json")
44+
metadata = read_json(EVIDENCE / "customer_churn_leakage.patch.json")
45+
verification = read_json(EVIDENCE / "patch-verification.json")
46+
patched = EVIDENCE / "customer_churn_leakage.patched.ipynb"
47+
48+
assert patch["status"] == "VERIFIED"
49+
assert patch["originalSha256"] == file_sha256(ORIGINAL)
50+
assert patch["patchedSha256"] == file_sha256(patched)
51+
assert metadata["sourceNotebookSha256"] == patch["originalSha256"]
52+
assert metadata["patchedNotebookSha256"] == patch["patchedSha256"]
53+
assert verification["candidateSha256"] == patch["patchedSha256"]
54+
metadata_without_hash = dict(metadata)
55+
recorded_metadata_hash = metadata_without_hash.pop("metadataHash")
56+
assert sha256_json(metadata_without_hash) == recorded_metadata_hash
57+
58+
59+
def test_current_patch_regenerates_deterministically(tmp_path: Path) -> None:
60+
checked = read_json(EVIDENCE / "patch-kernel-result.json")
61+
regenerated = compile_sample_notebook_patch(
62+
original_notebook=ORIGINAL,
63+
fixture_csv=FIXTURE,
64+
output_dir=tmp_path / "patch",
65+
transfer_result=passing_transfer(),
66+
)
67+
assert regenerated["status"] == "VERIFIED"
68+
69+
checked_portable = {
70+
key: value
71+
for key, value in checked.items()
72+
if key not in {"patchFile", "metadataFile"}
73+
}
74+
regenerated_portable = {
75+
key: value
76+
for key, value in regenerated.items()
77+
if key not in {"patchPath", "metadataPath"}
78+
}
79+
assert regenerated_portable == checked_portable
80+
assert (
81+
verify_sample_notebook_patch(
82+
original_notebook=ORIGINAL,
83+
candidate_notebook=Path(str(regenerated["patchPath"])),
84+
fixture_csv=FIXTURE,
85+
)["status"]
86+
== "VERIFIED"
87+
)

0 commit comments

Comments
 (0)