From 5c18418fdc3e8e6f5d8bad2c24b2b9b5da0721b7 Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 14:43:34 -0400 Subject: [PATCH 1/5] Add inactive deterministic verifier normalizer --- README.md | 15 + RESTORE.md | 15 + .../deterministic-verifier/v1/normalize.jq | 169 ++++++++++ ci/required-files.txt | 4 + ...ult-deterministic-verifier-adapter.test.sh | 312 ++++++++++++++++++ 5 files changed, 515 insertions(+) create mode 100644 adapters/deterministic-verifier/v1/normalize.jq create mode 100755 scripts/test/default-deterministic-verifier-adapter.test.sh diff --git a/README.md b/README.md index 78006e5..4464070 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,21 @@ main. The payload is offline and unqualified. It does not call GitHub or a CLI, credential, change a repository or request, grant authority or qualification, or activate a profile. +## Inactive deterministic verifier normalizer payload + +`adapters/deterministic-verifier/v1/normalize.jq` validates an already-supplied +portable-core v2 verifier request, resolved profile, adapter contract, and stage +result. It reuses the core's request, profile, and result relations, then returns +one canonical observation. It does not interpret provider or CI status as verifier +evidence. GitHub Actions remains a separate CI observation boundary. + +This payload is offline and unqualified. It does not execute a candidate or tool, +read proof bytes, enforce a sandbox, use a credential or network, write evidence, +grant authority or qualification, or activate a profile. A later assembly PR may +add its manifest and inactive default-set binding only after the payload has a +durable commit on main. A runnable verifier still requires a separately qualified +sandbox launcher and fixed verification implementation. + ## The current default team You talk **only** to yshifu, in a Claude Code session. yshifu orchestrates the other roles diff --git a/RESTORE.md b/RESTORE.md index e872d00..7fb7d40 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -464,6 +464,21 @@ durable main commit and add default-set wiring. The pure jq payload is offline a unqualified. It does not call GitHub, use a credential, change a repository or request, grant authority or qualification, or activate a profile. +Restore the two paths in the manifest's inactive deterministic verifier normalizer +payload block, then run: + +```sh +bash scripts/test/default-deterministic-verifier-adapter.test.sh +``` + +This validates exact core-v2 verifier request/profile/result relations, the +deterministic role and permission ceiling, candidate and verification-plan binding, +evidence precedence, stale inputs, and the boundary that keeps CI observations out +of verifier evidence. This stage intentionally has no adapter manifest. The pure +jq payload is offline and unqualified. It does not execute a candidate or tool, +read proof bytes, enforce a sandbox, use a credential or network, write evidence, +grant authority or qualification, or activate a profile. + --- ## 5. Smoke test — prove the rebuilt team is alive diff --git a/adapters/deterministic-verifier/v1/normalize.jq b/adapters/deterministic-verifier/v1/normalize.jq new file mode 100644 index 0000000..e4e495c --- /dev/null +++ b/adapters/deterministic-verifier/v1/normalize.jq @@ -0,0 +1,169 @@ +import "schema" as schema; +import "profile_graph" as profile; +import "stage_request" as request; +import "result_truth" as result; + +def absent: {state:"absent"}; +def present($value): {state:"present",value:$value}; + +def document_ref($pair): + { + schema_version:2, + kind:$pair.content.kind, + id:$pair.content.id, + sha256:$pair.sha256 + }; + +def trust_context_shape_ok: + schema::exact_fields(["body","id","kind","schema_version"];[]) and + .schema_version == 1 and .kind == "adapter_trust_context" and + (.id | schema::id_ok) and + (.body | + schema::exact_fields( + ["binding_id","manifest","request","resolved_profile","snapshot_ref"];[]) and + (.binding_id | schema::id_ok) and + (.manifest | profile::document_pair_ok("adapter_manifest")) and + (.request | profile::document_pair_ok("stage_request")) and + (.resolved_profile | profile::document_pair_ok("resolved_profile")) and + (.snapshot_ref | schema::content_ref_ok) and + .snapshot_ref.media_type == "application/json"); + +def snapshot_shape_ok: + schema::exact_fields(["body","id","kind","schema_version"];[]) and + .schema_version == 1 and .kind == "deterministic_verifier_snapshot" and + (.id | schema::id_ok) and + (.body | + schema::exact_fields(["observed_at","result"];[]) and + (.observed_at | schema::time_ok) and + (.result | profile::document_pair_ok("stage_result"))); + +def input_shape_ok: + schema::exact_fields(["snapshot","trust_context"];[]) and + (.trust_context | trust_context_shape_ok) and + (.snapshot | snapshot_shape_ok); + +def selected_binding($trust): + [$trust.body.resolved_profile.content.body.bindings[] | + select(.binding.binding_id == $trust.body.binding_id)]; + +def manifest_contract_ok: + profile::adapter_manifest_self_ok and + .id == "adapter.deterministic-verifier.v1" and + .body.adapter_version == "v1" and + .body.offered_roles == ["verifier"] and + .body.offered_execution_kinds == ["deterministic"] and + .body.offered_capabilities == ["core.verify.run.v1"] and + .body.offered_permissions == + ["core.perm.candidate.execute.v1","core.perm.evidence.write.v1", + "core.perm.target.read.v1"] and + .body.offered_tools == [] and + (.body | has("config_contract_ref") | not); + +def binding_ceiling_ok: + .role == "verifier" and .execution_kind == "deterministic" and + .requested_capabilities == ["core.verify.run.v1"] and + .requested_permissions == + ["core.perm.candidate.execute.v1","core.perm.evidence.write.v1", + "core.perm.target.read.v1"] and + .requested_tools == [] and .skill_refs == [] and + (has("config_ref") | not) and (has("prompt_ref") | not) and + (has("model_request") | not); + +def trust_relations_ok($trust): + $trust.body.request as $request_pair | + $trust.body.resolved_profile as $resolved_pair | + $trust.body.manifest as $manifest_pair | + selected_binding($trust) as $selected | + ($request_pair.content | request::document_self_ok) and + ($resolved_pair.content | profile::resolved_profile_self_ok) and + request::stage_request_resolved_ref_ok($request_pair;$resolved_pair) and + request::stage_request_resolved_relation_ok( + $request_pair.content.body;$resolved_pair.content.body) and + ($manifest_pair.content | manifest_contract_ok) and + ($selected | length) == 1 and + ($selected[0].binding | binding_ceiling_ok) and + profile::binding_manifest_graph_ok( + $selected[0].binding;$selected[0];$manifest_pair) and + $request_pair.content.body.operation.role == "verifier" and + $request_pair.content.body.operation.binding_id == $trust.body.binding_id and + $request_pair.content.body.operation.capability_id == "core.verify.run.v1" and + $request_pair.content.body.operation.arguments.network_mode == "deny"; + +def snapshot_relations_ok($trust; $snapshot): + $trust.body.snapshot_ref.sha256 == $snapshot.body.result.sha256 and + result::stage_run_ok( + $trust.body.request;$trust.body.resolved_profile;$snapshot.body.result) and + (if $snapshot.body.result.content.body | has("execution") then + $snapshot.body.result.content.body.reported_by == + $snapshot.body.result.content.body.execution.performer + else true end) and + $snapshot.body.result.content.body.recorded_at <= $snapshot.body.observed_at; + +def normalized_state($body): + if $body.status == "completed" then $body.outcome.value + else $body.status + end; + +def normalized_reason($body): + if $body.status == "completed" then + if $body.outcome.value == "passed" then "verifier.passed" + elif $body.outcome.value == "failed" then "verifier.failed" + else "verifier.inconclusive" + end + elif $body.status == "stale" then "verifier.inputs-stale" + elif $body.status == "failed" then "verifier.stage-failed" + elif $body.status == "cancelled" then "verifier.stage-cancelled" + elif $body.status == "blocked" then "verifier.stage-blocked" + else "verifier.stage-skipped" + end; + +def candidate_input($request_body): + [$request_body.inputs[] | + select(.input_id == $request_body.operation.arguments.candidate_input_id)][0]; + +def observation($trust; $snapshot): + $trust.body.request.content.body as $request_body | + $snapshot.body.result.content.body as $result_body | + { + schema_version:1, + kind:"adapter_observation", + adapter:{id:"adapter.deterministic-verifier.v1",version:"v1",status:"inactive"}, + state:normalized_state($result_body), + reason_id:normalized_reason($result_body), + trust_context:{ + snapshot_ref:$trust.body.snapshot_ref, + manifest_ref:document_ref($trust.body.manifest), + request_ref:document_ref($trust.body.request), + resolved_profile_ref:document_ref($trust.body.resolved_profile), + binding_id:$trust.body.binding_id + }, + observation:{ + observed_at:$snapshot.body.observed_at, + target_revision:$request_body.target_revision.value, + candidate_input:candidate_input($request_body), + verification_plan:$request_body.operation.arguments.verification_plan, + result:{ + result_ref:document_ref($snapshot.body.result), + attempt_id:$result_body.attempt_id, + attempt_number:$result_body.attempt_number, + status:$result_body.status, + outcome:(if $result_body | has("outcome") + then present($result_body.outcome) else absent end), + reason:(if $result_body | has("reason") + then present($result_body.reason) else absent end), + evidence:$result_body.evidence, + recorded_at:$result_body.recorded_at + } + }, + authority:"none", + qualification:{state:"unavailable",reason_id:"adapter.unqualified"}, + effects:[] + }; + +. as $input | +if ($input | input_shape_ok | not) then error("E_SHAPE") +elif (trust_relations_ok($input.trust_context) | not) then error("E_TRUST") +elif (snapshot_relations_ok($input.trust_context;$input.snapshot) | not) then + error("E_RESULT") +else observation($input.trust_context;$input.snapshot) +end diff --git a/ci/required-files.txt b/ci/required-files.txt index 64bb6b8..c25d87d 100644 --- a/ci/required-files.txt +++ b/ci/required-files.txt @@ -232,3 +232,7 @@ scripts/test/orchestrator-reconciliation-plan.test.sh # Inactive GitHub forge normalizer payload adapters/github-forge/v1/normalize.jq scripts/test/default-github-forge-adapter.test.sh + +# Inactive deterministic verifier normalizer payload +adapters/deterministic-verifier/v1/normalize.jq +scripts/test/default-deterministic-verifier-adapter.test.sh diff --git a/scripts/test/default-deterministic-verifier-adapter.test.sh b/scripts/test/default-deterministic-verifier-adapter.test.sh new file mode 100755 index 0000000..0672fec --- /dev/null +++ b/scripts/test/default-deterministic-verifier-adapter.test.sh @@ -0,0 +1,312 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +export LC_ALL=C + +root=$(CDPATH='' cd -P -- "${BASH_SOURCE[0]%/*}/../.." && pwd -P) +normalizer="$root/adapters/deterministic-verifier/v1/normalize.jq" +fixtures="$root/scripts/test" +tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-deterministic-verifier.XXXXXX") +trap '/bin/rm -rf -- "$tmp"' EXIT + +sha_file() { /usr/bin/shasum -a 256 "$1" | /usr/bin/awk '{print $1}'; } +fail() { /usr/bin/printf 'FAIL: %s\n' "$1" >&2; exit 1; } +passed=0 +pass() { passed=$((passed + 1)); /usr/bin/printf 'ok %s - %s\n' "$passed" "$1"; } + +platform=$(/usr/bin/uname -s):$(/usr/bin/uname -m) +case "$platform" in + Darwin:*) asset=jq-osx-amd64; digest=5c0a0a3ea600f302ee458b30317425dd9632d1ad8882259fcaf4e9b868b2b1ef ;; + Linux:x86_64) asset=jq-linux64; digest=af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44 ;; + *) fail "unsupported jq 1.6 proof platform: $platform" ;; +esac +jq_bin="${TMPDIR:-/tmp}/ystack-portable-core-jq16/$asset" +[ -f "$jq_bin" ] && [ "$(sha_file "$jq_bin")" = "$digest" ] || + fail 'verified jq 1.6 cache is required' +jq_command=("$jq_bin") +if [ "$platform" = Darwin:arm64 ]; then jq_command=(/usr/bin/arch -x86_64 "$jq_bin"); fi +[ "$("${jq_command[@]}" --version)" = jq-1.6 ] || fail 'jq version' + +generation=$("${jq_command[@]}" -er \ + 'select(type=="array" and length==1) | .[0].generation_id' \ + "$root/core/v2/generation-registry.json") +modules="$root/core/v2/generations/$generation/modules" +[ -d "$modules" ] && [ ! -L "$modules" ] || fail 'selected core modules' + +check() { + local name=$1 + shift + "$@" >/dev/null 2>&1 || fail "$name" + pass "$name" +} + +normalize() { + "${jq_command[@]}" -L "$modules" -S -c -f "$normalizer" "$1" +} + +expect_state() { + local name=$1 input=$2 expected=$3 output + output="$tmp/$name.out" + normalize "$input" >"$output" 2>"$tmp/$name.err" || fail "$name" + [ ! -s "$tmp/$name.err" ] || fail "$name diagnostics" + "${jq_command[@]}" -e --arg state "$expected" '.state == $state' "$output" \ + >/dev/null || fail "$name state" + pass "$name" +} + +expect_error() { + local name=$1 filter=$2 expected=$3 input + input="$tmp/$name.json" + "${jq_command[@]}" -S -c "$filter" "$tmp/passed.json" >"$input" + if normalize "$input" >"$tmp/$name.out" 2>"$tmp/$name.err"; then + fail "$name accepted" + fi + if [ -s "$tmp/$name.out" ] || + ! /usr/bin/grep -F "$expected" "$tmp/$name.err" >/dev/null; then + fail "$name diagnostics" + fi + pass "$name" +} + +for role in producer publisher reviewer; do + "${jq_command[@]}" -L "$fixtures" -S -c -n --arg role "$role" ' + import "portable-core-profile-graph-fixtures" as profile; + def v2: walk(if type == "object" and has("schema_version") + then .schema_version=2 else . end); + profile::manifest($role) | v2 + ' >"$tmp/manifest-$role.json" +done + +"${jq_command[@]}" -L "$fixtures" -S -c -n ' + import "portable-core-profile-graph-fixtures" as profile; + def v2: walk(if type == "object" and has("schema_version") + then .schema_version=2 else . end); + profile::manifest("verifier") | v2 | + .id="adapter.deterministic-verifier.v1" +' >"$tmp/manifest-verifier.json" + +"${jq_command[@]}" -L "$fixtures" -S -c -n ' + import "portable-core-profile-graph-fixtures" as profile; + { + schema_version:2,kind:"adapter_manifest",id:"manifest.forge", + body:{adapter_version:"v2",package_ref:profile::blob("packages/forge.bin";"6"), + offered_roles:["forge"],offered_execution_kinds:["deterministic"], + offered_capabilities:["core.forge.materialize-candidate.v2"], + offered_permissions:["core.perm.candidate-repository.write.v2", + "core.perm.evidence.write.v1","core.perm.scratch.write.v1", + "core.perm.target.read.v1"],offered_tools:[]} + } +' >"$tmp/manifest-forge.json" + +forge_sha=$(sha_file "$tmp/manifest-forge.json") +producer_sha=$(sha_file "$tmp/manifest-producer.json") +publisher_sha=$(sha_file "$tmp/manifest-publisher.json") +reviewer_sha=$(sha_file "$tmp/manifest-reviewer.json") +verifier_sha=$(sha_file "$tmp/manifest-verifier.json") +"${jq_command[@]}" -S -c -n --arg forge "$forge_sha" --arg producer "$producer_sha" \ + --arg publisher "$publisher_sha" --arg reviewer "$reviewer_sha" \ + --arg verifier "$verifier_sha" \ + '{forge:$forge,producer:$producer,publisher:$publisher,reviewer:$reviewer,verifier:$verifier}' \ + >"$tmp/manifest-shas.json" + +"${jq_command[@]}" -L "$fixtures" -S -c -n \ + --slurpfile shas "$tmp/manifest-shas.json" ' + import "portable-core-profile-graph-fixtures" as profile; + def v2: walk(if type == "object" and has("schema_version") + then .schema_version=2 else . end); + def forge_binding($digests): { + binding_id:"binding.forge",role:"forge", + manifest_ref:{schema_version:2,kind:"adapter_manifest",id:"manifest.forge", + sha256:$digests.forge},execution_kind:"deterministic", + adapter_instance_id:"instance.forge",principal_id:"principal.forge", + execution_boundary_id:"boundary.forge", + authority_ref:profile::scope("authority";"authority-forge";profile::sha("5")), + package_ref:profile::blob("packages/forge.bin";"6"),skill_refs:[],requested_tools:[], + requested_capabilities:["core.forge.materialize-candidate.v2"], + requested_permissions:["core.perm.candidate-repository.write.v2", + "core.perm.evidence.write.v1","core.perm.scratch.write.v1", + "core.perm.target.read.v1"]}; + profile::profile_doc($shas[0]) | v2 | + .body.profile_version="v2" | + (.body.bindings[] | select(.role=="verifier") | .manifest_ref.id)= + "adapter.deterministic-verifier.v1" | + .body.bindings += [forge_binding($shas[0])] | + .body.bindings |= sort_by(.binding_id) +' >"$tmp/profile.json" +profile_sha=$(sha_file "$tmp/profile.json") + +"${jq_command[@]}" -L "$fixtures" -S -c -n --slurpfile profile_doc "$tmp/profile.json" \ + --arg profile_sha "$profile_sha" --slurpfile shas "$tmp/manifest-shas.json" ' + import "portable-core-profile-graph-fixtures" as profile; + def v2: walk(if type == "object" and has("schema_version") + then .schema_version=2 else . end); + profile::resolved_profile_doc($profile_doc[0];$profile_sha;$shas[0]) | v2 | + .body.bindings |= map( + if .binding.role=="forge" then + .adapter_implementation.version="v2" | + .manifest_source=profile::source_value(profile::blob("manifests/forge.json";"a"); + "canonical-json";$shas[0].forge) + elif .binding.role=="verifier" then + .adapter_implementation.id="adapter.deterministic-verifier.v1" + else . end) +' >"$tmp/resolved.json" +resolved_sha=$(sha_file "$tmp/resolved.json") + +"${jq_command[@]}" -L "$fixtures" -S -c -n --arg resolved_sha "$resolved_sha" ' + import "portable-core-stage-request-fixtures" as request; + def v2: walk(if type == "object" and has("schema_version") + then .schema_version=2 else . end); + request::request_doc("verifier";$resolved_sha) | v2 +' >"$tmp/request.json" +request_sha=$(sha_file "$tmp/request.json") + +make_result() { + local flavor=$1 output=$2 + "${jq_command[@]}" -L "$fixtures" -S -c -n \ + --slurpfile request_doc "$tmp/request.json" --slurpfile resolved_doc "$tmp/resolved.json" \ + --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" --arg flavor "$flavor" ' + import "portable-core-result-truth-fixtures" as result; + def v2: walk(if type == "object" and has("schema_version") + then .schema_version=2 else . end); + (if $flavor=="passed" then + result::completed_result_doc($request_doc[0];$request_sha;$resolved_doc[0];$resolved_sha) + elif $flavor=="failed-check" then + result::completed_result_doc($request_doc[0];$request_sha;$resolved_doc[0];$resolved_sha) | + .body.evidence[0].verdict="failed" | + .body.outcome={family:"check",value:"failed"} + elif $flavor=="inconclusive" then + result::completed_result_doc($request_doc[0];$request_sha;$resolved_doc[0];$resolved_sha) | + .body.evidence[0].verdict="inconclusive" | + .body.outcome={family:"check",value:"inconclusive"} | + .body.reason={reason_id:"verification.inconclusive"} + elif $flavor=="stale" then + result::stale_result_doc($request_doc[0];$request_sha;$resolved_doc[0];$resolved_sha) + elif $flavor=="blocked" then + result::blocked_result_doc($request_doc[0];$request_sha;$resolved_doc[0];$resolved_sha) + elif $flavor=="stage-failed" then + result::failed_result_doc($request_doc[0];$request_sha;$resolved_doc[0];$resolved_sha) + else + result::cancelled_result_doc($request_doc[0];$request_sha;$resolved_doc[0];$resolved_sha) + end) | v2 + ' >"$output" +} + +make_input() { + local result_file=$1 output=$2 result_sha + result_sha=$(sha_file "$result_file") + "${jq_command[@]}" -S -c -n --slurpfile manifest "$tmp/manifest-verifier.json" \ + --slurpfile request_doc "$tmp/request.json" --slurpfile resolved "$tmp/resolved.json" \ + --slurpfile result_doc "$result_file" --arg manifest_sha "$verifier_sha" \ + --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" \ + --arg result_sha "$result_sha" ' + def pair($docs;$sha): {content:$docs[0],sha256:$sha}; + { + trust_context:{schema_version:1,kind:"adapter_trust_context",id:"trust.verifier", + body:{binding_id:"binding.verifier",manifest:pair($manifest;$manifest_sha), + request:pair($request_doc;$request_sha),resolved_profile:pair($resolved;$resolved_sha), + snapshot_ref:{content_id:"verifier-snapshot",media_type:"application/json", + sha256:$result_sha}}}, + snapshot:{schema_version:1,kind:"deterministic_verifier_snapshot",id:"snapshot.verifier", + body:{observed_at:"2026-08-30T00:00:04Z",result:pair($result_doc;$result_sha)}} + } + ' >"$output" +} + +for flavor in passed failed-check inconclusive stale blocked stage-failed cancelled; do + make_result "$flavor" "$tmp/result-$flavor.json" + make_input "$tmp/result-$flavor.json" "$tmp/$flavor.json" +done + +expect_state passed "$tmp/passed.json" passed +expect_state failed-check "$tmp/failed-check.json" failed +expect_state inconclusive "$tmp/inconclusive.json" inconclusive +expect_state stale "$tmp/stale.json" stale +expect_state blocked "$tmp/blocked.json" blocked +expect_state stage-failed "$tmp/stage-failed.json" failed +expect_state cancelled "$tmp/cancelled.json" cancelled + +expect_error github-actions-not-verifier \ + '.snapshot={schema_version:1,kind:"github_actions_ci_snapshot",id:"snapshot.ci",body:{}}' \ + E_SHAPE +expect_error ci-field-rejected '.snapshot.body.workflow_id="ci"' E_SHAPE +expect_error wrong-role '.trust_context.body.request.content.body.operation.role="ci"' E_TRUST +expect_error wrong-capability \ + '.trust_context.body.request.content.body.operation.capability_id="core.review.change.v1"' E_TRUST +expect_error permission-subset \ + '.trust_context.body.request.content.body.operation.permissions=["core.perm.target.read.v1"]' E_TRUST +expect_error network-not-denied \ + '.trust_context.body.request.content.body.operation.arguments.network_mode="allow"' E_TRUST +expect_error candidate-cross-repository \ + '(.trust_context.body.request.content.body.inputs[] | + select(.input_id=="input.candidate") | .value.value.value.revision.repository_id)="repo.other"' \ + E_TRUST +expect_error verification-plan-alias \ + '.trust_context.body.request.content.body.operation.arguments.verification_plan.input_id="input.candidate"' \ + E_TRUST +expect_error manifest-capability-ceiling \ + '.trust_context.body.manifest.content.body.offered_capabilities=["core.review.change.v1"]' \ + E_TRUST +expect_error manifest-config-contract \ + '.trust_context.body.manifest.content.body.config_contract_ref= + .trust_context.body.request.content.body.operation.arguments.verification_plan.ref' E_TRUST +expect_error verifier-tool \ + '(.trust_context.body.resolved_profile.content.body.bindings[] | + select(.binding.role=="verifier") | .binding.requested_tools)=[{ + tool_id:"tool.hidden",tool_version:"v1", + package_ref:.trust_context.body.resolved_profile.content.body.bindings[0].binding.package_ref, + config_ref:{state:"absent"}}]' E_TRUST +expect_error verifier-model \ + '(.trust_context.body.resolved_profile.content.body.bindings[] | + select(.binding.role=="verifier") | .binding) |= + (.execution_kind="model" | .model_request={provider_id:"p",model_id:"m",effort_id:"e"} | + .prompt_ref=.package_ref)' E_TRUST +expect_error stale-request-digest '.trust_context.body.request.sha256=("0"*64)' E_RESULT +expect_error stale-resolved-digest '.trust_context.body.resolved_profile.sha256=("0"*64)' E_TRUST +expect_error stale-manifest-digest '.trust_context.body.manifest.sha256=("0"*64)' E_TRUST +expect_error stale-snapshot-digest '.trust_context.body.snapshot_ref.sha256=("0"*64)' E_RESULT +expect_error performer-mismatch \ + '.snapshot.body.result.content.body.execution.performer.principal_id="principal.other"' E_RESULT +expect_error reporter-mismatch \ + '.snapshot.body.result.content.body.reported_by.execution_boundary_id="boundary.other"' E_RESULT +expect_error missing-deterministic-evidence \ + '.snapshot.body.result.content.body.evidence |= map(select(.kind!="deterministic"))' E_RESULT +expect_error false-pass-over-failure \ + '(.snapshot.body.result.content.body.evidence[0].verdict)="failed"' E_RESULT +expect_error verifier-output \ + '.snapshot.body.result.content.body.outputs=[{output_id:"forbidden", + ref:{content_id:"forbidden",media_type:"application/json",sha256:("a"*64)}}]' E_RESULT +expect_error observation-before-result \ + '.snapshot.body.observed_at="2026-08-30T00:00:02Z"' E_RESULT + +normalize "$tmp/passed.json" >"$tmp/repeat-a.json" +normalize "$tmp/passed.json" >"$tmp/repeat-b.json" +check canonical-repeat /usr/bin/cmp -s "$tmp/repeat-a.json" "$tmp/repeat-b.json" +check canonical-output /usr/bin/cmp -s "$tmp/repeat-a.json" \ + <("${jq_command[@]}" -S -c . "$tmp/repeat-a.json") +check inactive-no-authority-effects "${jq_command[@]}" -e ' + .adapter=={id:"adapter.deterministic-verifier.v1",version:"v1",status:"inactive"} and + .authority=="none" and .effects==[] and + .qualification=={state:"unavailable",reason_id:"adapter.unqualified"} +' "$tmp/repeat-a.json" +check exact-candidate-and-plan "${jq_command[@]}" -e \ + --slurpfile input "$tmp/passed.json" ' + .observation.candidate_input == + ($input[0].trust_context.body.request.content.body.inputs[] | + select(.input_id=="input.candidate")) and + .observation.verification_plan == + $input[0].trust_context.body.request.content.body.operation.arguments.verification_plan +' "$tmp/repeat-a.json" +check no-ci-projection "${jq_command[@]}" -e ' + ([.. | objects | keys[]] as $keys | + all(["app_id","check_run_id","check_suite_id","job_id","run_id","workflow_id"][]; + . as $key | $keys | index($key)==null)) +' "$tmp/repeat-a.json" +check no-selected-generation /usr/bin/env sh -c \ + '! grep -E "g-[0-9a-f]{64}" "$@"' sh "$normalizer" \ + "$root/scripts/test/default-deterministic-verifier-adapter.test.sh" +check pure-offline-normalizer /usr/bin/env sh -c \ + '! grep -Ei "github|actions|curl|graphql|https?://|@sh|system[(]|getenv|credential|token" "$1"' \ + sh "$normalizer" + +/usr/bin/printf 'default deterministic verifier adapter: %s/%s checks passed\n' \ + "$passed" "$passed" From 2fb72cb9d1b8da3909176393be9f721262a8caaf Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 15:11:04 -0400 Subject: [PATCH 2/5] Preserve merged adapter documentation --- README.md | 4 ++-- RESTORE.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0f80354..e9a2a2f 100644 --- a/README.md +++ b/README.md @@ -242,8 +242,8 @@ or inconclusive state. Provider names, text, and details stay opaque data. This PR lands only the immutable normalizer payload. A later assembly PR can add its manifest and default-set wiring after this payload has a durable commit on main. The payload is offline and unqualified. It does not call GitHub or a CLI, - use a credential, rerun or cancel work, dispatch a workflow, change a repository, - grant authority or qualification, or activate a profile. +use a credential, rerun or cancel work, dispatch a workflow, change a repository, +grant authority or qualification, or activate a profile. ## Inactive deterministic verifier normalizer payload diff --git a/RESTORE.md b/RESTORE.md index aedde02..7580949 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -476,8 +476,8 @@ normalization, opaque provider data, and fail-closed malformed, contradictory, incomplete, or stale input. This stage intentionally has no adapter manifest. A later assembly PR can bind the payload through a durable main commit and add default-set wiring. The pure jq payload is offline and unqualified. It does not - call GitHub, use a credential, rerun, cancel, or dispatch work, change a - repository, grant authority or qualification, or activate a profile. +call GitHub, use a credential, rerun, cancel, or dispatch work, change a +repository, grant authority or qualification, or activate a profile. Restore the two paths in the manifest's inactive deterministic verifier normalizer payload block, then run: From 4afe8c64b92bebcb6d642cd1ba3faf7ea64b6764 Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 17:50:01 -0400 Subject: [PATCH 3/5] Bind verifier evidence to caller expectations --- README.md | 5 +- RESTORE.md | 9 ++- .../deterministic-verifier/v1/normalize.jq | 53 +++++++++---- ...ult-deterministic-verifier-adapter.test.sh | 76 ++++++++++++++++--- 4 files changed, 113 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index d192f6b..2f644fc 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,10 @@ profile. portable-core v2 verifier request, resolved profile, adapter contract, and stage result. It reuses the core's request, profile, and result relations, then returns one canonical observation. It does not interpret provider or CI status as verifier -evidence. GitHub Actions remains a separate CI observation boundary. +evidence. GitHub Actions remains a separate CI observation boundary. The caller +first canonicalizes and hashes the snapshot and stage result, supplies those +verified content-and-digest pairs, and fixes the expected attempt ID and number. +The normalizer rejects any mismatch before emitting their references. This payload is offline and unqualified. It does not execute a candidate or tool, read proof bytes, enforce a sandbox, use a credential or network, write evidence, diff --git a/RESTORE.md b/RESTORE.md index 2eb9713..e63c0fe 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -506,11 +506,12 @@ bash scripts/test/default-deterministic-verifier-adapter.test.sh This validates exact core-v2 verifier request/profile/result relations, the deterministic role and permission ceiling, candidate and verification-plan binding, +exact caller-verified snapshot and result pairs, attempt identity, timestamp and evidence precedence, stale inputs, and the boundary that keeps CI observations out -of verifier evidence. This stage intentionally has no adapter manifest. The pure -jq payload is offline and unqualified. It does not execute a candidate or tool, -read proof bytes, enforce a sandbox, use a credential or network, write evidence, -grant authority or qualification, or activate a profile. +of verifier evidence. This stage intentionally has no adapter manifest. The pure jq +payload is offline and unqualified. It does not execute a candidate or tool, read +proof bytes, enforce a sandbox, use a credential or network, write evidence, grant +authority or qualification, or activate a profile. --- diff --git a/adapters/deterministic-verifier/v1/normalize.jq b/adapters/deterministic-verifier/v1/normalize.jq index e4e495c..15fbf46 100644 --- a/adapters/deterministic-verifier/v1/normalize.jq +++ b/adapters/deterministic-verifier/v1/normalize.jq @@ -14,28 +14,44 @@ def document_ref($pair): sha256:$pair.sha256 }; +def snapshot_ref($pair): + { + content_id:"deterministic-verifier-snapshot", + media_type:"application/json", + sha256:$pair.sha256 + }; + +def snapshot_shape_ok: + schema::exact_fields(["body","id","kind","schema_version"];[]) and + .schema_version == 1 and .kind == "deterministic_verifier_snapshot" and + (.id | schema::id_ok) and + (.body | + schema::exact_fields(["observed_at","result"];[]) and + (.observed_at | schema::time_ok) and + (.result | profile::document_pair_ok("stage_result"))); + +def verified_snapshot_pair_ok: + schema::exact_fields(["content","sha256"];[]) and + (.content | snapshot_shape_ok) and (.sha256 | schema::sha256_ok); + def trust_context_shape_ok: schema::exact_fields(["body","id","kind","schema_version"];[]) and .schema_version == 1 and .kind == "adapter_trust_context" and (.id | schema::id_ok) and (.body | schema::exact_fields( - ["binding_id","manifest","request","resolved_profile","snapshot_ref"];[]) and + ["binding_id","expected_attempt_id","expected_attempt_number","manifest", + "request","resolved_profile","verified_result","verified_snapshot"]; + []) and (.binding_id | schema::id_ok) and + (.expected_attempt_id | schema::id_ok) and + (.expected_attempt_number | schema::int_ok) and + .expected_attempt_number >= 1 and (.manifest | profile::document_pair_ok("adapter_manifest")) and (.request | profile::document_pair_ok("stage_request")) and (.resolved_profile | profile::document_pair_ok("resolved_profile")) and - (.snapshot_ref | schema::content_ref_ok) and - .snapshot_ref.media_type == "application/json"); - -def snapshot_shape_ok: - schema::exact_fields(["body","id","kind","schema_version"];[]) and - .schema_version == 1 and .kind == "deterministic_verifier_snapshot" and - (.id | schema::id_ok) and - (.body | - schema::exact_fields(["observed_at","result"];[]) and - (.observed_at | schema::time_ok) and - (.result | profile::document_pair_ok("stage_result"))); + (.verified_result | profile::document_pair_ok("stage_result")) and + (.verified_snapshot | verified_snapshot_pair_ok)); def input_shape_ok: schema::exact_fields(["snapshot","trust_context"];[]) and @@ -90,7 +106,12 @@ def trust_relations_ok($trust): $request_pair.content.body.operation.arguments.network_mode == "deny"; def snapshot_relations_ok($trust; $snapshot): - $trust.body.snapshot_ref.sha256 == $snapshot.body.result.sha256 and + $snapshot == $trust.body.verified_snapshot.content and + $snapshot.body.result == $trust.body.verified_result and + $snapshot.body.result.content.body.attempt_id == + $trust.body.expected_attempt_id and + $snapshot.body.result.content.body.attempt_number == + $trust.body.expected_attempt_number and result::stage_run_ok( $trust.body.request;$trust.body.resolved_profile;$snapshot.body.result) and (if $snapshot.body.result.content.body | has("execution") then @@ -131,11 +152,13 @@ def observation($trust; $snapshot): state:normalized_state($result_body), reason_id:normalized_reason($result_body), trust_context:{ - snapshot_ref:$trust.body.snapshot_ref, + snapshot_ref:snapshot_ref($trust.body.verified_snapshot), manifest_ref:document_ref($trust.body.manifest), request_ref:document_ref($trust.body.request), resolved_profile_ref:document_ref($trust.body.resolved_profile), - binding_id:$trust.body.binding_id + binding_id:$trust.body.binding_id, + expected_attempt_id:$trust.body.expected_attempt_id, + expected_attempt_number:$trust.body.expected_attempt_number }, observation:{ observed_at:$snapshot.body.observed_at, diff --git a/scripts/test/default-deterministic-verifier-adapter.test.sh b/scripts/test/default-deterministic-verifier-adapter.test.sh index 0672fec..09c2082 100755 --- a/scripts/test/default-deterministic-verifier-adapter.test.sh +++ b/scripts/test/default-deterministic-verifier-adapter.test.sh @@ -192,22 +192,31 @@ make_result() { } make_input() { - local result_file=$1 output=$2 result_sha + local result_file=$1 output=$2 result_sha snapshot_file snapshot_sha result_sha=$(sha_file "$result_file") + snapshot_file="${output%.json}.snapshot.json" + "${jq_command[@]}" -S -c -n --slurpfile result_doc "$result_file" \ + --arg result_sha "$result_sha" ' + def pair($docs;$sha): {content:$docs[0],sha256:$sha}; + {schema_version:1,kind:"deterministic_verifier_snapshot",id:"snapshot.verifier", + body:{observed_at:"2026-08-30T00:00:04Z",result:pair($result_doc;$result_sha)}} + ' >"$snapshot_file" + snapshot_sha=$(sha_file "$snapshot_file") "${jq_command[@]}" -S -c -n --slurpfile manifest "$tmp/manifest-verifier.json" \ --slurpfile request_doc "$tmp/request.json" --slurpfile resolved "$tmp/resolved.json" \ - --slurpfile result_doc "$result_file" --arg manifest_sha "$verifier_sha" \ + --slurpfile result_doc "$result_file" --slurpfile snapshot_doc "$snapshot_file" \ + --arg manifest_sha "$verifier_sha" \ --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" \ - --arg result_sha "$result_sha" ' + --arg result_sha "$result_sha" --arg snapshot_sha "$snapshot_sha" ' def pair($docs;$sha): {content:$docs[0],sha256:$sha}; { trust_context:{schema_version:1,kind:"adapter_trust_context",id:"trust.verifier", - body:{binding_id:"binding.verifier",manifest:pair($manifest;$manifest_sha), + body:{binding_id:"binding.verifier",expected_attempt_id:"attempt.example", + expected_attempt_number:1,manifest:pair($manifest;$manifest_sha), request:pair($request_doc;$request_sha),resolved_profile:pair($resolved;$resolved_sha), - snapshot_ref:{content_id:"verifier-snapshot",media_type:"application/json", - sha256:$result_sha}}}, - snapshot:{schema_version:1,kind:"deterministic_verifier_snapshot",id:"snapshot.verifier", - body:{observed_at:"2026-08-30T00:00:04Z",result:pair($result_doc;$result_sha)}} + verified_result:pair($result_doc;$result_sha), + verified_snapshot:pair($snapshot_doc;$snapshot_sha)}}, + snapshot:$snapshot_doc[0] } ' >"$output" } @@ -263,7 +272,17 @@ expect_error verifier-model \ expect_error stale-request-digest '.trust_context.body.request.sha256=("0"*64)' E_RESULT expect_error stale-resolved-digest '.trust_context.body.resolved_profile.sha256=("0"*64)' E_TRUST expect_error stale-manifest-digest '.trust_context.body.manifest.sha256=("0"*64)' E_TRUST -expect_error stale-snapshot-digest '.trust_context.body.snapshot_ref.sha256=("0"*64)' E_RESULT +expect_error moved-untrusted-snapshot '.snapshot.id="snapshot.moved"' E_RESULT +expect_error moved-verified-snapshot \ + '.trust_context.body.verified_snapshot.content.id="snapshot.moved"' E_RESULT +expect_error moved-verified-result \ + '.trust_context.body.verified_result.sha256=("0"*64)' E_RESULT +expect_error malformed-verified-snapshot-digest \ + '.trust_context.body.verified_snapshot.sha256=("A"*64)' E_SHAPE +expect_error expected-attempt-id \ + '.trust_context.body.expected_attempt_id="attempt.other"' E_RESULT +expect_error expected-attempt-number \ + '.trust_context.body.expected_attempt_number=2' E_RESULT expect_error performer-mismatch \ '.snapshot.body.result.content.body.execution.performer.principal_id="principal.other"' E_RESULT expect_error reporter-mismatch \ @@ -277,6 +296,21 @@ expect_error verifier-output \ ref:{content_id:"forbidden",media_type:"application/json",sha256:("a"*64)}}]' E_RESULT expect_error observation-before-result \ '.snapshot.body.observed_at="2026-08-30T00:00:02Z"' E_RESULT +expect_error terminal-before-start ' + .snapshot.body.result.content.body.finished_at="2026-08-29T23:59:59Z" | + .trust_context.body.verified_result=.snapshot.body.result | + .trust_context.body.verified_snapshot.content=.snapshot' E_RESULT +expect_error invalid-proof-media ' + .snapshot.body.result.content.body.evidence[0].proof_ref.media_type="INVALID" | + .trust_context.body.verified_result=.snapshot.body.result | + .trust_context.body.verified_snapshot.content=.snapshot' E_RESULT +expect_error non-core-content-id ' + .snapshot.body.result.content.body.evidence[0].proof_ref.content_id="proof:invalid" | + .trust_context.body.verified_result=.snapshot.body.result | + .trust_context.body.verified_snapshot.content=.snapshot' E_RESULT +expect_error opaque-metadata-rejected ' + .snapshot.body.provider_metadata={nested:{message:("x"*9000)}} | + .trust_context.body.verified_snapshot.content=.snapshot' E_SHAPE normalize "$tmp/passed.json" >"$tmp/repeat-a.json" normalize "$tmp/passed.json" >"$tmp/repeat-b.json" @@ -294,8 +328,30 @@ check exact-candidate-and-plan "${jq_command[@]}" -e \ ($input[0].trust_context.body.request.content.body.inputs[] | select(.input_id=="input.candidate")) and .observation.verification_plan == - $input[0].trust_context.body.request.content.body.operation.arguments.verification_plan + $input[0].trust_context.body.request.content.body.operation.arguments.verification_plan and + .trust_context.expected_attempt_id == + $input[0].trust_context.body.expected_attempt_id and + .trust_context.expected_attempt_number == + $input[0].trust_context.body.expected_attempt_number and + .trust_context.snapshot_ref.sha256 == + $input[0].trust_context.body.verified_snapshot.sha256 and + .observation.result.result_ref.sha256 == + $input[0].trust_context.body.verified_result.sha256 ' "$tmp/repeat-a.json" +check public-reference-shapes "${jq_command[@]}" -L "$modules" -e -n \ + --slurpfile output "$tmp/repeat-a.json" ' + import "schema" as schema; + ($output[0].trust_context.snapshot_ref | schema::content_ref_ok) and + ($output[0].trust_context.manifest_ref | + schema::document_ref_kind_ok("adapter_manifest")) and + ($output[0].trust_context.request_ref | + schema::document_ref_kind_ok("stage_request")) and + ($output[0].trust_context.resolved_profile_ref | + schema::document_ref_kind_ok("resolved_profile")) and + ($output[0].observation.result.result_ref | + schema::document_ref_kind_ok("stage_result")) and + all($output[0].observation.result.evidence[];.proof_ref | schema::content_ref_ok) + ' check no-ci-projection "${jq_command[@]}" -e ' ([.. | objects | keys[]] as $keys | all(["app_id","check_run_id","check_suite_id","job_id","run_id","workflow_id"][]; From 466d658cb7ab8caf160b1c7cc7bc3bfa589f5bc9 Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 18:34:06 -0400 Subject: [PATCH 4/5] Allow verifier adapter schema imports --- scripts/test/portable-core-schema.test.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/test/portable-core-schema.test.sh b/scripts/test/portable-core-schema.test.sh index 0f3c892..7c6ce37 100755 --- a/scripts/test/portable-core-schema.test.sh +++ b/scripts/test/portable-core-schema.test.sh @@ -776,8 +776,10 @@ schema_import_path_ok() { local import_path="$1" local test_path case "$import_path" in + adapters/deterministic-verifier/v1/normalize.jq|\ orchestrator/v1/reconciliation-plan.jq|orchestrator/v1/state-scanner.jq) ;; scripts/test/default-codex-native-reviewer-adapter.test.sh|\ + scripts/test/default-deterministic-verifier-adapter.test.sh|\ scripts/test/default-github-forge-adapter.test.sh) ;; scripts/test/portable-core-*) test_path="${import_path#scripts/test/}" From 5c9f070abb17d0734ceab8461ceb0e38904cb2a4 Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 22:52:17 -0400 Subject: [PATCH 5/5] Rebind verifier test to selected core generation --- .../default-deterministic-verifier-adapter.test.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/test/default-deterministic-verifier-adapter.test.sh b/scripts/test/default-deterministic-verifier-adapter.test.sh index 09c2082..799512f 100755 --- a/scripts/test/default-deterministic-verifier-adapter.test.sh +++ b/scripts/test/default-deterministic-verifier-adapter.test.sh @@ -27,9 +27,15 @@ jq_command=("$jq_bin") if [ "$platform" = Darwin:arm64 ]; then jq_command=(/usr/bin/arch -x86_64 "$jq_bin"); fi [ "$("${jq_command[@]}" --version)" = jq-1.6 ] || fail 'jq version' -generation=$("${jq_command[@]}" -er \ - 'select(type=="array" and length==1) | .[0].generation_id' \ - "$root/core/v2/generation-registry.json") +generation=$(/usr/bin/sed -n \ + "s/^PORTABLE_CORE_GENERATION='\(g-[0-9a-f]\{64\}\)'$/\1/p" \ + "$root/scripts/core-contract.sh") +[[ "$generation" =~ ^g-[0-9a-f]{64}$ ]] || fail 'selected core generation' +"${jq_command[@]}" -e --arg generation "$generation" ' + [.[] | select(.generation_id == $generation and + .semantic_identity == "core.contracts.v2")] | length == 1 +' "$root/core/v2/generation-registry.json" >/dev/null || + fail 'selected core registry identity' modules="$root/core/v2/generations/$generation/modules" [ -d "$modules" ] && [ ! -L "$modules" ] || fail 'selected core modules'