diff --git a/README.md b/README.md index 8d77199..a428b39 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,20 @@ The stable `scripts/core-contract.sh` wrapper and inactive resolver select this generation together. This is a repo-only compatibility switch. It does not install the resolver, select a live profile, or qualify a real forge. +## Inactive fake adapter contract matrix + +`adapter-tests/v1/` runs a fixed 2×2 producer/forge matrix against one unrelated +local Git fixture. Its accepted inventory and four distinct fake entrypoints are +digest-pinned. The runner revalidates each resolved profile with portable core v2, +validates every producer and forge stage request/result, then checks payload links, +package, target, candidate, receipt, and Git identities itself. + +The result is observation only. It grants no authority, qualification, approval, +publish, merge, or branch-write capability. The fakes run with a cleared environment, +fixed limits, and disposable directories, but this test does not provide or claim +mechanical network or host-filesystem isolation. Real-adapter sandbox qualification +and an external-target smoke remain required. + ## 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 d71b954..2066160 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -308,6 +308,17 @@ materialization in a caller-disposable repository. The package is not qualified It grants no credential, network, publish, push, merge, or remote branch-write capability. The switch does not install the resolver or select a live profile. +Restore every path in the manifest's inactive fake adapter matrix block, then run: + +```sh +bash scripts/test/portable-adapter-contracts.test.sh +``` + +The proof resolves four fake-only profiles, validates both core stage records in +each cell, and checks the accepted 2×2 matrix plus closed negative protocol cases. +It uses no real adapter or credential and makes no network/host isolation, +qualification, activation, or external-target-smoke claim. + --- ## 5. Smoke test — prove the rebuilt team is alive diff --git a/adapter-tests/v1/contract.jq b/adapter-tests/v1/contract.jq new file mode 100644 index 0000000..a5988b7 --- /dev/null +++ b/adapter-tests/v1/contract.jq @@ -0,0 +1,177 @@ +import "portable-core-stage-request-fixtures" as request; +import "portable-core-result-truth-fixtures" as result; + +def exact($keys): type == "object" and (keys | sort) == ($keys | sort); +def sha256: type == "string" and test("\\A[0-9a-f]{64}\\z"); +def v2: walk(if type == "object" and has("schema_version") then .schema_version = 2 else . end); +def payload_ok: + exact(["data","media_type","payload_id","sha256"]) and + (.data | type == "string") and (.media_type | type == "string") and + (.payload_id | type == "string") and (.sha256 | sha256); +def positive_assertions: + ["audit-projection","candidate-git","core-validation","environment-clean", + "evidence-projection","gate-projection","outcome-projection", + "risk-projection","target-git"]; +def expected_cases($fixture): + ["aa","ab","ba","bb"] | map(. as $cell | { + case_id:("matrix-"+$cell),phase:"pipeline", + producer_package_id:("fake.producer."+($cell[0:1])), + forge_package_id:("fake.forge."+($cell[1:2])),expected_verdict:"pass", + expected_error:"",equivalence_group:"portable-fake-v1", + assertions:positive_assertions,fixture_sha256:$fixture + }) + [ + {case_id:"reject-degraded",expected_error:"E_DEGRADED"}, + {case_id:"reject-empty",expected_error:"E_EMPTY"}, + {case_id:"reject-malformed",expected_error:"E_MALFORMED"}, + {case_id:"reject-partial",expected_error:"E_PARTIAL"}, + {case_id:"reject-relabelled",expected_error:"E_RELABELLED"}, + {case_id:"reject-timeout",expected_error:"E_TIMEOUT"}, + {case_id:"reject-transport",expected_error:"E_TRANSPORT"}, + {case_id:"reject-multiple",expected_error:"E_MULTIPLE"}, + {case_id:"reject-unlinked",expected_error:"E_UNLINKED_PAYLOAD"}, + {case_id:"reject-duplicate",expected_error:"E_DUPLICATE_PAYLOAD"}, + {case_id:"reject-descendant",expected_error:"E_DESCENDANT"} + ] | map(if has("phase") then . else . + { + phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none", + expected_verdict:"reject",equivalence_group:"protocol-negative", + assertions:["expected-error"],fixture_sha256:$fixture + } end); +def inventory_ok: + (.inventory | exact(["authorization_ref","cases","protocol","schema_version"])) and + .inventory.schema_version == 1 and + .inventory.protocol == "ystack.adapter-contract.inventory.v1" and + .inventory.authorization_ref == + {comment_id:5476938197,issue_number:153,scope_comment_id:5474023028} and + .inventory.cases == expected_cases(.fixture_sha256); + +def revision($c): + {repository_id:"fixture.target",hash_algorithm:"sha1",commit_id:$c.target_commit}; +def source_ref($c): + {revision:revision($c),location:{kind:"path",value:"source.txt"},object_type:"blob", + object_id:$c.target_object,mode:"100644"}; +def tree_ref($c): + {revision:revision($c),location:{kind:"root"},object_type:"tree", + object_id:$c.target_tree,mode:"040000"}; +def target_request($c): + .body.target_repository_id="fixture.target" | + .body.target_revision={state:"present",value:revision($c)} | + .body.base={state:"present",value:revision($c)} | + .body.source={state:"present",value:{type:"git-object",value:source_ref($c)}}; +def producer_request($c): + request::request_doc("producer";$c.resolved_sha) | v2 | + .id=("request."+$c.case_id+".producer") | + .body.stage_id=("stage."+$c.case_id+".producer") | + .body.resolved_profile_ref.id=$c.resolved_id | + .body.selection_ref=$c.resolved_profile.body.selection_ref | + .body.repository_context_ref=$c.resolved_profile.body.repository_context_ref | + target_request($c) | + (.body.inputs[] | select(.input_id=="input.output") | .input_id)="input.allowed-delta" | + .body.inputs |= sort_by(.input_id) | + .body.operation.arguments={artifact_kind:"git-patch", + allowed_delta:request::delivered("allowed-delta";"allowed-delta";request::sha("3"))} | + .body.operation.arguments.allowed_delta.ref.subject_ref as $allowed | + (.body.inputs[] | select(.input_id=="input.allowed-delta") | .value)=$allowed; +def forge_request($c): + request::request_doc("producer";$c.resolved_sha) | v2 | + .id=("request."+$c.case_id+".forge") | + .body.stage_id=("stage."+$c.case_id+".forge") | + .body.resolved_profile_ref.id=$c.resolved_id | + .body.selection_ref=$c.resolved_profile.body.selection_ref | + .body.repository_context_ref=$c.resolved_profile.body.repository_context_ref | + target_request($c) | + .body.inputs=([ + request::named_content_input("finish";request::sha("1")), + request::named_content_input("materialize";request::sha("3")), + request::named_content_input("producer-patch";$c.patch_sha), + {input_id:"input.source-tree",value:{type:"artifact",value:{type:"git-object",value:tree_ref($c)}}}, + request::named_content_input("verify";request::sha("2")) + ] | sort_by(.input_id)) | + .body.operation={role:"forge",binding_id:"binding.forge", + capability_id:"core.forge.materialize-candidate.v2", + permissions:["core.perm.candidate-repository.write.v2","core.perm.evidence.write.v1", + "core.perm.scratch.write.v1","core.perm.target.read.v1"], + arguments:{source_tree_input_id:"input.source-tree",candidate_output_id:"candidate.repository", + materialization_contract:request::delivered("output-contract";"materialize";request::sha("3")), + network_mode:"deny"}} | + .body.required_evidence_kinds=["deterministic"]; +def stage_result($c; $output_id; $media_type; $payload_sha): + result::completed_result_doc($c.stage_request;$c.request_sha; + $c.resolved_profile;$c.resolved_sha) | v2 | + .id=("result."+$c.case_id+"."+$c.phase) | + .body.outcome={family:"change",value:"changed"} | + .body.outputs=[{output_id:$output_id, + ref:{content_id:("output."+$c.case_id+"."+$c.phase),media_type:$media_type,sha256:$payload_sha}}] | + if $c.phase == "producer" then .body.delta_ref=.body.outputs[0].ref else . end; +def request_envelope_ok: + . as $request | + exact(["case_id","payloads","phase","protocol_version","stage_request"]) and + .protocol_version == 1 and (.phase == "producer" or .phase == "forge") and + .stage_request.schema_version == 2 and .stage_request.kind == "stage_request" and + (.payloads | type == "array" and all(.[];payload_ok) and + ([.[].payload_id] | length == (unique | length))) and + ([.payloads[].payload_id] | sort) == + (if .phase == "producer" then ["resolved-profile","source"] + else ["producer.patch","resolved-profile","source"] end) and + ([.payloads[] | select(.payload_id=="resolved-profile" and + .media_type=="application/json" and + .sha256==$request.stage_request.body.resolved_profile_ref.sha256)] | length) == 1 and + ([.payloads[] | select(.payload_id=="source" and .media_type=="text/plain")] | length) == 1 and + (if .phase == "forge" then + ([.stage_request.body.inputs[] | + select(.input_id=="input.producer-patch") | + .value.value.value.sha256] | length) == 1 and + ([.payloads[] as $payload | + select($payload.payload_id=="producer.patch" and $payload.media_type=="text/x-diff" and + ([$request.stage_request.body.inputs[] | + select(.input_id=="input.producer-patch" and + .value.value.value.sha256==$payload.sha256)] | length == 1))] | length) == 1 + else true end); +def response_envelope_ok: + (.response | exact(["case_id","payloads","phase","protocol_version","stage_result"])) and + .response.protocol_version == 1 and .response.case_id == .case_id and + .response.phase == .phase and .response.stage_result.schema_version == 2 and + .response.stage_result.kind == "stage_result" and + (.response.payloads | type == "array" and all(.[];payload_ok)) and + ([.response.payloads[].payload_id] | length == (unique | length)) and + ([.response.payloads[].payload_id] | sort) == + ([.response.stage_result.body.outputs[].output_id] | sort) and + .response as $response | + all($response.payloads[]; + . as $payload | + [$response.stage_result.body.outputs[] | + select(.output_id==$payload.payload_id and .ref.media_type==$payload.media_type and + .ref.sha256==$payload.sha256)] | length == 1); +def response_error: + if (.response | type) != "object" then "E_PARTIAL" + elif .response.status? == "degraded" then "E_DEGRADED" + elif (.response | exact(["case_id","payloads","phase","protocol_version","stage_result"]) | not) + then "E_PARTIAL" + elif .response.case_id != .case_id or .response.phase != .phase then "E_RELABELLED" + elif (.response.payloads | type) != "array" then "E_PARTIAL" + elif ([.response.payloads[].payload_id] as $ids | + ($ids | length) != ($ids | unique | length)) + then "E_DUPLICATE_PAYLOAD" + elif ([.response.payloads[].payload_id] | sort) != + ([.response.stage_result.body.outputs[]?.output_id] | sort) + then "E_UNLINKED_PAYLOAD" + elif (response_envelope_ok | not) then "E_PARTIAL" + else "" end; +def projection: + {artifact_sha256:.artifact_sha256,risk:.producer_request.body.risk, + gate_refs:.producer_request.body.risk.required_gate_refs, + outcome:[.producer_result.body.outcome,.forge_result.body.outcome], + evidence:[.producer_result.body.evidence,.forge_result.body.evidence], + audit:{producer_status:.producer_result.body.status,forge_status:.forge_result.body.status, + producer_attempt:.producer_result.body.attempt_number, + forge_attempt:.forge_result.body.attempt_number, + target_tree_id:.target_tree,candidate_tree_id:.candidate_tree}}; + +if $command == "inventory" then inventory_ok +elif $command == "producer-request" then producer_request(.) +elif $command == "forge-request" then forge_request(.) +elif $command == "stage-result" then stage_result(.;.output_id;.media_type;.payload_sha) +elif $command == "request-envelope" then request_envelope_ok +elif $command == "response-envelope" then response_envelope_ok +elif $command == "response-error" then response_error +elif $command == "projection" then projection +else error("unknown-command") end diff --git a/adapter-tests/v1/fakes/forge-a.sh b/adapter-tests/v1/fakes/forge-a.sh new file mode 100755 index 0000000..83650d6 --- /dev/null +++ b/adapter-tests/v1/fakes/forge-a.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +[ "$#" -eq 4 ] || exit 64 +[ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 +request=$1 jq_bin=$2 contract=$3 modules=$4 +"$jq_bin" -L "${contract%/*}" -L "$modules" -e --arg command request-envelope \ + -f "$contract" "$request" >/dev/null +"$jq_bin" -S -c '.stage_request' "$request" > "$HOME/stage-request.json" +"$jq_bin" -j '.payloads[] | select(.payload_id=="resolved-profile") | .data' \ + "$request" > "$HOME/resolved.json" +"$jq_bin" -j '.payloads[] | select(.payload_id=="source") | .data' "$request" > "$HOME/source.txt" +"$jq_bin" -j '.payloads[] | select(.payload_id=="producer.patch") | .data' "$request" > "$HOME/producer.patch" +patch_sha=$(/usr/bin/shasum -a 256 "$HOME/producer.patch" | /usr/bin/awk '{print $1}') +candidate="$HOME/candidate" +/bin/mkdir -m 700 "$candidate" +/usr/bin/git init -q "$candidate" +/bin/cp "$HOME/source.txt" "$candidate/source.txt" +/usr/bin/git -C "$candidate" add source.txt +/usr/bin/git -C "$candidate" commit -q -m source +/usr/bin/git -C "$candidate" apply "$HOME/producer.patch" +/usr/bin/git -C "$candidate" add source.txt +/usr/bin/git -C "$candidate" commit -q -m candidate +commit_id=$(/usr/bin/git -C "$candidate" rev-parse HEAD) +tree_id=$(/usr/bin/git -C "$candidate" rev-parse 'HEAD^{tree}') +object_id=$(/usr/bin/git -C "$candidate" rev-parse HEAD:source.txt) +source_tree=$("$jq_bin" -r '.stage_request.body.inputs[] | select(.input_id=="input.source-tree") | .value.value.value.object_id' "$request") +"$jq_bin" -S -c -n --arg source_tree "$source_tree" --arg patch "$patch_sha" \ + --arg commit "$commit_id" --arg tree "$tree_id" --arg object "$object_id" ' + {candidate_commit_id:$commit,candidate_object_id:$object,candidate_tree_id:$tree, + patch_sha256:$patch,source_tree_id:$source_tree}' > "$HOME/receipt.json" +receipt_sha=$(/usr/bin/shasum -a 256 "$HOME/receipt.json" | /usr/bin/awk '{print $1}') +request_sha=$(/usr/bin/shasum -a 256 "$HOME/stage-request.json" | /usr/bin/awk '{print $1}') +resolved_sha=$(/usr/bin/shasum -a 256 "$HOME/resolved.json" | /usr/bin/awk '{print $1}') +"$jq_bin" -S -c -n --slurpfile stage_request "$HOME/stage-request.json" \ + --slurpfile resolved "$HOME/resolved.json" --arg case_id "$("$jq_bin" -r '.case_id' "$request")" \ + --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" --arg payload_sha "$receipt_sha" \ + '{case_id:$case_id,phase:"forge",stage_request:$stage_request[0],request_sha:$request_sha, + resolved_profile:$resolved[0],resolved_sha:$resolved_sha,output_id:"candidate.repository", + media_type:"application/json",payload_sha:$payload_sha}' > "$HOME/result-context.json" +"$jq_bin" -L "${contract%/*}" -L "$modules" -S -c --arg command stage-result \ + -f "$contract" "$HOME/result-context.json" > "$HOME/stage-result.json" +"$jq_bin" -S -c -n --slurpfile result "$HOME/stage-result.json" --rawfile receipt "$HOME/receipt.json" \ + --arg case_id "$("$jq_bin" -r '.case_id' "$request")" --arg receipt_sha "$receipt_sha" ' + {case_id:$case_id,payloads:[{data:$receipt,media_type:"application/json", + payload_id:"candidate.repository",sha256:$receipt_sha}],phase:"forge",protocol_version:1, + stage_result:$result[0]}' diff --git a/adapter-tests/v1/fakes/forge-b.sh b/adapter-tests/v1/fakes/forge-b.sh new file mode 100755 index 0000000..2196a94 --- /dev/null +++ b/adapter-tests/v1/fakes/forge-b.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +[ "$#" -eq 4 ] || exit 64 +[ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 +request=$1 jq_bin=$2 contract=$3 modules=$4 +"$jq_bin" -L "${contract%/*}" -L "$modules" -e --arg command request-envelope \ + -f "$contract" "$request" >/dev/null +"$jq_bin" -S -c '.stage_request' "$request" > "$HOME/stage-request.json" +"$jq_bin" -j '.payloads[] | select(.payload_id=="resolved-profile") | .data' \ + "$request" > "$HOME/resolved.json" +"$jq_bin" -j '.payloads[] | select(.payload_id=="source") | .data' "$request" > "$HOME/source.txt" +"$jq_bin" -j '.payloads[] | select(.payload_id=="producer.patch") | .data' "$request" > "$HOME/producer.patch" +patch_sha=$(/usr/bin/shasum -a 256 "$HOME/producer.patch" | /usr/bin/awk '{print $1}') +candidate="$HOME/candidate" +/bin/mkdir -m 700 "$candidate" +/usr/bin/git init -q "$candidate" +/bin/cp "$HOME/source.txt" "$candidate/source.txt" +/usr/bin/git -C "$candidate" add source.txt +/usr/bin/git -C "$candidate" commit -q -m source +/usr/bin/git -C "$candidate" apply --index "$HOME/producer.patch" +/usr/bin/git -C "$candidate" commit -q -m candidate +commit_id=$(/usr/bin/git -C "$candidate" rev-parse HEAD) +tree_id=$(/usr/bin/git -C "$candidate" rev-parse 'HEAD^{tree}') +object_id=$(/usr/bin/git -C "$candidate" rev-parse HEAD:source.txt) +source_tree=$("$jq_bin" -r '.stage_request.body.inputs[] | select(.input_id=="input.source-tree") | .value.value.value.object_id' "$request") +"$jq_bin" -S -c -n --arg source_tree "$source_tree" --arg patch "$patch_sha" \ + --arg commit "$commit_id" --arg tree "$tree_id" --arg object "$object_id" ' + {candidate_commit_id:$commit,candidate_object_id:$object,candidate_tree_id:$tree, + patch_sha256:$patch,source_tree_id:$source_tree}' > "$HOME/receipt.json" +receipt_sha=$(/usr/bin/shasum -a 256 "$HOME/receipt.json" | /usr/bin/awk '{print $1}') +request_sha=$(/usr/bin/shasum -a 256 "$HOME/stage-request.json" | /usr/bin/awk '{print $1}') +resolved_sha=$(/usr/bin/shasum -a 256 "$HOME/resolved.json" | /usr/bin/awk '{print $1}') +"$jq_bin" -S -c -n --slurpfile stage_request "$HOME/stage-request.json" \ + --slurpfile resolved "$HOME/resolved.json" --arg case_id "$("$jq_bin" -r '.case_id' "$request")" \ + --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" --arg payload_sha "$receipt_sha" \ + '{case_id:$case_id,phase:"forge",stage_request:$stage_request[0],request_sha:$request_sha, + resolved_profile:$resolved[0],resolved_sha:$resolved_sha,output_id:"candidate.repository", + media_type:"application/json",payload_sha:$payload_sha}' > "$HOME/result-context.json" +"$jq_bin" -L "${contract%/*}" -L "$modules" -S -c --arg command stage-result \ + -f "$contract" "$HOME/result-context.json" > "$HOME/stage-result.json" +"$jq_bin" -S -c -n --slurpfile result "$HOME/stage-result.json" --rawfile receipt "$HOME/receipt.json" \ + --arg case_id "$("$jq_bin" -r '.case_id' "$request")" --arg receipt_sha "$receipt_sha" ' + {case_id:$case_id,payloads:[{data:$receipt,media_type:"application/json", + payload_id:"candidate.repository",sha256:$receipt_sha}],phase:"forge",protocol_version:1, + stage_result:$result[0]}' diff --git a/adapter-tests/v1/fakes/producer-a.sh b/adapter-tests/v1/fakes/producer-a.sh new file mode 100755 index 0000000..1c9a55f --- /dev/null +++ b/adapter-tests/v1/fakes/producer-a.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +[ "$#" -eq 4 ] || exit 64 +[ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 +request=$1 jq_bin=$2 contract=$3 modules=$4 +"$jq_bin" -L "${contract%/*}" -L "$modules" -e --arg command request-envelope \ + -f "$contract" "$request" >/dev/null +"$jq_bin" -S -c '.stage_request' "$request" > "$HOME/stage-request.json" +"$jq_bin" -j '.payloads[] | select(.payload_id=="resolved-profile") | .data' \ + "$request" > "$HOME/resolved.json" +/usr/bin/printf '%s\n' \ + 'diff --git a/source.txt b/source.txt' \ + '--- a/source.txt' \ + '+++ b/source.txt' \ + '@@ -1,2 +1,3 @@' \ + ' project = moon-garden' \ + ' feature = portable greeting' \ + '+result = portable candidate' > "$HOME/producer.patch" +patch_sha=$(/usr/bin/shasum -a 256 "$HOME/producer.patch" | /usr/bin/awk '{print $1}') +request_sha=$(/usr/bin/shasum -a 256 "$HOME/stage-request.json" | /usr/bin/awk '{print $1}') +resolved_sha=$(/usr/bin/shasum -a 256 "$HOME/resolved.json" | /usr/bin/awk '{print $1}') +"$jq_bin" -S -c -n --slurpfile stage_request "$HOME/stage-request.json" \ + --slurpfile resolved "$HOME/resolved.json" --arg case_id "$("$jq_bin" -r '.case_id' "$request")" \ + --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" --arg payload_sha "$patch_sha" \ + '{case_id:$case_id,phase:"producer",stage_request:$stage_request[0],request_sha:$request_sha, + resolved_profile:$resolved[0],resolved_sha:$resolved_sha,output_id:"producer.patch", + media_type:"text/x-diff",payload_sha:$payload_sha}' > "$HOME/result-context.json" +"$jq_bin" -L "${contract%/*}" -L "$modules" -S -c --arg command stage-result \ + -f "$contract" "$HOME/result-context.json" > "$HOME/stage-result.json" +"$jq_bin" -S -c -n --slurpfile result "$HOME/stage-result.json" \ + --rawfile patch "$HOME/producer.patch" --arg case_id "$("$jq_bin" -r '.case_id' "$request")" \ + --arg patch_sha "$patch_sha" ' + {case_id:$case_id,payloads:[{data:$patch,media_type:"text/x-diff", + payload_id:"producer.patch",sha256:$patch_sha}],phase:"producer",protocol_version:1, + stage_result:$result[0]}' diff --git a/adapter-tests/v1/fakes/producer-b.sh b/adapter-tests/v1/fakes/producer-b.sh new file mode 100755 index 0000000..d44b898 --- /dev/null +++ b/adapter-tests/v1/fakes/producer-b.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +[ "$#" -eq 4 ] || exit 64 +[ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 +request=$1 jq_bin=$2 contract=$3 modules=$4 +"$jq_bin" -L "${contract%/*}" -L "$modules" -e --arg command request-envelope \ + -f "$contract" "$request" >/dev/null +"$jq_bin" -S -c '.stage_request' "$request" > "$HOME/stage-request.json" +"$jq_bin" -j '.payloads[] | select(.payload_id=="resolved-profile") | .data' \ + "$request" > "$HOME/resolved.json" +patch_text=$'diff --git a/source.txt b/source.txt\n--- a/source.txt\n+++ b/source.txt\n@@ -1,2 +1,3 @@\n project = moon-garden\n feature = portable greeting\n+result = portable candidate' +/usr/bin/printf '%s\n' "$patch_text" > "$HOME/producer.patch" +patch_sha=$(/usr/bin/shasum -a 256 "$HOME/producer.patch" | /usr/bin/awk '{print $1}') +request_sha=$(/usr/bin/shasum -a 256 "$HOME/stage-request.json" | /usr/bin/awk '{print $1}') +resolved_sha=$(/usr/bin/shasum -a 256 "$HOME/resolved.json" | /usr/bin/awk '{print $1}') +"$jq_bin" -S -c -n --slurpfile stage_request "$HOME/stage-request.json" \ + --slurpfile resolved "$HOME/resolved.json" --arg case_id "$("$jq_bin" -r '.case_id' "$request")" \ + --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" --arg payload_sha "$patch_sha" \ + '{case_id:$case_id,phase:"producer",stage_request:$stage_request[0],request_sha:$request_sha, + resolved_profile:$resolved[0],resolved_sha:$resolved_sha,output_id:"producer.patch", + media_type:"text/x-diff",payload_sha:$payload_sha}' > "$HOME/result-context.json" +"$jq_bin" -L "${contract%/*}" -L "$modules" -S -c --arg command stage-result \ + -f "$contract" "$HOME/result-context.json" > "$HOME/stage-result.json" +"$jq_bin" -S -c -n --slurpfile result "$HOME/stage-result.json" \ + --rawfile patch "$HOME/producer.patch" --arg case_id "$("$jq_bin" -r '.case_id' "$request")" \ + --arg patch_sha "$patch_sha" ' + {case_id:$case_id,payloads:[{data:$patch,media_type:"text/x-diff", + payload_id:"producer.patch",sha256:$patch_sha}],phase:"producer",protocol_version:1, + stage_result:$result[0]}' diff --git a/adapter-tests/v1/fakes/protocol-fault.sh b/adapter-tests/v1/fakes/protocol-fault.sh new file mode 100755 index 0000000..f9c468a --- /dev/null +++ b/adapter-tests/v1/fakes/protocol-fault.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +[ "$#" -eq 6 ] || exit 64 +[ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 +mode=$1 +request=$2 +jq_bin=$3 +contract=$4 +modules=$5 +producer_snapshot=$6 +case "$mode" in + empty) ;; + malformed) /usr/bin/printf '{\n' ;; + partial) /usr/bin/printf '%s\n' \ + '{"case_id":"matrix-aa","payloads":[],"phase":"producer","protocol_version":1}' ;; + degraded) /usr/bin/printf '%s\n' '{"status":"degraded"}' ;; + timeout) + marker_parent=${TMPDIR%/*} + marker="${marker_parent%/*}/timeout-survived" + (trap '' TERM; /bin/sleep 2; /usr/bin/printf survived > "$marker") & + trap '' TERM + wait + ;; + transport) exit 9 ;; + relabelled|multiple|unlinked|duplicate|descendant) + /bin/bash "$producer_snapshot" "$request" "$jq_bin" \ + "$contract" "$modules" > "$HOME/base-response.json" + case "$mode" in + relabelled) "$jq_bin" -S -c '.phase="forge"' "$HOME/base-response.json" ;; + multiple) /bin/cat "$HOME/base-response.json" "$HOME/base-response.json" ;; + unlinked) + "$jq_bin" -S -c '.payloads += [{data:"extra",media_type:"text/plain", + payload_id:"unlinked",sha256:("0"*64)}]' "$HOME/base-response.json" + ;; + duplicate) "$jq_bin" -S -c '.payloads += [.payloads[0]]' "$HOME/base-response.json" ;; + descendant) + marker_parent=${TMPDIR%/*} + marker="${marker_parent%/*}/descendant-survived" + (trap '' TERM; /bin/sleep 2; /usr/bin/printf survived > "$marker") & + /bin/cat "$HOME/base-response.json" + ;; + esac + ;; + *) exit 64 ;; +esac diff --git a/adapter-tests/v1/fixture/source.txt b/adapter-tests/v1/fixture/source.txt new file mode 100644 index 0000000..8ed9265 --- /dev/null +++ b/adapter-tests/v1/fixture/source.txt @@ -0,0 +1,2 @@ +project = moon-garden +feature = portable greeting diff --git a/adapter-tests/v1/inventory.json b/adapter-tests/v1/inventory.json new file mode 100644 index 0000000..53fff13 --- /dev/null +++ b/adapter-tests/v1/inventory.json @@ -0,0 +1 @@ +{"authorization_ref":{"comment_id":5476938197,"issue_number":153,"scope_comment_id":5474023028},"cases":[{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-aa","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.a","phase":"pipeline","producer_package_id":"fake.producer.a"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-ab","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.b","phase":"pipeline","producer_package_id":"fake.producer.a"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-ba","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.a","phase":"pipeline","producer_package_id":"fake.producer.b"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-bb","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.b","phase":"pipeline","producer_package_id":"fake.producer.b"},{"assertions":["expected-error"],"case_id":"reject-degraded","equivalence_group":"protocol-negative","expected_error":"E_DEGRADED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-empty","equivalence_group":"protocol-negative","expected_error":"E_EMPTY","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-malformed","equivalence_group":"protocol-negative","expected_error":"E_MALFORMED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-partial","equivalence_group":"protocol-negative","expected_error":"E_PARTIAL","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-relabelled","equivalence_group":"protocol-negative","expected_error":"E_RELABELLED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-timeout","equivalence_group":"protocol-negative","expected_error":"E_TIMEOUT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-transport","equivalence_group":"protocol-negative","expected_error":"E_TRANSPORT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-multiple","equivalence_group":"protocol-negative","expected_error":"E_MULTIPLE","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-unlinked","equivalence_group":"protocol-negative","expected_error":"E_UNLINKED_PAYLOAD","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-duplicate","equivalence_group":"protocol-negative","expected_error":"E_DUPLICATE_PAYLOAD","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-descendant","equivalence_group":"protocol-negative","expected_error":"E_DESCENDANT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"}],"protocol":"ystack.adapter-contract.inventory.v1","schema_version":1} diff --git a/adapter-tests/v1/runner.sh b/adapter-tests/v1/runner.sh new file mode 100755 index 0000000..0ffe8d7 --- /dev/null +++ b/adapter-tests/v1/runner.sh @@ -0,0 +1,770 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +export LC_ALL=C +umask 077 + +runner_error() { + printf '%s\n' "${1:-E_RUNTIME}" >&2 + exit 1 +} + +[ "$#" -eq 2 ] || runner_error E_USAGE +inventory=$1 +fixture_root=$2 +runner_source=${BASH_SOURCE[0]} +case "$runner_source:$inventory:$fixture_root" in /*:/*:/*) ;; *) runner_error E_USAGE ;; esac +runner_dir=${runner_source%/*} +repo_root=$(CDPATH='' cd -P -- "$runner_dir/../.." && pwd -P) || runner_error +contract="$runner_dir/contract.jq" +core="$repo_root/scripts/core-contract.sh" +core_fixture_modules="$repo_root/scripts/test" +for runner_file in "$runner_source" "$contract" "$inventory" "$core"; do + [ -f "$runner_file" ] && [ ! -L "$runner_file" ] || runner_error E_BINDING +done +jq_bin=$(command -v jq) || runner_error E_DEPENDENCY +[ "$($jq_bin --version 2>/dev/null)" = jq-1.6 ] || runner_error E_DEPENDENCY +fixture_root=$(CDPATH='' cd -P -- "$fixture_root" && pwd -P) || runner_error E_FIXTURE +[ -z "$(find "$fixture_root" -type l -print -quit)" ] || runner_error E_FIXTURE +[ -d "$fixture_root/scratch" ] && [ -d "$fixture_root/target/.git" ] || runner_error E_FIXTURE + +run_tmp=$(/usr/bin/mktemp -d "$fixture_root/scratch/run.XXXXXX") || runner_error +cleanup() { /bin/rm -rf -- "$run_tmp"; } +ACTIVE_CHILD_GROUP='' +ACTIVE_CHILD_PID='' +PENDING_SIGNAL_STATUS=0 +group_alive() { + [[ "${1:-}" =~ ^[1-9][0-9]*$ ]] && kill -0 -- "-$1" 2>/dev/null +} +terminate_active_group() { + local group=$ACTIVE_CHILD_GROUP + local leader=$ACTIVE_CHILD_PID + local tick + if [[ "$group" =~ ^[1-9][0-9]*$ ]]; then + kill -TERM -- "-$group" 2>/dev/null || : + for tick in {1..5}; do + group_alive "$group" || break + /bin/sleep 0.02 + done + if group_alive "$group"; then + kill -KILL -- "-$group" 2>/dev/null || : + fi + fi + if [[ "$leader" =~ ^[1-9][0-9]*$ ]]; then + wait "$leader" 2>/dev/null || : + fi + ACTIVE_CHILD_GROUP='' + ACTIVE_CHILD_PID='' +} +signal_exit() { + local status=$1 + trap '' HUP INT TERM + trap - EXIT + exec >/dev/null 2>&1 + terminate_active_group + cleanup + exit "$status" +} +record_pending_signal() { PENDING_SIGNAL_STATUS=$1; } +install_normal_signal_handlers() { + trap 'signal_exit 129' HUP + trap 'signal_exit 130' INT + trap 'signal_exit 143' TERM +} +trap cleanup EXIT +install_normal_signal_handlers +/bin/mkdir -m 700 "$run_tmp/home" + +sha_file() { /usr/bin/shasum -a 256 "$1" | /usr/bin/awk '{print $1}'; } +sha_text() { /usr/bin/printf '%s' "$1" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}'; } +canonical() { + local source=$1 + local output=$2 + "$jq_bin" -s -e 'length == 1' "$source" >/dev/null 2>&1 && + "$jq_bin" -S -c . "$source" > "$output" 2>/dev/null && + cmp -s "$source" "$output" +} + +snapshot_json() { + local source=$1 + local snapshot=$2 + local check=$3 + [ -f "$source" ] && [ ! -L "$source" ] || return 1 + /bin/cp "$source" "$snapshot" || return 1 + /bin/chmod 0400 "$snapshot" || return 1 + canonical "$snapshot" "$check" +} + +[ "$(sha_file "$inventory")" = 100edb973c586192683917bc4896121117ef397161263510e09f03a0b269bd4a ] || + runner_error E_INVENTORY +inventory_canonical="$run_tmp/inventory.canonical" +canonical "$inventory" "$inventory_canonical" || runner_error E_INVENTORY +inventory=$inventory_canonical +fixture_source="$fixture_root/target/source.txt" +[ -f "$fixture_source" ] && [ ! -L "$fixture_source" ] || runner_error E_FIXTURE +fixture_sha=$(sha_file "$fixture_source") +[ "$fixture_sha" = ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6 ] || + runner_error E_FIXTURE +"$jq_bin" -c --slurpfile inventory "$inventory" --arg fixture_sha256 "$fixture_sha" -n \ + '{inventory:$inventory[0],fixture_sha256:$fixture_sha256}' | + "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -e \ + --arg command inventory -f "$contract" >/dev/null || + runner_error E_INVENTORY + +git_safe() { + /usr/bin/env -i HOME="$run_tmp/home" TMPDIR="$run_tmp/home" PATH=/usr/bin:/bin LC_ALL=C \ + GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_NO_REPLACE_OBJECTS=1 \ + GIT_NO_LAZY_FETCH=1 GIT_TERMINAL_PROMPT=0 \ + /usr/bin/git --no-replace-objects "$@" +} + +verify_repo() { + local root=$1 + [ -d "$root/.git" ] && [ ! -L "$root/.git" ] && + [ ! -e "$root/.git/objects/info/alternates" ] && + [ "$(git_safe -C "$root" rev-parse --show-toplevel)" = "$root" ] && + [ -z "$(git_safe -C "$root" status --porcelain)" ] && + ! git_safe -C "$root" config --local --get-regexp \ + '(^remote\.|promisor|partialclone|alternates|insteadOf)' >/dev/null 2>&1 +} + +mapping="$fixture_root/repository-map.json" +mapping_canonical="$run_tmp/repository-map.canonical" +if [ ! -f "$mapping" ] || [ -L "$mapping" ] || + ! canonical "$mapping" "$mapping_canonical"; then + runner_error E_MAPPING +fi +mapping=$mapping_canonical +"$jq_bin" -e --arg root "$fixture_root" ' + . as $map | + .version == 1 and (.repositories | length) == 13 and + ([.repositories[] | [.cell_id,.repository_id]] | length == (unique | length)) and + ([.repositories[].root] | length == (unique | length)) and + ([.repositories[] | select(.cell_id == "shared" and + .repository_id == "fixture.target" and .root == ($root+"/target"))] | length) == 1 and + all(["aa","ab","ba","bb"][]; + . as $cell | all(["assets","manifests","profile"][]; + . as $repo | + ([$map.repositories[] | select(.cell_id == $cell and + .repository_id == ("repo."+$repo) and + .root == ($root+"/cells/"+$cell+"/"+$repo))] | length) == 1)) +' "$mapping" >/dev/null || runner_error E_MAPPING + +verify_repo "$fixture_root/target" || runner_error E_GIT +target_commit=$(git_safe -C "$fixture_root/target" rev-parse HEAD) +target_tree=$(git_safe -C "$fixture_root/target" rev-parse 'HEAD^{tree}') +target_entry=$(git_safe -C "$fixture_root/target" ls-tree HEAD -- source.txt) +target_meta=${target_entry%%$'\t'*} +read -r target_mode target_type target_object <<< "$target_meta" +[ "$target_mode:$target_type:$target_object" = \ + "100644:blob:$(git_safe -C "$fixture_root/target" hash-object source.txt)" ] || + runner_error E_GIT +target_source_snapshot="$run_tmp/target-source.snapshot" +git_safe -C "$fixture_root/target" cat-file blob "$target_object" > "$target_source_snapshot" || + runner_error E_GIT +/bin/chmod 0400 "$target_source_snapshot" || runner_error E_GIT +if [ "$(sha_file "$target_source_snapshot")" != "$fixture_sha" ] || + ! cmp -s "$target_source_snapshot" "$fixture_source"; then + runner_error E_GIT +fi + +verify_target_unchanged() { + local current_entry current_meta current_mode current_type current_object current_snapshot + verify_repo "$fixture_root/target" && + [ "$(git_safe -C "$fixture_root/target" rev-parse HEAD)" = "$target_commit" ] && + [ "$(git_safe -C "$fixture_root/target" rev-parse 'HEAD^{tree}')" = "$target_tree" ] || return 1 + current_entry=$(git_safe -C "$fixture_root/target" ls-tree HEAD -- source.txt) || return 1 + current_meta=${current_entry%%$'\t'*} + read -r current_mode current_type current_object <<< "$current_meta" + [ "$current_mode:$current_type:$current_object" = \ + "$target_mode:$target_type:$target_object" ] || return 1 + [ "$(sha_file "$fixture_source")" = "$fixture_sha" ] || return 1 + current_snapshot="$run_tmp/target-source.late" + git_safe -C "$fixture_root/target" cat-file blob "$current_object" > "$current_snapshot" || return 1 + cmp -s "$current_snapshot" "$target_source_snapshot" && cmp -s "$current_snapshot" "$fixture_source" +} + +package_digest() { + case "$1" in + fake.producer.a) printf '%s\n' 39ee6f968f4b823530e2b36de31faab71c1dc1d675ab9f72c409fd9bcccf3aa4 ;; + fake.producer.b) printf '%s\n' 602650c6cf4b161f5b92b273b6944a42fb84eb5946918e9ec13fed9746a20049 ;; + fake.forge.a) printf '%s\n' 672636d079df76736b3b9160d7a38f67ff83767d7da455458c418c1cb0b539a6 ;; + fake.forge.b) printf '%s\n' a87a8453b2daf7723a9565bb597531f198c7c891d2b47c7dfef80ffd3dc2096b ;; + fake.protocol-fault) printf '%s\n' a31c07896edb1ee04d2c87b4512e7199c6b6f514a998d110005ce24d42cdd27c ;; + *) return 1 ;; + esac +} + +package_path() { + case "$1" in + fake.producer.a) printf '%s\n' packages/producer-a.sh ;; + fake.producer.b) printf '%s\n' packages/producer-b.sh ;; + fake.forge.a) printf '%s\n' packages/forge-a.sh ;; + fake.forge.b) printf '%s\n' packages/forge-b.sh ;; + fake.protocol-fault) printf '%s\n' packages/protocol-fault.sh ;; + *) return 1 ;; + esac +} + +mapped_root() { + local cell=$1 + local repository_id=$2 + "$jq_bin" -r --arg cell "$cell" --arg id "$repository_id" ' + [.repositories[] | select(.cell_id==$cell and .repository_id==$id)] | + if length == 1 then .[0].root else "" end + ' "$mapping" +} + +VERIFIED_PACKAGE_SNAPSHOT='' +verify_package() { + local cell=$1 + local package=$2 + local ref=$3 + local relative digest repository_id root algorithm commit entry meta mode type object snapshot package_key + VERIFIED_PACKAGE_SNAPSHOT='' + relative=$(package_path "$package") || return 1 + digest=$(package_digest "$package") || return 1 + "$jq_bin" -e --arg path "$relative" ' + type == "object" and + (keys | sort) == ["location","mode","object_id","object_type","revision"] and + (.revision | type == "object" and + (keys | sort) == ["commit_id","hash_algorithm","repository_id"] and + .repository_id == "repo.assets" and + (.hash_algorithm == "sha1" or .hash_algorithm == "sha256") and + (.commit_id | test("\\A[0-9a-f]{40}([0-9a-f]{24})?\\z"))) and + .location == {kind:"path",value:$path} and .object_type == "blob" and + .mode == "100755" and (.object_id | test("\\A[0-9a-f]{40}([0-9a-f]{24})?\\z")) + ' <<< "$ref" >/dev/null || return 1 + repository_id=$("$jq_bin" -r '.revision.repository_id' <<< "$ref") + root=$(mapped_root "$cell" "$repository_id") + [ -n "$root" ] && verify_repo "$root" || return 1 + algorithm=$(git_safe -C "$root" rev-parse --show-object-format) + [ "$algorithm" = "$("$jq_bin" -r '.revision.hash_algorithm' <<< "$ref")" ] || return 1 + commit=$("$jq_bin" -r '.revision.commit_id' <<< "$ref") + git_safe -C "$root" cat-file -e "$commit^{commit}" >/dev/null 2>&1 || return 1 + entry=$(git_safe -C "$root" ls-tree "$commit" -- "$relative") + meta=${entry%%$'\t'*} + read -r mode type object <<< "$meta" + [ "$mode" = "$("$jq_bin" -r '.mode' <<< "$ref")" ] && + [ "$type" = "$("$jq_bin" -r '.object_type' <<< "$ref")" ] && + [ "$object" = "$("$jq_bin" -r '.object_id' <<< "$ref")" ] || return 1 + package_key=${package//./-} + snapshot="$run_tmp/package.$cell.$package_key" + git_safe -C "$root" cat-file blob "$object" > "$snapshot" || return 1 + /bin/chmod 0500 "$snapshot" || return 1 + [ -f "$snapshot" ] && [ ! -L "$snapshot" ] && + [ -f "$root/$relative" ] && [ ! -L "$root/$relative" ] && + cmp -s "$snapshot" "$root/$relative" && [ "$(sha_file "$snapshot")" = "$digest" ] || + return 1 + VERIFIED_PACKAGE_SNAPSHOT=$snapshot +} + +verify_source_claim() { + local cell=$1 + local claim=$2 + local index=$3 + local ref repository_id root algorithm commit location path entry meta mode type object actual_path + local snapshot format expected_sha + "$jq_bin" -e ' + type == "object" and (keys | sort) == ["source","value_format","value_sha256"] and + (.value_format == "canonical-json" or .value_format == "raw-bytes") and + (.value_sha256 | test("\\A[0-9a-f]{64}\\z")) and + (.source | type == "object" and + (keys | sort) == ["location","mode","object_id","object_type","revision"]) + ' <<< "$claim" >/dev/null || return 1 + ref=$("$jq_bin" -c '.source' <<< "$claim") + repository_id=$("$jq_bin" -r '.revision.repository_id' <<< "$ref") + root=$(mapped_root "$cell" "$repository_id") + [ -n "$root" ] && verify_repo "$root" || return 1 + algorithm=$(git_safe -C "$root" rev-parse --show-object-format) + [ "$algorithm" = "$("$jq_bin" -r '.revision.hash_algorithm' <<< "$ref")" ] || return 1 + commit=$("$jq_bin" -r '.revision.commit_id' <<< "$ref") + git_safe -C "$root" cat-file -e "$commit^{commit}" >/dev/null 2>&1 || return 1 + location=$("$jq_bin" -r '.location.kind' <<< "$ref") + if [ "$location" = root ]; then + mode=040000 + type=tree + object=$(git_safe -C "$root" rev-parse "$commit^{tree}") || return 1 + elif [ "$location" = path ]; then + path=$("$jq_bin" -r '.location.value' <<< "$ref") + entry=$(git_safe -C "$root" ls-tree "$commit" -- "$path" 2>/dev/null) || return 1 + [ -n "$entry" ] || return 1 + meta=${entry%%$'\t'*} + actual_path=${entry#*$'\t'} + read -r mode type object <<< "$meta" + [ "$actual_path" = "$path" ] || return 1 + else + return 1 + fi + [ "$mode" = "$("$jq_bin" -r '.mode' <<< "$ref")" ] && + [ "$type" = "$("$jq_bin" -r '.object_type' <<< "$ref")" ] && + [ "$object" = "$("$jq_bin" -r '.object_id' <<< "$ref")" ] && + [ "$(git_safe -C "$root" cat-file -t "$object")" = "$type" ] || return 1 + snapshot="$run_tmp/source.$cell.$index" + git_safe -C "$root" cat-file "$type" "$object" > "$snapshot" 2>/dev/null || return 1 + expected_sha=$("$jq_bin" -r '.value_sha256' <<< "$claim") + [ "$(sha_file "$snapshot")" = "$expected_sha" ] || return 1 + format=$("$jq_bin" -r '.value_format' <<< "$claim") + [ "$format" = raw-bytes ] || canonical "$snapshot" "$run_tmp/source-canonical.$cell.$index" +} + +verify_all_sources() { + local cell=$1 + local resolved=$2 + local claims="$run_tmp/source-claims.$cell" + local claim index=0 + "$jq_bin" -c '[ + .body.profile_source, + (.body.bindings[] | + .manifest_source, + .package_source, + (.config_source | select(.state=="present") | .value), + (.prompt_source | select(.state=="present") | .value), + .skill_sources[]?, + (.tool_sources[]? | + .package_source, + (.config_source | select(.state=="present") | .value))) + ] | .[]' "$resolved" > "$claims" || return 1 + while IFS= read -r claim; do + verify_source_claim "$cell" "$claim" "$index" || return 1 + index=$((index + 1)) + done < "$claims" + [ "$index" -gt 0 ] +} + +CHILD_STATUS=0 +CHILD_ERROR='' +run_child() { + local executable=$1 + local mode=$2 + local request=$3 + local output=$4 + local diagnostic=$5 + local child_home=$6 + local dependency=${7:-} + local pid tick=0 pending_status + CHILD_STATUS=0 + CHILD_ERROR='' + /bin/mkdir -m 700 "$child_home" + if [ -n "$ACTIVE_CHILD_GROUP" ] || [ -n "$ACTIVE_CHILD_PID" ]; then + CHILD_ERROR=E_RUNTIME + return 1 + fi + PENDING_SIGNAL_STATUS=0 + trap 'record_pending_signal 129' HUP + trap 'record_pending_signal 130' INT + trap 'record_pending_signal 143' TERM + set -m + ( + ulimit -t 2 -f 2048 -n 64 + child_args=("$request" "$jq_bin" "$contract" "$core_fixture_modules") + [ -z "$dependency" ] || child_args+=("$dependency") + [ "$mode" = direct ] || child_args=("$mode" "${child_args[@]}") + exec /usr/bin/env -i HOME="$child_home" TMPDIR="$child_home" PATH=/usr/bin:/bin \ + LC_ALL=C GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null \ + GIT_NO_REPLACE_OBJECTS=1 GIT_NO_LAZY_FETCH=1 GIT_TERMINAL_PROMPT=0 \ + GIT_AUTHOR_NAME=fake GIT_AUTHOR_EMAIL=fake@example.invalid \ + GIT_COMMITTER_NAME=fake GIT_COMMITTER_EMAIL=fake@example.invalid \ + GIT_AUTHOR_DATE=2000-01-01T00:00:00Z GIT_COMMITTER_DATE=2000-01-01T00:00:00Z \ + /bin/bash "$executable" "${child_args[@]}" + ) > "$output" 2> "$diagnostic" & + pid=$! + ACTIVE_CHILD_GROUP=$pid + ACTIVE_CHILD_PID=$pid + set +m + install_normal_signal_handlers + pending_status=$PENDING_SIGNAL_STATUS + PENDING_SIGNAL_STATUS=0 + if [ "$pending_status" -ne 0 ]; then + signal_exit "$pending_status" + fi + while kill -0 "$pid" 2>/dev/null; do + tick=$((tick + 1)) + if [ "$tick" -ge 20 ]; then + terminate_active_group + CHILD_ERROR=E_TIMEOUT + return 1 + fi + /bin/sleep 0.05 + done + wait "$pid" || CHILD_STATUS=$? + if group_alive "$ACTIVE_CHILD_GROUP"; then + terminate_active_group + CHILD_ERROR=E_DESCENDANT + return 1 + fi + ACTIVE_CHILD_GROUP='' + ACTIVE_CHILD_PID='' + [ "$CHILD_STATUS" -eq 0 ] || { CHILD_ERROR=E_TRANSPORT; return 1; } + [ "$(wc -c < "$output" | tr -d ' ')" -le 1048576 ] && + [ "$(wc -c < "$diagnostic" | tr -d ' ')" -le 4096 ] || { + CHILD_ERROR=E_LIMIT + return 1 + } +} + +extract_payload() { + local response=$1 + local payload_id=$2 + local output=$3 + [ "$("$jq_bin" -r --arg id "$payload_id" '[.payloads[] | select(.payload_id==$id)] | length' "$response")" -eq 1 ] || + return 1 + "$jq_bin" -j --arg id "$payload_id" '.payloads[] | select(.payload_id==$id) | .data' \ + "$response" > "$output" && + [ "$(sha_file "$output")" = \ + "$("$jq_bin" -r --arg id "$payload_id" '.payloads[] | select(.payload_id==$id) | .sha256' "$response")" ] +} + +verify_request_payloads() { + local request=$1 + local resolved_snapshot=$2 + local source_snapshot=$3 + local patch_snapshot=${4:-} + local payload_id payload_file payload_index=0 + while IFS= read -r payload_id; do + payload_file="$run_tmp/request-payload.$payload_index" + extract_payload "$request" "$payload_id" "$payload_file" || return 1 + case "$payload_id" in + resolved-profile) cmp -s "$payload_file" "$resolved_snapshot" || return 1 ;; + source) cmp -s "$payload_file" "$source_snapshot" || return 1 ;; + producer.patch) + [ -n "$patch_snapshot" ] && cmp -s "$payload_file" "$patch_snapshot" || return 1 + ;; + *) return 1 ;; + esac + payload_index=$((payload_index + 1)) + done < <("$jq_bin" -r '.payloads[].payload_id' "$request") + [ "$payload_index" -eq "$("$jq_bin" '.payloads | length' "$request")" ] +} + +stage_consistent() { + local request=$1 + local resolved=$2 + local result=$3 + "$jq_bin" -e --slurpfile request "$request" --slurpfile resolved "$resolved" ' + .body.reported_by == .body.execution.performer and + .body.execution.actual_binding.binding_id == $request[0].body.operation.binding_id and + .body.execution.actual_binding.role == $request[0].body.operation.role and + (.body.execution.actual_binding as $actual | + [$resolved[0].body.bindings[] | + select(.binding.binding_id==$actual.binding_id and + .binding.package_ref==$actual.package_ref and + .binding.principal_id==$actual.principal_id)] | length == 1) + ' "$result" >/dev/null +} + +RESPONSE_ERROR='' +validate_response() { + local response=$1 + local request=$2 + local resolved=$3 + local case_id=$4 + local phase=$5 + local result_output=$6 + local roots response_context observed_error payload_id payload_index=0 + RESPONSE_ERROR='' + [ -s "$response" ] || { RESPONSE_ERROR=E_EMPTY; return 1; } + roots=$("$jq_bin" -s 'length' "$response" 2>/dev/null) || { + RESPONSE_ERROR=E_MALFORMED + return 1 + } + [ "$roots" -eq 1 ] || { RESPONSE_ERROR=E_MULTIPLE; return 1; } + canonical "$response" "$run_tmp/response.canonical" || { + RESPONSE_ERROR=E_MALFORMED + return 1 + } + response_context="$run_tmp/response-context.json" + "$jq_bin" -S -c -n --slurpfile response "$response" --arg case_id "$case_id" \ + --arg phase "$phase" '{case_id:$case_id,phase:$phase,response:$response[0]}' > "$response_context" + observed_error=$("$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -r \ + --arg command response-error -f "$contract" "$response_context") || { + RESPONSE_ERROR=E_PARTIAL + return 1 + } + [ -z "$observed_error" ] || { RESPONSE_ERROR=$observed_error; return 1; } + "$jq_bin" -S -c '.stage_result' "$response" > "$result_output" + "$core" validate-stage-run "$request" "$resolved" "$result_output" \ + >/dev/null 2>&1 || { RESPONSE_ERROR=E_CORE; return 1; } + stage_consistent "$request" "$resolved" "$result_output" || { + RESPONSE_ERROR=E_PROVENANCE + return 1 + } + while IFS= read -r payload_id; do + extract_payload "$response" "$payload_id" \ + "$run_tmp/accepted-payload.$payload_index" || { + RESPONSE_ERROR=E_PAYLOAD_DIGEST + return 1 + } + payload_index=$((payload_index + 1)) + done < <("$jq_bin" -r '.payloads[].payload_id' "$response") +} + +git_write() { + /usr/bin/env -i HOME="$run_tmp/home" TMPDIR="$run_tmp/home" PATH=/usr/bin:/bin LC_ALL=C \ + GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_NO_REPLACE_OBJECTS=1 \ + GIT_NO_LAZY_FETCH=1 GIT_TERMINAL_PROMPT=0 \ + GIT_AUTHOR_NAME=fake GIT_AUTHOR_EMAIL=fake@example.invalid \ + GIT_COMMITTER_NAME=fake GIT_COMMITTER_EMAIL=fake@example.invalid \ + GIT_AUTHOR_DATE=2000-01-01T00:00:00Z GIT_COMMITTER_DATE=2000-01-01T00:00:00Z \ + /usr/bin/git --no-replace-objects "$@" +} + +cells="$run_tmp/cells.ndjson" +negatives="$run_tmp/negatives.ndjson" +: > "$cells" +: > "$negatives" +for cell in aa ab ba bb; do + cell_id="matrix-$cell" + cell_root="$fixture_root/cells/$cell" + assets="$cell_root/assets" + manifests="$cell_root/manifests" + profiles="$cell_root/profile" + profile_input="$profiles/profiles/default.json" + profile="$run_tmp/profile.$cell.snapshot" + snapshot_json "$profile_input" "$profile" "$run_tmp/profile.$cell.canonical" || + runner_error E_INPUT + resolved_input="$cell_root/resolved.json" + resolved="$run_tmp/resolved.$cell.snapshot" + snapshot_json "$resolved_input" "$resolved" "$run_tmp/resolved.$cell.canonical" || + runner_error E_INPUT + manifest_args=() + manifest_set="$run_tmp/manifest-set.$cell" + : > "$manifest_set" + for role in forge producer publisher reviewer verifier; do + manifest_input="$manifests/manifests/$role.json" + manifest_file="$run_tmp/manifest.$cell.$role.snapshot" + snapshot_json "$manifest_input" "$manifest_file" "$run_tmp/manifest.$cell.$role.canonical" || + runner_error E_INPUT + manifest_args+=("$manifest_file") + /bin/cat "$manifest_file" >> "$manifest_set" + done + manifest_set_sha=$(sha_file "$manifest_set") + for mapped_repo in "$assets" "$manifests" "$profiles"; do + verify_repo "$mapped_repo" || runner_error E_GIT + done + producer_variant=${cell%?} + forge_variant=${cell#?} + producer_package="fake.producer.$producer_variant" + forge_package="fake.forge.$forge_variant" + producer_ref=$("$jq_bin" -c '.body.bindings[] | select(.role=="producer") | .package_ref' "$profile") + forge_ref=$("$jq_bin" -c '.body.bindings[] | select(.role=="forge") | .package_ref' "$profile") + producer_resolved_ref=$("$jq_bin" -c '.body.bindings[] | select(.binding.role=="producer") | .binding.package_ref' "$resolved") + forge_resolved_ref=$("$jq_bin" -c '.body.bindings[] | select(.binding.role=="forge") | .binding.package_ref' "$resolved") + assets_commit=$("$jq_bin" -r '.revision.commit_id' <<< "$forge_ref") + fault_entry=$(git_safe -C "$assets" ls-tree "$assets_commit" -- packages/protocol-fault.sh 2>/dev/null) || + runner_error E_PACKAGE + fault_meta=${fault_entry%%$'\t'*} + read -r fault_mode fault_type fault_object <<< "$fault_meta" + fault_ref=$("$jq_bin" -c --arg path packages/protocol-fault.sh \ + --arg mode "$fault_mode" --arg type "$fault_type" --arg object "$fault_object" \ + '.location={kind:"path",value:$path} | .mode=$mode | .object_type=$type | .object_id=$object' \ + <<< "$forge_ref") + if [ "$producer_ref" != "$producer_resolved_ref" ] || [ "$forge_ref" != "$forge_resolved_ref" ]; then + runner_error E_PACKAGE + fi + verify_package "$cell" "$producer_package" "$producer_ref" || runner_error E_PACKAGE + producer_executable=$VERIFIED_PACKAGE_SNAPSHOT + [ "$cell" != aa ] || fault_producer_executable=$producer_executable + verify_package "$cell" "$forge_package" "$forge_ref" || runner_error E_PACKAGE + forge_executable=$VERIFIED_PACKAGE_SNAPSHOT + verify_package "$cell" fake.protocol-fault "$fault_ref" || runner_error E_PACKAGE + [ "$cell" != aa ] || fault_executable=$VERIFIED_PACKAGE_SNAPSHOT + verify_all_sources "$cell" "$resolved" || runner_error E_SOURCE + for manifest_file in "${manifest_args[@]}"; do + "$core" validate-document "$manifest_file" >/dev/null 2>&1 || runner_error E_CORE + done + "$core" validate-document "$profile" >/dev/null 2>&1 || runner_error E_CORE + "$core" validate-profile-set "$profile" "$resolved" "${manifest_args[@]}" \ + >/dev/null 2>&1 || runner_error E_CORE + "$jq_bin" -e --arg profile "profile.$cell" --arg producer "fake-$producer_variant" \ + --arg forge "fake-$forge_variant" ' + .id == $profile and + ([.body.bindings[] | select(.role=="producer") | .manifest_ref.id] == ["manifest.producer"]) + ' "$profile" >/dev/null || runner_error E_PROVENANCE + "$jq_bin" -e --arg producer "fake-$producer_variant" --arg forge "fake-$forge_variant" ' + ([.body.bindings[] | select(.binding.role=="producer") | .adapter_implementation.version] == [$producer]) and + ([.body.bindings[] | select(.binding.role=="forge") | .adapter_implementation.version] == [$forge]) + ' "$resolved" >/dev/null || runner_error E_PROVENANCE + + resolved_sha=$(sha_file "$resolved") + resolved_id=$("$jq_bin" -r '.id' "$resolved") + producer_context="$run_tmp/$cell.producer.context" + "$jq_bin" -S -c -n --slurpfile resolved_profile "$resolved" \ + --arg case_id "$cell_id" --arg resolved_sha "$resolved_sha" \ + --arg resolved_id "$resolved_id" \ + --arg target_commit "$target_commit" --arg target_tree "$target_tree" \ + --arg target_object "$target_object" \ + '{case_id:$case_id,resolved_sha:$resolved_sha,resolved_id:$resolved_id, + resolved_profile:$resolved_profile[0],target_commit:$target_commit, + target_tree:$target_tree,target_object:$target_object}' > "$producer_context" + producer_stage_request="$run_tmp/$cell.producer.stage-request" + "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -S -c \ + --arg command producer-request -f "$contract" "$producer_context" > "$producer_stage_request" + "$core" validate-document "$producer_stage_request" >/dev/null 2>&1 || runner_error E_CORE_PRODUCER_REQUEST + producer_request="$run_tmp/$cell.producer.request" + "$jq_bin" -S -c -n --arg case_id "$cell_id" --slurpfile stage "$producer_stage_request" \ + --rawfile source "$target_source_snapshot" --arg source_sha "$fixture_sha" \ + --rawfile resolved "$resolved" --arg resolved_sha "$resolved_sha" ' + {case_id:$case_id,payloads:[ + {data:$resolved,media_type:"application/json",payload_id:"resolved-profile",sha256:$resolved_sha}, + {data:$source,media_type:"text/plain",payload_id:"source",sha256:$source_sha}], + phase:"producer",protocol_version:1,stage_request:$stage[0]}' > "$producer_request" + "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -e \ + --arg command request-envelope -f "$contract" "$producer_request" >/dev/null || runner_error E_PROTOCOL_PRODUCER_REQUEST + verify_request_payloads "$producer_request" "$resolved" "$target_source_snapshot" || + runner_error E_REQUEST_PAYLOAD + producer_output="$run_tmp/$cell.producer.output" + producer_error="$run_tmp/$cell.producer.stderr" + run_child "$producer_executable" direct "$producer_request" "$producer_output" \ + "$producer_error" "$run_tmp/$cell.producer.home" || runner_error "$CHILD_ERROR" + producer_result="$run_tmp/$cell.producer.stage-result" + [ ! -s "$producer_error" ] || runner_error E_PROTOCOL_PRODUCER_OUTPUT + validate_response "$producer_output" "$producer_stage_request" "$resolved" \ + "$cell_id" producer "$producer_result" || runner_error "$RESPONSE_ERROR" + producer_patch="$run_tmp/$cell.producer.patch" + extract_payload "$producer_output" producer.patch "$producer_patch" || runner_error E_OBSERVATION + patch_sha=$(sha_file "$producer_patch") + "$jq_bin" -e --arg sha "$patch_sha" ' + .body.outputs == [{output_id:"producer.patch",ref:.body.delta_ref}] and + .body.delta_ref.media_type == "text/x-diff" and .body.delta_ref.sha256 == $sha + ' "$producer_result" >/dev/null || runner_error E_OBSERVATION + + forge_context="$run_tmp/$cell.forge.context" + "$jq_bin" -S -c -n --slurpfile resolved_profile "$resolved" \ + --arg case_id "$cell_id" --arg resolved_sha "$resolved_sha" \ + --arg resolved_id "$resolved_id" \ + --arg target_commit "$target_commit" --arg target_tree "$target_tree" \ + --arg target_object "$target_object" --arg patch_sha "$patch_sha" \ + '{case_id:$case_id,resolved_sha:$resolved_sha,resolved_id:$resolved_id, + resolved_profile:$resolved_profile[0],target_commit:$target_commit, + target_tree:$target_tree,target_object:$target_object,patch_sha:$patch_sha}' > "$forge_context" + forge_stage_request="$run_tmp/$cell.forge.stage-request" + "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -S -c \ + --arg command forge-request -f "$contract" "$forge_context" > "$forge_stage_request" + "$core" validate-document "$forge_stage_request" >/dev/null 2>&1 || runner_error E_CORE_FORGE_REQUEST + forge_request="$run_tmp/$cell.forge.request" + "$jq_bin" -S -c -n --arg case_id "$cell_id" --slurpfile stage "$forge_stage_request" \ + --rawfile source "$target_source_snapshot" --arg source_sha "$fixture_sha" \ + --rawfile resolved "$resolved" --arg resolved_sha "$resolved_sha" \ + --rawfile patch "$producer_patch" --arg patch_sha "$patch_sha" ' + {case_id:$case_id,payloads:[ + {data:$resolved,media_type:"application/json",payload_id:"resolved-profile",sha256:$resolved_sha}, + {data:$source,media_type:"text/plain",payload_id:"source",sha256:$source_sha}, + {data:$patch,media_type:"text/x-diff",payload_id:"producer.patch",sha256:$patch_sha}], + phase:"forge",protocol_version:1,stage_request:$stage[0]}' > "$forge_request" + "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -e \ + --arg command request-envelope -f "$contract" "$forge_request" >/dev/null || runner_error E_PROTOCOL_FORGE_REQUEST + verify_request_payloads "$forge_request" "$resolved" "$target_source_snapshot" "$producer_patch" || + runner_error E_REQUEST_PAYLOAD + forge_output="$run_tmp/$cell.forge.output" + forge_error="$run_tmp/$cell.forge.stderr" + forge_home="$run_tmp/$cell.forge.home" + run_child "$forge_executable" direct "$forge_request" "$forge_output" \ + "$forge_error" "$forge_home" || runner_error "$CHILD_ERROR" + forge_result="$run_tmp/$cell.forge.stage-result" + [ ! -s "$forge_error" ] || runner_error E_PROTOCOL_FORGE_OUTPUT + validate_response "$forge_output" "$forge_stage_request" "$resolved" \ + "$cell_id" forge "$forge_result" || runner_error "$RESPONSE_ERROR" + receipt="$run_tmp/$cell.receipt" + extract_payload "$forge_output" candidate.repository "$receipt" || runner_error E_OBSERVATION + receipt_sha=$(sha_file "$receipt") + "$jq_bin" -e --arg sha "$receipt_sha" ' + (.body | has("delta_ref") | not) and .body.outputs == [{ + output_id:"candidate.repository",ref:.body.outputs[0].ref}] and + .body.outputs[0].ref.media_type == "application/json" and + .body.outputs[0].ref.sha256 == $sha + ' "$forge_result" >/dev/null || runner_error E_OBSERVATION + candidate_root="$forge_home/candidate" + verify_repo "$candidate_root" || runner_error E_GIT + candidate_commit=$(git_safe -C "$candidate_root" rev-parse HEAD) + candidate_tree=$(git_safe -C "$candidate_root" rev-parse 'HEAD^{tree}') + candidate_object=$(git_safe -C "$candidate_root" rev-parse HEAD:source.txt) + "$jq_bin" -e --arg source "$target_tree" --arg patch "$patch_sha" \ + --arg commit "$candidate_commit" --arg tree "$candidate_tree" --arg object "$candidate_object" ' + . == {candidate_commit_id:$commit,candidate_object_id:$object,candidate_tree_id:$tree, + patch_sha256:$patch,source_tree_id:$source} + ' "$receipt" >/dev/null || runner_error E_OBSERVATION + oracle="$run_tmp/$cell.oracle" + /bin/mkdir -m 700 "$oracle" + git_write init -q "$oracle" + /bin/cp "$target_source_snapshot" "$oracle/source.txt" + git_write -C "$oracle" add source.txt + git_write -C "$oracle" commit -q -m source + git_write -C "$oracle" apply "$producer_patch" + git_write -C "$oracle" add source.txt + git_write -C "$oracle" commit -q -m candidate + [ "$candidate_tree" = "$(git_safe -C "$oracle" rev-parse 'HEAD^{tree}')" ] || runner_error E_OBSERVATION + projection_context="$run_tmp/$cell.projection-context" + "$jq_bin" -S -c -n --slurpfile producer_request "$producer_stage_request" \ + --slurpfile producer_result "$producer_result" --slurpfile forge_request "$forge_stage_request" \ + --slurpfile forge_result "$forge_result" --arg artifact_sha256 "$patch_sha" \ + --arg target_tree "$target_tree" --arg candidate_tree "$candidate_tree" ' + {artifact_sha256:$artifact_sha256,producer_request:$producer_request[0], + producer_result:$producer_result[0],forge_request:$forge_request[0], + forge_result:$forge_result[0],target_tree:$target_tree,candidate_tree:$candidate_tree}' > "$projection_context" + projection=$("$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -c \ + --arg command projection -f "$contract" "$projection_context") + "$jq_bin" -S -c -n --arg case_id "$cell_id" --argjson projection "$projection" \ + --arg profile_sha256 "$(sha_file "$profile")" \ + --arg manifest_set_sha256 "$manifest_set_sha" \ + --arg producer "$producer_package" --arg producer_digest "$(package_digest "$producer_package")" \ + --arg forge "$forge_package" --arg forge_digest "$(package_digest "$forge_package")" ' + {assertion_ids:["audit-projection","candidate-git","core-validation","environment-clean", + "evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"], + case_id:$case_id,projection:$projection, + provenance:{profile_sha256:$profile_sha256,manifest_set_sha256:$manifest_set_sha256, + producer:{package_id:$producer,sha256:$producer_digest}, + forge:{package_id:$forge,sha256:$forge_digest}}} + ' >> "$cells" +done + +[ -n "${fault_executable:-}" ] || runner_error E_PACKAGE +[ -n "${fault_producer_executable:-}" ] || runner_error E_PACKAGE +negative_request="$run_tmp/aa.producer.request" +negative_stage_request="$run_tmp/aa.producer.stage-request" +negative_resolved="$run_tmp/resolved.aa.snapshot" +for negative in degraded empty malformed partial relabelled timeout transport multiple unlinked duplicate descendant; do + negative_id="reject-$negative" + negative_output="$run_tmp/$negative.output" + negative_stderr="$run_tmp/$negative.stderr" + negative_result="$run_tmp/$negative.stage-result" + observed='' + verify_request_payloads "$negative_request" "$negative_resolved" "$target_source_snapshot" || + runner_error E_REQUEST_PAYLOAD + if ! run_child "$fault_executable" "$negative" "$negative_request" "$negative_output" \ + "$negative_stderr" "$run_tmp/$negative.home" "$fault_producer_executable"; then + observed=$CHILD_ERROR + elif validate_response "$negative_output" "$negative_stage_request" "$negative_resolved" \ + matrix-aa producer "$negative_result"; then + observed=E_UNEXPECTED_PASS + else + observed=$RESPONSE_ERROR + fi + expected=$("$jq_bin" -r --arg id "$negative_id" '.cases[] | select(.case_id==$id) | .expected_error' "$inventory") + [ "$observed" = "$expected" ] || runner_error E_NEGATIVE + "$jq_bin" -S -c -n --arg case_id "$negative_id" --arg observed_error "$observed" \ + '{assertion_ids:["expected-error"],case_id:$case_id,observed_error:$observed_error}' >> "$negatives" +done +/bin/sleep 1.2 +[ ! -e "$fixture_root/scratch/timeout-survived" ] && + [ ! -e "$fixture_root/scratch/descendant-survived" ] || runner_error E_GROUP_LEAK + +"$jq_bin" -s -e '([.[].projection] | unique | length) == 1 and + ([.[].provenance.producer.package_id] | unique | length) == 2 and + ([.[].provenance.forge.package_id] | unique | length) == 2 and + ([.[].provenance.profile_sha256] | unique | length) == 4' "$cells" >/dev/null || + runner_error E_EQUIVALENCE +verify_target_unchanged || runner_error E_TARGET_STALE +inventory_sha=$(sha_file "$inventory") +"$jq_bin" -S -c -n --slurpfile cells "$cells" --slurpfile negatives "$negatives" \ + --arg inventory_sha "$inventory_sha" --arg target_commit "$target_commit" \ + --arg target_object "$target_object" ' + {schema_version:1,kind:"adapter_contract_observation", + inventory_acceptance_ref:{authorization_comment_id:5476938197,sha256:$inventory_sha}, + cells:$cells,negative_observations:$negatives, + target_facts:{commit_id:$target_commit,source_object_id:$target_object}, + non_claims:["authority","qualification","approval","publish","merge","branch-write", + "real-adapter-safety","network-isolation","host-isolation","external-target-smoke"]} +' +trap - EXIT HUP INT TERM +cleanup diff --git a/ci/required-files.txt b/ci/required-files.txt index 9f8fdd4..8411409 100644 --- a/ci/required-files.txt +++ b/ci/required-files.txt @@ -154,3 +154,15 @@ core/v2/generations/g-392d20099dfa99872764009b268c8871914b4dbc0da467ec346baa9218 core/v2/generations/g-392d20099dfa99872764009b268c8871914b4dbc0da467ec346baa921818ae3e/modules/schema.jq core/v2/generations/g-392d20099dfa99872764009b268c8871914b4dbc0da467ec346baa921818ae3e/modules/stage_request.jq scripts/test/portable-core-v2-fake-forge.test.sh + +# Inactive fake producer/forge adapter contract matrix +adapter-tests/v1/runner.sh +adapter-tests/v1/contract.jq +adapter-tests/v1/inventory.json +adapter-tests/v1/fakes/producer-a.sh +adapter-tests/v1/fakes/producer-b.sh +adapter-tests/v1/fakes/forge-a.sh +adapter-tests/v1/fakes/forge-b.sh +adapter-tests/v1/fakes/protocol-fault.sh +adapter-tests/v1/fixture/source.txt +scripts/test/portable-adapter-contracts.test.sh diff --git a/scripts/test/portable-adapter-contracts.test.sh b/scripts/test/portable-adapter-contracts.test.sh new file mode 100755 index 0000000..e505c31 --- /dev/null +++ b/scripts/test/portable-adapter-contracts.test.sh @@ -0,0 +1,452 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +export LC_ALL=C +umask 077 + +test_root=$(CDPATH='' cd -P -- "${BASH_SOURCE[0]%/*}/../.." && pwd -P) +runner="$test_root/adapter-tests/v1/runner.sh" +inventory_source="$test_root/adapter-tests/v1/inventory.json" +fixture_builder="$test_root/scripts/test/portable-profile-resolution-fixtures.sh" +resolver_runtime="$test_root/resolver/v1/profile-resolve-runtime.sh" +tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-adapter-contracts.XXXXXX") +tmp=$(CDPATH='' cd -P -- "$tmp" && pwd -P) +download='' + +cleanup() { + if [ -n "$download" ] && [ -f "$download" ]; then + /bin/rm -f -- "$download" + fi + /bin/rm -rf -- "$tmp" +} +trap cleanup EXIT + +fail() { /usr/bin/printf 'FAIL: %s\n' "$1" >&2; exit 1; } +pass_count=0 +pass() { pass_count=$((pass_count + 1)); /usr/bin/printf 'ok %s - %s\n' "$pass_count" "$1"; } +sha_file() { /usr/bin/shasum -a 256 "$1" | /usr/bin/awk '{print $1}'; } + +platform=$(/usr/bin/uname -s):$(/usr/bin/uname -m) +case "$platform" in + Darwin:*) jq_asset=jq-osx-amd64; compiler=/usr/bin/clang ;; + Linux:x86_64) jq_asset=jq-linux64; compiler=/usr/bin/cc ;; + *) fail "unsupported host $platform" ;; +esac +case "$jq_asset" in + jq-osx-amd64) jq_sha=5c0a0a3ea600f302ee458b30317425dd9632d1ad8882259fcaf4e9b868b2b1ef ;; + *) jq_sha=af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44 ;; +esac +jq_cache_dir="${TMPDIR:-/tmp}/ystack-portable-core-jq16" +/bin/mkdir -p "$jq_cache_dir" +jq_cache="$jq_cache_dir/$jq_asset" +if [ ! -f "$jq_cache" ] || [ "$(sha_file "$jq_cache")" != "$jq_sha" ]; then + download=$(/usr/bin/mktemp "$jq_cache_dir/.jq-1.6.XXXXXX") + /usr/bin/curl --proto '=https' --tlsv1.2 -fsSL \ + "https://github.com/jqlang/jq/releases/download/jq-1.6/$jq_asset" \ + -o "$download" + [ "$(sha_file "$download")" = "$jq_sha" ] || fail 'jq release digest' + /bin/chmod 0555 "$download" + /bin/mv "$download" "$jq_cache" + download='' +fi +[ "$(sha_file "$jq_cache")" = "$jq_sha" ] || fail 'jq digest' +bin="$tmp/bin" +/bin/mkdir -m 700 "$bin" +/bin/cp "$jq_cache" "$bin/jq" +/bin/chmod 0555 "$bin/jq" +jq_bin="$bin/jq" +if [ "$platform" = Linux:x86_64 ]; then + /bin/cp /usr/bin/awk "$bin/awk" +else + /usr/bin/printf '%s\n' '#!/bin/bash' 'exec /usr/bin/awk "$@"' > "$bin/awk" +fi +/bin/chmod 0555 "$bin/awk" +"$compiler" -std=c11 -O2 -Wall -Wextra -Werror -pedantic \ + "$test_root/resolver/v1/nofollow-snapshot.c" -o "$bin/nofollow-snapshot" +"$compiler" -std=c11 -O2 -Wall -Wextra -Werror -pedantic \ + "$test_root/scripts/test/portable-profile-resolution-launcher.c" -o "$bin/launcher" +pass 'fixed resolver launcher and dependencies built' + +fixture="$tmp/garden-fixture" +/bin/mkdir -m 700 "$fixture" "$fixture/cells" "$fixture/scratch" "$fixture/target" +inventory="$fixture/inventory.json" +/bin/cp "$inventory_source" "$inventory" +/bin/cp "$test_root/adapter-tests/v1/fixture/source.txt" "$fixture/target/source.txt" +git_home="$tmp/git-home" +/bin/mkdir -m 700 "$git_home" +fixture_git() { + /usr/bin/env -i HOME="$git_home" TMPDIR="$tmp" PATH=/usr/bin:/bin LC_ALL=C \ + GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_NO_REPLACE_OBJECTS=1 \ + GIT_NO_LAZY_FETCH=1 GIT_TERMINAL_PROMPT=0 \ + GIT_AUTHOR_DATE=2000-01-01T00:00:00Z GIT_COMMITTER_DATE=2000-01-01T00:00:00Z \ + /usr/bin/git --no-replace-objects "$@" +} +/usr/bin/env -i HOME="$git_home" TMPDIR="$tmp" PATH=/usr/bin:/bin LC_ALL=C \ + GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_DEFAULT_HASH=sha256 \ + /usr/bin/git -c init.defaultObjectFormat=sha256 init -q --object-format=sha1 "$fixture/target" +[ "$(fixture_git -C "$fixture/target" rev-parse --show-object-format)" = sha1 ] || + fail 'explicit target SHA-1 format' +fixture_git -C "$fixture/target" config user.name fixture +fixture_git -C "$fixture/target" config user.email fixture@example.invalid +fixture_git -C "$fixture/target" add source.txt +fixture_git -C "$fixture/target" commit -q -m fixture +pass 'hostile defaults cannot override explicit target SHA-1' + +for cell in aa ab ba bb; do + cell_root="$fixture/cells/$cell" + PATH="$bin:/usr/bin:/bin" "$fixture_builder" "$cell_root" "$jq_bin" "$cell" >/dev/null + /bin/mkdir -m 700 "$cell_root/resolver-sandbox" + YSTACK_TEST_SANDBOX="$cell_root/resolver-sandbox" \ + "$bin/launcher" resolve "$resolver_runtime" "$bin/nofollow-snapshot" \ + "$jq_bin" "$cell_root/request.json" "$cell_root/map.json" > "$cell_root/resolved.json" +done +pass 'four honest profile variants resolved through the inactive resolver' + +bad_request_payload="$tmp/bad-request-payload.json" +"$jq_bin" -S -c -n '{case_id:"matrix-aa",phase:"producer",protocol_version:1, + stage_request:{schema_version:2,kind:"stage_request",body:{resolved_profile_ref:{sha256:("a"*64)}}}, + payloads:[ + {data:"resolved",media_type:"application/json",payload_id:"resolved-profile",sha256:("b"*64)}, + {data:"source",media_type:"text/plain",payload_id:"source",sha256:("c"*64)}]}' > "$bad_request_payload" +if "$jq_bin" -L "$test_root/adapter-tests/v1" -L "$test_root/scripts/test" -e \ + --arg command request-envelope -f "$test_root/adapter-tests/v1/contract.jq" \ + "$bad_request_payload" >/dev/null; then + fail 'declared request payload digest mismatch accepted' +fi +pass 'declared request payload digest mismatch' + +"$jq_bin" -S -c -n --arg root "$fixture" ' + {version:1,repositories: + ([{cell_id:"shared",repository_id:"fixture.target",root:($root+"/target")}] + + (["aa","ab","ba","bb"] | + map(. as $cell | ["assets","manifests","profile"] | + map(. as $repo | {cell_id:$cell,repository_id:("repo."+$repo), + root:($root+"/cells/"+$cell+"/"+$repo)})) | add))} +' > "$fixture/repository-map.json" + +fingerprint_before=$(fixture_git -C "$fixture/target" rev-parse 'HEAD^{tree}') +output="$tmp/observation.json" +error="$tmp/observation.stderr" +GH_TOKEN=must-not-leak AWS_SECRET_ACCESS_KEY=must-not-leak SSH_AUTH_SOCK=/must/not/leak \ + PATH="$bin:/usr/bin:/bin" "$runner" "$inventory" "$fixture" > "$output" 2> "$error" || { + /bin/cat "$error" >&2 + fail 'accepted matrix run' +} +[ ! -s "$error" ] || fail 'success diagnostics' +"$jq_bin" -e ' + .schema_version == 1 and .kind == "adapter_contract_observation" and + (.cells | length) == 4 and (.negative_observations | length) == 11 and + ([.cells[].projection] | unique | length) == 1 and + ([.cells[].provenance.profile_sha256] | unique | length) == 4 and + (.non_claims | index("external-target-smoke") != null) and + (.non_claims | index("network-isolation") != null) and + (.inventory_acceptance_ref.authorization_comment_id == 5476938197) +' "$output" >/dev/null || fail 'observation shape and equivalence' +[ "$fingerprint_before" = "$(fixture_git -C "$fixture/target" rev-parse 'HEAD^{tree}')" ] || + fail 'target changed' +[ -z "$(find "$fixture/scratch" -mindepth 1 -print -quit)" ] || fail 'scratch cleanup' +/usr/bin/grep -Fq 'moon-garden' "$fixture/target/source.txt" || fail 'unrelated target fixture' +pass '2x2 projection, provenance, environment, Git truth, and cleanup' + +race_out="$tmp/package-race.out" +race_err="$tmp/package-race.err" +race_forge="$fixture/cells/aa/assets/packages/forge-a.sh" +race_saved="$tmp/forge-a.saved" +race_source_saved="$tmp/source-race.saved" +race_inventory_saved="$tmp/inventory-race.saved" +race_map_saved="$tmp/map-race.saved" +race_profile="$fixture/cells/aa/profile/profiles/default.json" +race_profile_saved="$tmp/profile-race.saved" +race_manifest="$fixture/cells/aa/manifests/manifests/forge.json" +race_manifest_saved="$tmp/manifest-race.saved" +/bin/cp "$race_forge" "$race_saved" +/bin/cp "$fixture/target/source.txt" "$race_source_saved" +/bin/cp "$inventory" "$race_inventory_saved" +/bin/cp "$fixture/repository-map.json" "$race_map_saved" +/bin/cp "$race_profile" "$race_profile_saved" +/bin/cp "$race_manifest" "$race_manifest_saved" +PATH="$bin:/usr/bin:/bin" "$runner" "$inventory" "$fixture" > "$race_out" 2> "$race_err" & +race_pid=$! +race_marker='' +for _ in {1..12000}; do + race_marker=$(/usr/bin/find "$fixture/scratch" -name 'package.aa.fake-producer-a' -type f -print -quit) + [ -z "$race_marker" ] || break + /bin/kill -0 "$race_pid" 2>/dev/null || break + /bin/sleep 0.005 +done +if [ -z "$race_marker" ]; then + /bin/kill "$race_pid" 2>/dev/null || : + wait "$race_pid" 2>/dev/null || : + fail 'verified snapshot race marker' +fi +/usr/bin/printf '%s\n' 'project = mutable-garden' 'feature = wrong bytes' > "$fixture/target/source.txt" +race_marker='' +for _ in {1..12000}; do + race_marker=$(/usr/bin/find "$fixture/scratch" -name 'aa.producer.home' -type d -print -quit) + [ -z "$race_marker" ] || break + /bin/kill -0 "$race_pid" 2>/dev/null || break + /bin/sleep 0.005 +done +if [ -z "$race_marker" ]; then + /bin/kill "$race_pid" 2>/dev/null || : + wait "$race_pid" 2>/dev/null || : + /bin/cp "$race_source_saved" "$fixture/target/source.txt" + fail 'immutable package race marker' +fi +/usr/bin/printf '%s\n' '#!/bin/bash' \ + "/usr/bin/printf mutable > '$fixture/scratch/mutable-package-ran'" \ + 'exit 99' > "$race_forge" +/bin/chmod 0755 "$race_forge" +/usr/bin/printf '%s\n' '{}' > "$inventory" +/usr/bin/printf '%s\n' '{}' > "$fixture/repository-map.json" +/usr/bin/printf '%s\n' '{}' > "$race_profile" +/usr/bin/printf '%s\n' '{}' > "$race_manifest" +race_marker='' +for _ in {1..12000}; do + race_marker=$(/usr/bin/find "$fixture/scratch" -name 'aa.forge.stage-result' -type f -print -quit) + [ -z "$race_marker" ] || break + /bin/kill -0 "$race_pid" 2>/dev/null || break + /bin/sleep 0.005 +done +/bin/cp "$race_source_saved" "$fixture/target/source.txt" +/bin/cp "$race_inventory_saved" "$inventory" +/bin/cp "$race_map_saved" "$fixture/repository-map.json" +/bin/cp "$race_profile_saved" "$race_profile" +/bin/cp "$race_manifest_saved" "$race_manifest" +if [ -z "$race_marker" ]; then + /bin/kill "$race_pid" 2>/dev/null || : + wait "$race_pid" 2>/dev/null || : + fail 'immutable source execution marker' +fi +if ! wait "$race_pid"; then + fail 'immutable package race run' +fi +[ ! -s "$race_err" ] && [ ! -e "$fixture/scratch/mutable-package-ran" ] && + [ -z "$(find "$fixture/scratch" -mindepth 1 -print -quit)" ] || + fail 'mutable package executed' +"$jq_bin" -e --slurpfile baseline "$output" ' + .inventory_acceptance_ref == $baseline[0].inventory_acceptance_ref and + [.cells[] | {projection,provenance}] == + [$baseline[0].cells[] | {projection,provenance}] +' "$race_out" >/dev/null || + fail 'source snapshot projection drift' +/bin/cp "$race_saved" "$race_forge" +/usr/bin/git -C "$fixture/cells/aa/assets" diff --quiet -- packages/forge-a.sh || + fail 'package race restore' +fixture_git -C "$fixture/target" diff --quiet -- source.txt || fail 'source race restore' +/usr/bin/git -C "$fixture/cells/aa/profile" diff --quiet -- profiles/default.json || + fail 'profile race restore' +/usr/bin/git -C "$fixture/cells/aa/manifests" diff --quiet -- manifests/forge.json || + fail 'manifest race restore' +if ! cmp -s "$race_inventory_saved" "$inventory" || + ! cmp -s "$race_map_saved" "$fixture/repository-map.json"; then + fail 'metadata race restore' +fi +pass 'validated fixture mutations cannot change snapshot execution' + +expect_failure() { + local name=$1 + local expected=$2 + local chosen_inventory=${3:-$inventory} + local case_out="$tmp/$name.out" + local case_err="$tmp/$name.err" + if PATH="$bin:/usr/bin:/bin" "$runner" "$chosen_inventory" "$fixture" \ + > "$case_out" 2> "$case_err"; then + fail "$name accepted" + fi + [ ! -s "$case_out" ] && [ "$(/bin/cat "$case_err")" = "$expected" ] || + fail "$name error" + pass "$name" +} + +for mutation in dropped duplicated relabelled mutated; do + changed="$tmp/inventory-$mutation.json" + case "$mutation" in + dropped) filter='del(.cases[0])' ;; + duplicated) filter='.cases += [.cases[0]]' ;; + relabelled) filter='.cases[0].case_id = "matrix-zz"' ;; + mutated) filter='.cases[0].assertions[0] = "different"' ;; + esac + "$jq_bin" -S -c "$filter" "$inventory" > "$changed" + expect_failure "inventory-$mutation" E_INVENTORY "$changed" +done + +map_saved="$tmp/repository-map.saved" +/bin/cp "$fixture/repository-map.json" "$map_saved" +"$jq_bin" -S -c '.repositories[1].root = .repositories[0].root' \ + "$map_saved" > "$fixture/repository-map.json" +expect_failure mapping-mismatch E_MAPPING +/bin/cp "$map_saved" "$fixture/repository-map.json" + +/usr/bin/printf '%s\n' /invalid/alternate > "$fixture/target/.git/objects/info/alternates" +expect_failure git-alternates E_GIT +/bin/rm "$fixture/target/.git/objects/info/alternates" + +resolved_saved="$tmp/resolved.saved" +/bin/cp "$fixture/cells/aa/resolved.json" "$resolved_saved" +"$jq_bin" -S -c '.body.profile_ref.sha256 = ("0"*64)' "$resolved_saved" > \ + "$fixture/cells/aa/resolved.json" +expect_failure core-ref-mismatch E_CORE +/bin/cp "$resolved_saved" "$fixture/cells/aa/resolved.json" + +profile_file="$fixture/cells/aa/profile/profiles/default.json" +profile_saved="$tmp/profile.saved" +/bin/cp "$profile_file" "$profile_saved" +resolved_ref_saved="$tmp/resolved-ref.saved" +/bin/cp "$fixture/cells/aa/resolved.json" "$resolved_ref_saved" +original_package_object=$("$jq_bin" -r \ + '.body.bindings[] | select(.role=="producer") | .package_ref.object_id' "$profile_file") +for ref_mutation in repository revision hash path type mode; do + case "$ref_mutation" in + repository) mutate='def mutate: .revision.repository_id="repo.manifests";' ;; + revision) mutate='def mutate: .revision.commit_id=("0"*64);' ;; + hash) mutate='def mutate: .revision.hash_algorithm="sha1";' ;; + path) mutate='def mutate: .location={kind:"path",value:"packages/producer-b.sh"};' ;; + type) mutate='def mutate: .object_type="tree";' ;; + mode) mutate='def mutate: .mode="100644";' ;; + esac + "$jq_bin" -S -c "$mutate (.body.bindings[] | select(.role==\"producer\") | .package_ref) |= mutate" \ + "$profile_saved" > "$profile_file" + "$jq_bin" -S -c "$mutate (.body.bindings[] | select(.binding.role==\"producer\") | .binding.package_ref) |= mutate" \ + "$resolved_ref_saved" > "$fixture/cells/aa/resolved.json" + [ "$("$jq_bin" -r '.body.bindings[] | select(.role=="producer") | .package_ref.object_id' \ + "$profile_file")" = "$original_package_object" ] || fail "ref-$ref_mutation changed object id" + /usr/bin/git -C "$fixture/cells/aa/profile" add profiles/default.json + GIT_AUTHOR_DATE=2000-01-04T00:00:00Z GIT_COMMITTER_DATE=2000-01-04T00:00:00Z \ + /usr/bin/git -C "$fixture/cells/aa/profile" commit -q -m "ref-$ref_mutation" + expect_failure "package-ref-$ref_mutation" E_PACKAGE + /bin/cp "$profile_saved" "$profile_file" + /bin/cp "$resolved_ref_saved" "$fixture/cells/aa/resolved.json" + /usr/bin/git -C "$fixture/cells/aa/profile" add profiles/default.json + GIT_AUTHOR_DATE=2000-01-05T00:00:00Z GIT_COMMITTER_DATE=2000-01-05T00:00:00Z \ + /usr/bin/git -C "$fixture/cells/aa/profile" commit -q -m "restore-$ref_mutation" +done + +source_claim_saved="$tmp/source-claim.saved" +/bin/cp "$fixture/cells/aa/resolved.json" "$source_claim_saved" +for source_mutation in manifest-repository config-commit prompt-path skill-oid tool-mode profile-value; do + case "$source_mutation" in + manifest-repository) + source_filter='(.body.bindings[] | select(.binding.role=="forge") | + .manifest_source.source.revision.repository_id)="repo.profile"' + ;; + config-commit) + source_filter='(.body.bindings[] | select(.binding.role=="producer") | + .config_source.value.source.revision.commit_id)=("0"*64)' + ;; + prompt-path) + source_filter='(.body.bindings[] | select(.binding.role=="producer") | + .prompt_source.value.source.location.value)="prompts/reviewer.md"' + ;; + skill-oid) + source_filter='(.body.bindings[] | select(.binding.role=="producer") | + .skill_sources[0].source.object_id)=("0"*64)' + ;; + tool-mode) + source_filter='(.body.bindings[] | select(.binding.role=="producer") | + .tool_sources[0].package_source.source.mode)="100755"' + ;; + profile-value) source_filter='.body.profile_source.value_sha256=("0"*64)' ;; + esac + "$jq_bin" -S -c "$source_filter" "$source_claim_saved" > "$fixture/cells/aa/resolved.json" + expect_failure "source-claim-$source_mutation" E_SOURCE + /bin/cp "$source_claim_saved" "$fixture/cells/aa/resolved.json" +done + +late_out="$tmp/late-target.out" +late_err="$tmp/late-target.err" +PATH="$bin:/usr/bin:/bin" "$runner" "$inventory" "$fixture" > "$late_out" 2> "$late_err" & +late_pid=$! +late_marker='' +for _ in {1..3000}; do + late_marker=$(/usr/bin/find "$fixture/scratch" -path '*/aa.producer.home' -type d -print -quit) + [ -z "$late_marker" ] || break + /bin/kill -0 "$late_pid" 2>/dev/null || break + /bin/sleep 0.02 +done +if [ -z "$late_marker" ]; then + /bin/kill "$late_pid" 2>/dev/null || : + wait "$late_pid" 2>/dev/null || : + fail 'late target marker' +fi +fixture_git -C "$fixture/target" commit -q --allow-empty -m late-change +if wait "$late_pid"; then + fail 'late target mutation accepted' +fi +[ ! -s "$late_out" ] && [ "$(/bin/cat "$late_err")" = E_TARGET_STALE ] || + fail 'late target mutation error' +pass 'late target commit recheck' + +signal_out="$tmp/signal.out" +signal_err="$tmp/signal.err" +PATH="$bin:/usr/bin:/bin" "$runner" "$inventory" "$fixture" > "$signal_out" 2> "$signal_err" & +signal_pid=$! +signal_marker='' +for _ in {1..6000}; do + signal_marker=$(/usr/bin/find "$fixture/scratch" -name 'request-payload.1' -type f -print -quit) + [ -z "$signal_marker" ] || break + /bin/kill -0 "$signal_pid" 2>/dev/null || break + /bin/sleep 0.02 +done +if [ -z "$signal_marker" ]; then + /bin/kill "$signal_pid" 2>/dev/null || : + wait "$signal_pid" 2>/dev/null || : + fail 'signal launch marker' +fi +signal_sent=0 +for _ in {1..200}; do + /bin/kill -0 "$signal_pid" 2>/dev/null || break + /bin/kill -TERM "$signal_pid" 2>/dev/null || break + signal_sent=$((signal_sent + 1)) + /bin/sleep 0.005 +done +signal_status=0 +wait "$signal_pid" || signal_status=$? +/bin/sleep 2.2 +signal_stdout_bytes=$(/usr/bin/wc -c < "$signal_out" | /usr/bin/tr -d ' ') +signal_stderr_bytes=$(/usr/bin/wc -c < "$signal_err" | /usr/bin/tr -d ' ') +signal_stdout_class=empty +signal_stderr_class=empty +[ "$signal_stdout_bytes" -eq 0 ] || signal_stdout_class=other +if [ "$signal_stderr_bytes" -ne 0 ]; then + if /usr/bin/grep -Eq '(^|[[:space:]])(Terminated|Killed|Hangup|Interrupt|Done)(:|[[:space:]]|$)' \ + "$signal_err"; then + signal_stderr_class=job-control + else + signal_stderr_class=other + fi +fi +signal_scratch_count=$(/usr/bin/find "$fixture/scratch" -mindepth 1 -print | /usr/bin/wc -l | /usr/bin/tr -d ' ') +signal_survivor=absent +if /usr/bin/find "$fixture/scratch" \( -name timeout-survived -o -name descendant-survived \) \ + -print -quit | /usr/bin/grep -q .; then + signal_survivor=present +fi +if [ "$signal_sent" -le 0 ] || [ "$signal_status" -ne 143 ] || + [ "$signal_stdout_bytes" -ne 0 ] || [ "$signal_stderr_bytes" -ne 0 ] || + [ "$signal_scratch_count" -ne 0 ] || [ "$signal_survivor" != absent ]; then + /usr/bin/printf 'signal-debug status=%s sent=%s stdout_bytes=%s stdout_class=%s stderr_bytes=%s stderr_class=%s scratch_entries=%s survivor=%s\n' \ + "$signal_status" "$signal_sent" "$signal_stdout_bytes" "$signal_stdout_class" \ + "$signal_stderr_bytes" "$signal_stderr_class" "$signal_scratch_count" "$signal_survivor" >&2 + fail 'signal group cleanup' +fi +pass 'TERM stops adapter group and cleans scratch' + +source_saved="$tmp/source.saved" +/bin/cp "$fixture/target/source.txt" "$source_saved" +/usr/bin/printf '%s\n' changed > "$fixture/target/source.txt" +fixture_git -C "$fixture/target" add source.txt +fixture_git -C "$fixture/target" commit -q -m changed +expect_failure fixture-digest-mismatch E_FIXTURE +/bin/cp "$source_saved" "$fixture/target/source.txt" +fixture_git -C "$fixture/target" add source.txt +fixture_git -C "$fixture/target" commit -q -m restored + +producer="$fixture/cells/aa/assets/packages/producer-a.sh" +/usr/bin/printf '%s\n' '# changed' >> "$producer" +/usr/bin/git -C "$fixture/cells/aa/assets" add packages/producer-a.sh +GIT_AUTHOR_DATE=2000-01-02T00:00:00Z GIT_COMMITTER_DATE=2000-01-02T00:00:00Z \ + /usr/bin/git -C "$fixture/cells/aa/assets" commit -q -m changed +expect_failure executable-digest-mismatch E_PACKAGE + +/usr/bin/printf 'portable adapter contracts: %s focused checks passed\n' "$pass_count" diff --git a/scripts/test/portable-profile-resolution-fixtures.sh b/scripts/test/portable-profile-resolution-fixtures.sh index c0fd83b..7cc357b 100755 --- a/scripts/test/portable-profile-resolution-fixtures.sh +++ b/scripts/test/portable-profile-resolution-fixtures.sh @@ -67,12 +67,22 @@ fixture_scope() { scope_sha256:$hash}' } -if [ "$#" -ne 2 ]; then - printf '%s\n' 'usage: portable-profile-resolution-fixtures.sh OUTPUT_ROOT JQ_1_6' >&2 +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + printf '%s\n' 'usage: portable-profile-resolution-fixtures.sh OUTPUT_ROOT JQ_1_6 [aa|ab|ba|bb]' >&2 exit 64 fi fixture_root=$1 fixture_jq=$2 +fixture_variant=${3:-default} +case "$fixture_variant" in + default) fixture_producer_variant=a; fixture_forge_variant=a; fixture_profile_id=profile.example ;; + aa|ab|ba|bb) + fixture_producer_variant=${fixture_variant%?} + fixture_forge_variant=${fixture_variant#?} + fixture_profile_id="profile.$fixture_variant" + ;; + *) exit 64 ;; +esac case "$fixture_jq" in /*) ;; *) exit 1 ;; esac [ -x "$fixture_jq" ] && [ -f "$fixture_jq" ] && [ ! -L "$fixture_jq" ] && [ "$("$fixture_jq" --version)" = jq-1.6 ] || exit 1 @@ -96,10 +106,23 @@ fixture_init_repo "$fixture_assets" sha256 /usr/bin/printf '%s\n' 'Bounded fixture skill.' > "$fixture_assets/skills/producer.md" /usr/bin/printf '%s\n' tool-package > "$fixture_assets/tools/producer.bin" /usr/bin/printf '%s\n' '{"tool":true}' > "$fixture_assets/tools/producer.json" +if [ "$fixture_variant" != default ]; then + for fixture_fake in producer-a producer-b forge-a forge-b protocol-fault; do + /bin/cp "$fixture_repo/adapter-tests/v1/fakes/$fixture_fake.sh" \ + "$fixture_assets/packages/$fixture_fake.sh" + /bin/chmod 0755 "$fixture_assets/packages/$fixture_fake.sh" + done +fi /bin/dd if=/dev/zero of="$fixture_assets/packages/producer-large.bin" bs=1048576 count=17 2>/dev/null fixture_assets_commit=$(fixture_commit "$fixture_assets") fixture_producer_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 "$fixture_assets_commit" packages/producer.bin) fixture_forge_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 "$fixture_assets_commit" packages/forge.bin) +if [ "$fixture_variant" != default ]; then + fixture_producer_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 \ + "$fixture_assets_commit" "packages/producer-$fixture_producer_variant.sh") + fixture_forge_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 \ + "$fixture_assets_commit" "packages/forge-$fixture_forge_variant.sh") +fi fixture_publisher_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 "$fixture_assets_commit" packages/publisher.bin) fixture_reviewer_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 "$fixture_assets_commit" packages/reviewer.bin) fixture_verifier_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 "$fixture_assets_commit" packages/verifier.bin) @@ -119,6 +142,7 @@ fixture_tool=$("$fixture_jq" -S -c -n --argjson package "$fixture_tool_package" fixture_init_repo "$fixture_manifests" sha1 /bin/mkdir -p "$fixture_manifests/manifests" for fixture_role in forge producer publisher reviewer verifier; do + fixture_adapter_version=v1 case "$fixture_role" in forge) fixture_package=$fixture_forge_package @@ -127,6 +151,7 @@ for fixture_role in forge producer publisher reviewer verifier; do fixture_permissions='["core.perm.candidate-repository.write.v2","core.perm.evidence.write.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"]' fixture_tools='[]' fixture_extra='' + [ "$fixture_variant" = default ] || fixture_adapter_version="fake-$fixture_forge_variant" ;; producer) fixture_package=$fixture_producer_package @@ -135,6 +160,7 @@ for fixture_role in forge producer publisher reviewer verifier; do fixture_permissions='["core.perm.evidence.write.v1","core.perm.model.invoke.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"]' fixture_tools="[$fixture_tool]" fixture_extra=",\"config_contract_ref\":$fixture_config_scope" + [ "$fixture_variant" = default ] || fixture_adapter_version="fake-$fixture_producer_variant" ;; publisher) fixture_package=$fixture_publisher_package @@ -162,7 +188,7 @@ for fixture_role in forge producer publisher reviewer verifier; do ;; esac /usr/bin/printf '%s\n' \ - "{\"schema_version\":2,\"kind\":\"adapter_manifest\",\"id\":\"manifest.$fixture_role\",\"body\":{\"adapter_version\":\"v1\",\"package_ref\":$fixture_package,\"offered_roles\":[\"$fixture_role\"],\"offered_execution_kinds\":[\"$fixture_execution\"],\"offered_capabilities\":$fixture_capabilities,\"offered_permissions\":$fixture_permissions,\"offered_tools\":$fixture_tools$fixture_extra}}" | + "{\"schema_version\":2,\"kind\":\"adapter_manifest\",\"id\":\"manifest.$fixture_role\",\"body\":{\"adapter_version\":\"$fixture_adapter_version\",\"package_ref\":$fixture_package,\"offered_roles\":[\"$fixture_role\"],\"offered_execution_kinds\":[\"$fixture_execution\"],\"offered_capabilities\":$fixture_capabilities,\"offered_permissions\":$fixture_permissions,\"offered_tools\":$fixture_tools$fixture_extra}}" | fixture_canonical "$fixture_manifests/manifests/$fixture_role.json" /bin/bash "$fixture_core" validate-document "$fixture_manifests/manifests/$fixture_role.json" done @@ -253,7 +279,8 @@ for fixture_role in forge producer publisher reviewer verifier; do fixture_bindings=$("$fixture_jq" -S -c --argjson binding "$fixture_binding" '. + [$binding] | sort_by(.binding_id)' <<< "$fixture_bindings") done "$fixture_jq" -S -c -n --argjson bindings "$fixture_bindings" \ - '{schema_version:2,kind:"profile",id:"profile.example",body:{profile_version:"v1",bindings:$bindings}}' \ + --arg profile_id "$fixture_profile_id" \ + '{schema_version:2,kind:"profile",id:$profile_id,body:{profile_version:"v1",bindings:$bindings}}' \ > "$fixture_profile/profiles/default.json" /bin/bash "$fixture_core" validate-document "$fixture_profile/profiles/default.json" /bin/cp "$fixture_profile/profiles/default.json" "$fixture_profile/default.json"