diff --git a/README.md b/README.md index a99b4e5..868a2e1 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,33 @@ The payload is inactive and unqualified. It does not call Claude Code, invoke a model, use a credential or network, write a target, publish, or activate a profile. +## Inactive local Git materializer protocol + +`adapters/local-git-materializer/v1/protocol.jq` defines the pure input, receipt, +and stage-result boundary for the existing portable-core v2 +`core.forge.materialize-candidate.v2` capability. It validates a complete profile, +resolved profile, manifest set, exact stage request, materialization contract, and +patch payload links before projecting a canonical path-free receipt and core-valid +result. The caller first canonicalizes and hashes both payloads, supplies those +verified content-and-digest pairs in the trust context, and keeps raw payloads +separate; changed bytes are rejected before any projection. + +Successful result projection likewise requires the raw materialization receipt and +its caller-verified content-and-digest pair. The protocol rechecks the receipt's +request, attempt, source, candidate, path count, and changed/no-change relation +before its digest may back passing evidence. + +This stage contains no materialization executable. Its fixture builder is test-only +and creates synthetic JSON under a caller-owned test directory; it is not a product +execution seam. The protocol cannot read a repository, write a candidate, invoke a +hook or filter, use a credential or network, contact a provider, or perform an +external effect. + +A later runtime PR can consume this exact protocol and test fixture without copying +them. That PR must separately prove the physical Git and scratch boundaries before +any manifest or profile may bind the package. Nothing here is qualified, selected, +installed, or activated. + ## Inactive dormant publisher normalizer payload `adapters/dormant-publisher/v1/normalize.jq` validates one bounded publisher diff --git a/RESTORE.md b/RESTORE.md index 3c94c7a..06d7ded 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -526,6 +526,30 @@ The pure jq payload is inactive, offline, and unqualified. It does not call Clau Code, invoke a model, use a credential or network, write a target, publish, or activate a profile. +Restore the three paths in the manifest's inactive local Git materializer protocol +block, then run: + +```sh +bash scripts/test/local-git-materializer-protocol.test.sh +``` + +This builds only synthetic JSON fixtures. It validates the exact portable-core v2 +profile, request, manifest, contract, payload, receipt, and result relations. The +negative matrix rejects malformed, stale, duplicate, relabelled, unsafe-path, +expanded-mode, and weakened-limit inputs, and repeat checks require canonical +output. It also requires caller-verified content-and-digest payload pairs, binds the +source repository, commit, and tree to the request, enforces patch and changed-path +limits, and revalidates the envelope before every projection. A stage result also +requires a caller-verified receipt pair whose request, attempt, source, limits, and +changed/no-change outcome all match before the receipt digest can back passing +evidence. + +There is no materialization executable in this stage. Restoring it cannot read or +write a Git repository, create a candidate, run provider tooling, use a credential +or network, grant authority or qualification, or perform an external effect. A +later runtime PR may consume the protocol and test fixture; only a still-later +assembly may add a manifest after the complete package has a durable main commit. + Restore the two paths in the manifest's inactive dormant publisher normalizer payload block, then run: diff --git a/adapters/local-git-materializer/v1/protocol.jq b/adapters/local-git-materializer/v1/protocol.jq new file mode 100644 index 0000000..d5bdf2b --- /dev/null +++ b/adapters/local-git-materializer/v1/protocol.jq @@ -0,0 +1,435 @@ +import "schema" as schema; +import "profile_graph" as profile; +import "stage_request" as request; + +def exact($required; $optional): + . as $value | + type == "object" and + ((keys_unsorted - ($required + $optional)) | length) == 0 and + all($required[]; . as $key | $value | has($key)); + +def pair($kind): + profile::document_pair_ok($kind); + +def path_ok: + type == "string" and utf8bytelength >= 1 and utf8bytelength <= 4096 and + (test("[\\x{0000}-\\x{001f}\\x{007f}-\\x{009f}]") | not) and + (startswith("/") | not) and (contains("\\") | not) and + (split("/") | + all(.[]; + . != "" and . != "." and . != ".." and + (ascii_downcase != ".git") and + (endswith(".") | not) and (endswith(" ") | not))); + +def attempt_ok: + exact( + ["attempt_id","attempt_number","result_id","started_at","finished_at", + "recorded_at"]; + []) and + (.attempt_id | schema::id_ok) and + (.attempt_number | schema::int_ok) and .attempt_number >= 1 and + (.result_id | schema::id_ok) and + (.started_at | schema::time_ok) and + (.finished_at | schema::time_ok) and + (.recorded_at | schema::time_ok) and + .started_at <= .finished_at and .finished_at <= .recorded_at; + +def payload_ok: + exact(["input_id","media_type","data"];[]) and + (.input_id | schema::id_ok) and + (.media_type | schema::media_type_ok) and + (.data | type == "string" and utf8bytelength <= 2097152); + +def verified_payload_ok: + exact(["input_id","content","sha256"];[]) and + (.input_id | schema::id_ok) and + (.content | + exact(["media_type","data"];[]) and + (.media_type | schema::media_type_ok) and + (.data | type == "string" and utf8bytelength <= 2097152)) and + (.sha256 | schema::sha256_ok); + +def materialization_contract_ok: + exact( + ["schema_version","kind","allowed_paths","max_patch_bytes", + "max_changed_paths","allowed_modes","allow_binary_patch", + "allow_symlinks","allow_submodules","candidate_repository_kind"]; + []) and + .schema_version == 1 and .kind == "local_git_materialization_contract" and + (.allowed_paths | + type == "array" and length >= 1 and length <= 256 and + all(.[];path_ok) and . == sort and length == (unique | length)) and + (.max_patch_bytes | schema::int_ok) and + .max_patch_bytes >= 1 and .max_patch_bytes <= 1048576 and + (.max_changed_paths | schema::int_ok) and + .max_changed_paths >= 1 and .max_changed_paths <= 256 and + .max_changed_paths <= (.allowed_paths | length) and + .allowed_modes == ["100644","100755"] and + .allow_binary_patch == false and .allow_symlinks == false and + .allow_submodules == false and .candidate_repository_kind == "bare"; + +def input_content_ref($body; $input_id): + [$body.inputs[] | + select(.input_id == $input_id and .value.type == "artifact" and + .value.value.type == "content") | + .value.value.value] as $matches | + if ($matches | length) == 1 then $matches[0] else null end; + +def payload_for($input; $input_id): + [$input.payloads[] | select(.input_id == $input_id)] as $matches | + if ($matches | length) == 1 then $matches[0] else null end; + +def verified_payload_for($input; $input_id): + [$input.trust_context.verified_payloads[] | + select(.input_id == $input_id)] as $matches | + if ($matches | length) == 1 then $matches[0] else null end; + +def payload_matches_input($input; $input_id): + input_content_ref($input.stage_request.content.body;$input_id) as $ref | + payload_for($input;$input_id) as $payload | + verified_payload_for($input;$input_id) as $verified | + $ref != null and $payload != null and $verified != null and + $payload == ({input_id:$input_id} + $verified.content) and + $verified.content.media_type == $ref.media_type and + $verified.sha256 == $ref.sha256; + +def git_object_input($body; $input_id): + [$body.inputs[] | + select(.input_id == $input_id and .value.type == "artifact" and + .value.value.type == "git-object") | + .value.value.value] as $matches | + if ($matches | length) == 1 then $matches[0] else null end; + +def selected_binding($input): + [$input.resolved_profile.content.body.bindings[] | + select(.binding.binding_id == + $input.stage_request.content.body.operation.binding_id)]; + +def selected_manifest($input; $binding): + [$input.manifests[] | + select(profile::document_ref_for_pair(.) == $binding.binding.manifest_ref)]; + +def materializer_manifest_ok: + (.content | profile::adapter_manifest_self_ok) and + .content.id == "adapter.local-git-materializer.v1" and + .content.body.adapter_version == "v1" and + .content.body.offered_roles == ["forge"] and + .content.body.offered_execution_kinds == ["deterministic"] and + .content.body.offered_capabilities == ["core.forge.materialize-candidate.v2"] and + .content.body.offered_permissions == [ + "core.perm.candidate-repository.write.v2", + "core.perm.evidence.write.v1", + "core.perm.scratch.write.v1", + "core.perm.target.read.v1" + ] and + .content.body.offered_tools == [] and + (.content.body | has("config_contract_ref") | not); + +def core_relations_ok($input): + ($input.profile | pair("profile")) and + ($input.resolved_profile | pair("resolved_profile")) and + ($input.manifests | + type == "array" and length >= 1 and length <= 8 and + all(.[];pair("adapter_manifest")) and + (map(.content.id) | . == sort and length == (unique | length))) and + profile::profile_set_ok( + $input.profile;$input.resolved_profile;$input.manifests) and + ($input.stage_request | pair("stage_request")) and + ($input.stage_request.content | request::document_self_ok) and + request::stage_request_resolved_ref_ok( + $input.stage_request;$input.resolved_profile) and + request::stage_request_resolved_relation_ok( + $input.stage_request.content.body;$input.resolved_profile.content.body); + +def materializer_relations_ok($input): + $input.stage_request.content.body as $body | + selected_binding($input) as $bindings | + ($bindings | length) == 1 and + $bindings[0] as $binding | + selected_manifest($input;$binding) as $manifests | + ($manifests | length) == 1 and + ($manifests[0] | materializer_manifest_ok) and + profile::binding_manifest_graph_ok( + $binding.binding;$binding;$manifests[0]) and + $body.operation == { + role:"forge", + binding_id:$binding.binding.binding_id, + 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:$body.operation.arguments + } and + $body.operation.arguments.network_mode == "deny" and + $body.target_revision.state == "present" and + $binding.binding.execution_kind == "deterministic" and + ($binding.binding | has("config_ref") | not) and + ($binding.binding | has("prompt_ref") | not) and + ($binding.binding | has("model_request") | not) and + $binding.binding.skill_refs == [] and $binding.binding.requested_tools == [] and + $input.attempt.started_at >= $body.requested_at; + +def trust_context_ok: + exact(["verified_payloads"];[]) and + (.verified_payloads | + type == "array" and length == 2 and all(.[];verified_payload_ok) and + (map(.input_id) | . == sort and length == (unique | length))); + +def payload_relations_ok($input): + $input.stage_request.content.body.operation.arguments as $arguments | + $arguments.materialization_contract.input_id as $contract_id | + (payload_for($input;$contract_id).data | + try fromjson catch null) as $contract | + payload_for($input;"input.producer-patch") as $patch | + ($input.payloads | + type == "array" and length == 2 and all(.[];payload_ok) and + (map(.input_id) | . == sort and length == (unique | length))) and + ($input.trust_context | trust_context_ok) and + payload_matches_input($input;$contract_id) and + payload_matches_input($input;"input.producer-patch") and + (payload_for($input;$contract_id).media_type == "application/json") and + ($patch.media_type == "text/x-diff") and + ($contract | materialization_contract_ok) and + ($patch.data | utf8bytelength) <= $contract.max_patch_bytes; + +def input_ok: + . as $input | + exact( + ["schema_version","kind","attempt","profile","resolved_profile", + "manifests","stage_request","payloads","trust_context"]; + []) and + .schema_version == 1 and .kind == "local_git_materialization_input" and + (.attempt | attempt_ok) and + core_relations_ok($input) and materializer_relations_ok($input) and + payload_relations_ok($input); + +def document_ref($pair): profile::document_ref_for_pair($pair); + +def receipt: + . as $input | + $input.stage_request.content.body as $request_body | + $request_body.operation.arguments as $arguments | + $arguments.materialization_contract.input_id as $contract_id | + (payload_for($input;$contract_id).data | fromjson) as $contract | + git_object_input($request_body;$arguments.source_tree_input_id) as $source_ref | + $request_body.target_revision.value as $target_revision | + ($ARGS.named.source_repository_id // "") as $source_repository_id | + ($ARGS.named.source_hash_algorithm // "") as $source_hash_algorithm | + ($ARGS.named.source_commit // "") as $source_commit | + ($ARGS.named.source_tree // "") as $source_tree | + ($ARGS.named.candidate_commit // "") as $candidate_commit | + ($ARGS.named.candidate_tree // "") as $candidate_tree | + ($ARGS.named.changed_path_count // "") as $changed_path_count | + ($ARGS.named.changed_paths_sha256 // "") as $changed_paths_sha256 | + if input_ok and + $source_repository_id == $request_body.target_repository_id and + $target_revision == { + repository_id:$source_repository_id, + hash_algorithm:$source_hash_algorithm, + commit_id:$source_commit + } and + $source_ref != null and $source_ref.revision == $target_revision and + $source_ref.location == {kind:"root"} and + $source_ref.object_type == "tree" and $source_ref.mode == "040000" and + $source_ref.object_id == $source_tree and + (($source_hash_algorithm == "sha1" and + ($source_commit | test("\\A[0-9a-f]{40}\\z")) and + ($source_tree | test("\\A[0-9a-f]{40}\\z")) and + ($candidate_commit | test("\\A[0-9a-f]{40}\\z")) and + ($candidate_tree | test("\\A[0-9a-f]{40}\\z"))) or + ($source_hash_algorithm == "sha256" and + ($source_commit | test("\\A[0-9a-f]{64}\\z")) and + ($source_tree | test("\\A[0-9a-f]{64}\\z")) and + ($candidate_commit | test("\\A[0-9a-f]{64}\\z")) and + ($candidate_tree | test("\\A[0-9a-f]{64}\\z")))) and + ($changed_paths_sha256 | schema::sha256_ok) and + ($changed_path_count | tonumber | schema::int_ok) and + ($changed_path_count | tonumber) <= $contract.max_changed_paths then + { + schema_version:1, + kind:"candidate_materialization_receipt", + adapter:{id:"adapter.local-git-materializer.v1",version:"v1",status:"inactive"}, + attempt:{ + attempt_id:$input.attempt.attempt_id, + attempt_number:$input.attempt.attempt_number + }, + request_ref:document_ref($input.stage_request), + resolved_profile_ref:document_ref($input.resolved_profile), + manifest_ref:(selected_binding($input)[0].binding.manifest_ref), + materialization_contract_ref: + input_content_ref($input.stage_request.content.body;$contract_id), + patch_ref: + input_content_ref($input.stage_request.content.body;"input.producer-patch"), + source:{ + repository_id:$source_repository_id, + hash_algorithm:$source_hash_algorithm, + commit_id:$source_commit, + tree_id:$source_tree + }, + candidate:{ + repository_kind:"bare", + hash_algorithm:$source_hash_algorithm, + commit_id:$candidate_commit, + tree_id:$candidate_tree, + parent_commit_id:$source_commit + }, + changed_paths:{ + count:($changed_path_count | tonumber), + sha256:$changed_paths_sha256 + } + } + else error("E_RECEIPT") end; + +def recorded($value; $source_ref): + {state:"recorded",value:$value,source_ref:$source_ref}; +def not_applicable: {state:"not-applicable"}; + +def oid_ok($algorithm): + type == "string" and + if $algorithm == "sha1" then test("\\A[0-9a-f]{40}\\z") + elif $algorithm == "sha256" then test("\\A[0-9a-f]{64}\\z") + else false + end; + +def verified_receipt_pair_ok: + exact(["content","sha256"];[]) and + (.content | type == "object") and (.sha256 | schema::sha256_ok); + +def receipt_relations_ok($input; $value): + $input.stage_request.content.body as $request_body | + $request_body.operation.arguments as $arguments | + $arguments.materialization_contract.input_id as $contract_id | + (payload_for($input;$contract_id).data | fromjson) as $contract | + git_object_input($request_body;$arguments.source_tree_input_id) as $source_ref | + $request_body.target_revision.value as $target_revision | + selected_binding($input)[0].binding as $binding | + ($value | exact( + ["schema_version","kind","adapter","attempt","request_ref", + "resolved_profile_ref","manifest_ref","materialization_contract_ref", + "patch_ref","source","candidate","changed_paths"]; + [])) and + $value.schema_version == 1 and + $value.kind == "candidate_materialization_receipt" and + $value.adapter == { + id:"adapter.local-git-materializer.v1",version:"v1",status:"inactive" + } and + $value.attempt == { + attempt_id:$input.attempt.attempt_id, + attempt_number:$input.attempt.attempt_number + } and + $value.request_ref == document_ref($input.stage_request) and + $value.resolved_profile_ref == document_ref($input.resolved_profile) and + $value.manifest_ref == $binding.manifest_ref and + $value.materialization_contract_ref == + input_content_ref($request_body;$contract_id) and + $value.patch_ref == input_content_ref($request_body;"input.producer-patch") and + $source_ref.location == {kind:"root"} and + $value.source == { + repository_id:$request_body.target_repository_id, + hash_algorithm:$target_revision.hash_algorithm, + commit_id:$target_revision.commit_id, + tree_id:$source_ref.object_id + } and + ($value.candidate | + exact(["repository_kind","hash_algorithm","commit_id","tree_id", + "parent_commit_id"];[]) and + .repository_kind == "bare" and + .hash_algorithm == $target_revision.hash_algorithm and + (.commit_id | oid_ok($target_revision.hash_algorithm)) and + (.tree_id | oid_ok($target_revision.hash_algorithm)) and + .parent_commit_id == $target_revision.commit_id) and + ($value.changed_paths | + exact(["count","sha256"];[]) and + (.count | schema::int_ok) and .count <= $contract.max_changed_paths and + (.sha256 | schema::sha256_ok)); + +def receipt_outcome_ok($value; $outcome): + if $outcome == "changed" then + $value.changed_paths.count >= 1 and + ($value.candidate.commit_id != $value.source.commit_id and + $value.candidate.tree_id != $value.source.tree_id) + elif $outcome == "no-change" then + $value.changed_paths.count == 0 and + $value.candidate.commit_id == $value.source.commit_id and + $value.candidate.tree_id == $value.source.tree_id + else false + end; + +def stage_result: + . as $input | + ($ARGS.named.receipt_json // "" | try fromjson catch null) as $receipt | + ($ARGS.named.verified_receipt_json // "" | try fromjson catch null) as $verified | + ($ARGS.named.outcome // "") as $outcome | + request::expected_execution_projection( + $input.stage_request.content.body;$input.resolved_profile.content.body) as $projection | + { + content_id:"candidate.materialization.receipt", + media_type:"application/json", + sha256:$verified.sha256 + } as $receipt_ref | + if input_ok and ($verified | verified_receipt_pair_ok) and + $receipt == $verified.content and + receipt_relations_ok($input;$receipt) and + receipt_outcome_ok($receipt;$outcome) and $projection != null then + { + schema_version:2, + kind:"stage_result", + id:$input.attempt.result_id, + body:{ + request_ref:document_ref($input.stage_request), + resolved_profile_ref:document_ref($input.resolved_profile), + attempt_id:$input.attempt.attempt_id, + attempt_number:$input.attempt.attempt_number, + reported_by:$projection.performer, + status:"completed", + outcome:{family:"change",value:$outcome}, + outputs:(if $outcome == "changed" then [{ + output_id:$input.stage_request.content.body.operation.arguments.candidate_output_id, + ref:$receipt_ref + }] else [] end), + diagnostics:[], + execution:{ + performer:$projection.performer, + actual_binding:$projection.actual_binding, + environment:$projection.environment, + used_capability:$projection.used_capability, + metadata:{ + kind:"deterministic", + provider:not_applicable, + model:not_applicable, + snapshot:not_applicable, + effort:not_applicable, + prompt:not_applicable, + skills:not_applicable, + tools:recorded([];$receipt_ref) + } + }, + evidence:[{ + evidence_id:"evidence.local-git-materialization", + kind:"deterministic", + verdict:"passed", + proof_ref:$receipt_ref + }], + started_at:$input.attempt.started_at, + finished_at:$input.attempt.finished_at, + recorded_at:$input.attempt.recorded_at + } + } + else error("E_RESULT") end; + +if $command == "validate-input" then input_ok +elif $command == "contract" then + . as $input | + if input_ok then + payload_for( + $input; + $input.stage_request.content.body.operation.arguments.materialization_contract.input_id).data + else error("E_INPUT") end +elif $command == "patch" then + if input_ok then payload_for(.;"input.producer-patch").data else error("E_INPUT") end +elif $command == "receipt" then receipt +elif $command == "stage-result" then stage_result +else error("E_COMMAND") end diff --git a/ci/required-files.txt b/ci/required-files.txt index 9a04033..e3bf9e5 100644 --- a/ci/required-files.txt +++ b/ci/required-files.txt @@ -256,6 +256,11 @@ scripts/test/default-github-actions-ci-adapter.test.sh adapters/claude-code-producer/v1/normalize.jq scripts/test/default-claude-code-producer-adapter.test.sh +# Inactive provider-neutral local Git materializer protocol +adapters/local-git-materializer/v1/protocol.jq +scripts/test/local-git-materializer-fixtures.sh +scripts/test/local-git-materializer-protocol.test.sh + # Inactive dormant publisher normalizer payload adapters/dormant-publisher/v1/normalize.jq scripts/test/default-dormant-publisher-adapter.test.sh diff --git a/scripts/test/local-git-materializer-fixtures.sh b/scripts/test/local-git-materializer-fixtures.sh new file mode 100755 index 0000000..6e68f39 --- /dev/null +++ b/scripts/test/local-git-materializer-fixtures.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +export LC_ALL=C +umask 077 + +[ "$#" -eq 6 ] && [ "$1" = build ] || exit 64 +output_root=$2 +jq_bin=$3 +source_algorithm=$4 +source_commit=$5 +source_tree=$6 +case "$output_root:$jq_bin" in /*:/*) ;; *) exit 64 ;; esac +[ -x "$jq_bin" ] && [ -f "$jq_bin" ] && [ ! -L "$jq_bin" ] && + [ "$($jq_bin --version 2>/dev/null)" = jq-1.6 ] || exit 1 +case "$source_algorithm" in + sha1) oid_pattern='^[0-9a-f]{40}$' ;; + sha256) oid_pattern='^[0-9a-f]{64}$' ;; + *) exit 64 ;; +esac +[[ "$source_commit" =~ $oid_pattern ]] && [[ "$source_tree" =~ $oid_pattern ]] || exit 64 +[ ! -e "$output_root" ] || exit 1 +/bin/mkdir -m 700 "$output_root" "$output_root/manifests" + +script_dir=$(CDPATH='' cd -P -- "${BASH_SOURCE[0]%/*}" && pwd -P) +repo_root=$(CDPATH='' cd -P -- "$script_dir/../.." && pwd -P) +fixtures="$repo_root/scripts/test" +sha_file() { /usr/bin/shasum -a 256 "$1" | /usr/bin/awk '{print $1}'; } + +contract_file="$output_root/materialization-contract.json" +"$jq_bin" -S -c -n '{ + schema_version:1,kind:"local_git_materialization_contract", + allowed_paths:["source.txt"],max_patch_bytes:65536,max_changed_paths:1, + allowed_modes:["100644","100755"],allow_binary_patch:false, + allow_symlinks:false,allow_submodules:false,candidate_repository_kind:"bare" +}' > "$contract_file" +patch_file="$output_root/producer.patch" +printf '%s\n' \ + 'diff --git a/source.txt b/source.txt' \ + '--- a/source.txt' \ + '+++ b/source.txt' \ + '@@ -1,2 +1,3 @@' \ + ' alpha' \ + ' beta' \ + '+gamma' > "$patch_file" +contract_sha=$(sha_file "$contract_file") +patch_sha=$(sha_file "$patch_file") + +manifest_dir="$output_root/manifests" +forge_manifest="$manifest_dir/forge.json" +"$jq_bin" -L "$fixtures" -S -c -n ' + import "portable-core-profile-graph-fixtures" as f; + def v2: walk(if type=="object" and has("schema_version") then .schema_version=2 else . end); + { + schema_version:2,kind:"adapter_manifest",id:"adapter.local-git-materializer.v1", + body:{adapter_version:"v1",package_ref:(f::blob("adapters/local-git-materializer/v1";"6") | + .location={kind:"root"} | .object_type="tree" | .mode="040000"), + offered_roles:["forge"],offered_execution_kinds:["deterministic"], + offered_capabilities:["core.forge.materialize-candidate.v2"], + offered_permissions:["core.perm.candidate-repository.write.v2", + "core.perm.evidence.write.v1","core.perm.scratch.write.v1", + "core.perm.target.read.v1"],offered_tools:[]}} + | v2 +' > "$forge_manifest" +for role in producer publisher reviewer verifier; do + "$jq_bin" -L "$fixtures" -S -c -n --arg role "$role" ' + import "portable-core-profile-graph-fixtures" as f; + def v2: walk(if type=="object" and has("schema_version") then .schema_version=2 else . end); + f::manifest($role) | v2 + ' > "$manifest_dir/$role.json" +done +manifest_shas=$( + "$jq_bin" -S -c -n \ + --arg forge "$(sha_file "$forge_manifest")" \ + --arg producer "$(sha_file "$manifest_dir/producer.json")" \ + --arg publisher "$(sha_file "$manifest_dir/publisher.json")" \ + --arg reviewer "$(sha_file "$manifest_dir/reviewer.json")" \ + --arg verifier "$(sha_file "$manifest_dir/verifier.json")" \ + '{forge:$forge,producer:$producer,publisher:$publisher,reviewer:$reviewer,verifier:$verifier}' +) + +profile_file="$output_root/profile.json" +"$jq_bin" -L "$fixtures" -S -c -n --argjson shas "$manifest_shas" \ + --slurpfile forge "$forge_manifest" ' + import "portable-core-profile-graph-fixtures" as f; + def v2: walk(if type=="object" and has("schema_version") then .schema_version=2 else . end); + def forge_binding: { + binding_id:"binding.forge",role:"forge", + manifest_ref:{schema_version:2,kind:"adapter_manifest",id:$forge[0].id,sha256:$shas.forge}, + execution_kind:"deterministic",adapter_instance_id:"instance.forge", + principal_id:"principal.forge",execution_boundary_id:"boundary.forge", + authority_ref:f::scope("authority";"authority-forge";f::sha("5")), + package_ref:$forge[0].body.package_ref,skill_refs:[],requested_tools:[], + requested_capabilities:["core.forge.materialize-candidate.v2"], + requested_permissions:["core.perm.candidate-repository.write.v2", + "core.perm.evidence.write.v1","core.perm.scratch.write.v1", + "core.perm.target.read.v1"]}; + f::profile_doc($shas) | v2 | + .body.bindings += [forge_binding] | .body.bindings |= sort_by(.binding_id) +' > "$profile_file" +profile_sha=$(sha_file "$profile_file") + +resolved_file="$output_root/resolved-profile.json" +"$jq_bin" -L "$fixtures" -S -c -n --argjson shas "$manifest_shas" \ + --slurpfile profile "$profile_file" --slurpfile forge "$forge_manifest" \ + --arg profile_sha "$profile_sha" ' + import "portable-core-profile-graph-fixtures" as f; + def v2: walk(if type=="object" and has("schema_version") then .schema_version=2 else . end); + f::resolved_profile_doc($profile[0];$profile_sha;$shas) | v2 | + .body.bindings |= map(if .binding.role=="forge" then + .adapter_implementation={id:$forge[0].id,version:"v1"} | + .manifest_source=f::source_value(f::blob("manifests/forge.json";"a");"canonical-json";$shas.forge) | + .package_source=f::source_value($forge[0].body.package_ref;"raw-bytes";f::sha("6")) | + .config_source={state:"absent"} | .prompt_source={state:"absent"} | + .skill_sources=[] | .tool_sources=[] + else . end) +' > "$resolved_file" +resolved_sha=$(sha_file "$resolved_file") + +request_file="$output_root/stage-request.json" +"$jq_bin" -L "$fixtures" -S -c -n \ + --arg resolved_sha "$resolved_sha" --arg algorithm "$source_algorithm" \ + --arg source_commit "$source_commit" --arg source_tree "$source_tree" \ + --arg contract_sha "$contract_sha" --arg patch_sha "$patch_sha" ' + import "portable-core-stage-request-fixtures" as f; + def v2: walk(if type=="object" and has("schema_version") then .schema_version=2 else . end); + def revision: {repository_id:"fixture.target",hash_algorithm:$algorithm,commit_id:$source_commit}; + def content($id;$media;$sha): {content_id:$id,media_type:$media,sha256:$sha}; + def named($id;$ref): {input_id:$id,value:{type:"artifact",value:{type:"content",value:$ref}}}; + f::request_doc("producer";$resolved_sha) | v2 | + .id="request.local-git-materializer" | .body.stage_id="stage.materialize" | + .body.target_repository_id="fixture.target" | + .body.target_revision={state:"present",value:revision} | + .body.source={state:"present",value:{type:"git-object",value:{revision:revision, + location:{kind:"root"},object_type:"tree",object_id:$source_tree,mode:"040000"}}} | + .body.base={state:"present",value:revision} | + .body.inputs=([ + f::named_content_input("finish";f::sha("1")), + named("input.materialize";content("payload-materialize";"application/json";$contract_sha)), + named("input.producer-patch";content("producer.patch";"text/x-diff";$patch_sha)), + {input_id:"input.source-tree",value:{type:"artifact",value:{type:"git-object",value:{ + revision:revision,location:{kind:"root"},object_type:"tree",object_id:$source_tree,mode:"040000"}}}}, + f::named_content_input("verify";f::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:{ref:(f::scope("output-contract";"materialize";f::sha("3")) | + .subject_ref.value.value=content("payload-materialize";"application/json";$contract_sha)), + input_id:"input.materialize"},network_mode:"deny"}} | + .body.required_evidence_kinds=["deterministic"] +' > "$request_file" +request_sha=$(sha_file "$request_file") + +input_file="$output_root/input.json" +"$jq_bin" -S -c -n --slurpfile profile "$profile_file" \ + --slurpfile resolved "$resolved_file" --slurpfile request "$request_file" \ + --slurpfile forge "$forge_manifest" --slurpfile producer "$manifest_dir/producer.json" \ + --slurpfile publisher "$manifest_dir/publisher.json" --slurpfile reviewer "$manifest_dir/reviewer.json" \ + --slurpfile verifier "$manifest_dir/verifier.json" --rawfile contract "$contract_file" \ + --rawfile patch "$patch_file" --argjson shas "$manifest_shas" \ + --arg profile_sha "$profile_sha" --arg resolved_sha "$resolved_sha" \ + --arg request_sha "$request_sha" --arg contract_sha "$contract_sha" --arg patch_sha "$patch_sha" ' + {schema_version:1,kind:"local_git_materialization_input", + attempt:{attempt_id:"attempt.materialize",attempt_number:1,result_id:"result.materialize", + started_at:"2026-08-30T00:00:01Z",finished_at:"2026-08-30T00:00:02Z", + recorded_at:"2026-08-30T00:00:03Z"}, + profile:{content:$profile[0],sha256:$profile_sha}, + resolved_profile:{content:$resolved[0],sha256:$resolved_sha}, + manifests:([ + {content:$forge[0],sha256:$shas.forge}, + {content:$producer[0],sha256:$shas.producer}, + {content:$publisher[0],sha256:$shas.publisher}, + {content:$reviewer[0],sha256:$shas.reviewer}, + {content:$verifier[0],sha256:$shas.verifier}] | sort_by(.content.id)), + stage_request:{content:$request[0],sha256:$request_sha}, + payloads:([ + {input_id:"input.materialize",media_type:"application/json",data:$contract}, + {input_id:"input.producer-patch",media_type:"text/x-diff",data:$patch}] + | sort_by(.input_id)), + trust_context:{verified_payloads:([ + {input_id:"input.materialize",content:{media_type:"application/json",data:$contract}, + sha256:$contract_sha}, + {input_id:"input.producer-patch",content:{media_type:"text/x-diff",data:$patch}, + sha256:$patch_sha}] + | sort_by(.input_id))}} +' > "$input_file" diff --git a/scripts/test/local-git-materializer-protocol.test.sh b/scripts/test/local-git-materializer-protocol.test.sh new file mode 100755 index 0000000..1f8470c --- /dev/null +++ b/scripts/test/local-git-materializer-protocol.test.sh @@ -0,0 +1,356 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +export LC_ALL=C +umask 077 + +root=$(CDPATH='' cd -P -- "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P) +protocol="$root/adapters/local-git-materializer/v1/protocol.jq" +fixture_builder="$root/scripts/test/local-git-materializer-fixtures.sh" +test_tmp_base=${TMPDIR:-/tmp} +tmp=$(/usr/bin/mktemp -d "${test_tmp_base%/}/ystack-materializer-protocol.XXXXXX") +tmp=$(CDPATH='' cd -P -- "$tmp" && pwd -P) +cleanup() { /bin/rm -rf -- "$tmp"; } +trap cleanup EXIT + +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 + Linux:x86_64) jq_asset=jq-linux64; jq_sha=af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44 ;; + Darwin:x86_64|Darwin:arm64) jq_asset=jq-osx-amd64; jq_sha=5c0a0a3ea600f302ee458b30317425dd9632d1ad8882259fcaf4e9b868b2b1ef ;; + *) printf 'FAIL: unsupported host %s\n' "$platform" >&2; exit 1 ;; +esac +jq_source="${TMPDIR:-/tmp}/ystack-portable-core-jq16/$jq_asset" +[ -f "$jq_source" ] && [ ! -L "$jq_source" ] && [ "$(sha_file "$jq_source")" = "$jq_sha" ] || { + printf '%s\n' 'FAIL: pinned jq 1.6 is required' >&2 + exit 1 +} +bin="$tmp/bin" +/bin/mkdir -m 700 "$bin" +if [ "$platform" = Darwin:arm64 ]; then + printf '%s\n' '#!/bin/bash' "exec /usr/bin/arch -x86_64 '$jq_source' \"\$@\"" > "$bin/jq" +else + /bin/cp "$jq_source" "$bin/jq" +fi +/bin/chmod 0555 "$bin/jq" +jq_bin="$bin/jq" +export PATH="$bin:/usr/bin:/bin" +[ "$($jq_bin --version)" = jq-1.6 ] || exit 1 +generation=$(/usr/bin/sed -n \ + "s/^PORTABLE_CORE_GENERATION='\(g-[0-9a-f]\{64\}\)'$/\1/p" \ + "$root/scripts/core-contract.sh") +[[ "$generation" =~ ^g-[0-9a-f]{64}$ ]] || exit 1 +$jq_bin -e --arg generation "$generation" ' + [.[] | select(.generation_id == $generation and + .semantic_identity == "core.contracts.v2")] | length == 1 +' "$root/core/v2/generation-registry.json" >/dev/null || exit 1 +modules="$root/core/v2/generations/$generation/modules" +core="$root/scripts/core-contract.sh" + +passed=0 +pass() { passed=$((passed + 1)); printf 'ok %s - %s\n' "$passed" "$1"; } +fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; } + +fixture="$tmp/fixture" +source_commit=$(printf '%040d' 0 | /usr/bin/tr 0 1) +source_tree=$(printf '%040d' 0 | /usr/bin/tr 0 2) +"$fixture_builder" build "$fixture" "$jq_bin" sha1 "$source_commit" "$source_tree" +input="$fixture/input.json" + +manifest_args=( + "$fixture/manifests/forge.json" + "$fixture/manifests/producer.json" + "$fixture/manifests/publisher.json" + "$fixture/manifests/reviewer.json" + "$fixture/manifests/verifier.json" +) +for document in "$fixture/profile.json" "$fixture/resolved-profile.json" \ + "$fixture/stage-request.json" "${manifest_args[@]}"; do + "$core" validate-document "$document" || fail "core-document-${document##*/}" +done +"$core" validate-profile-set "$fixture/profile.json" "$fixture/resolved-profile.json" \ + "${manifest_args[@]}" || fail core-profile-set +"$jq_bin" -L "$modules" -e --arg command validate-input -f "$protocol" "$input" >/dev/null || + fail valid-input +pass 'exact core v2 graph, request, manifest, and payload envelope validate' + +expect_invalid() { + local name=$1 filter=$2 + local candidate="$tmp/$name.json" + "$jq_bin" -S -c "$filter" "$input" > "$candidate" + if "$jq_bin" -L "$modules" -e --arg command validate-input -f "$protocol" \ + "$candidate" >/dev/null 2> "$tmp/$name.err"; then + fail "$name accepted" + fi + pass "$name" +} + +expect_invalid_contract() { + local name=$1 filter=$2 + expect_invalid "$name" "$filter | + (.trust_context.verified_payloads[] | + select(.input_id==\"input.materialize\") | .content.data) = + (.payloads[] | select(.input_id==\"input.materialize\") | .data)" +} + +expect_invalid extra-envelope-field '.unexpected=true' +expect_invalid missing-attempt 'del(.attempt)' +expect_invalid malformed-time '.attempt.finished_at="2026-02-30T00:00:02Z"' +expect_invalid duplicate-payload '.payloads[1]=.payloads[0]' +expect_invalid relabelled-payload '.payloads[1].input_id="input.other"' +expect_invalid payload-media-mismatch '.payloads[1].media_type="application/json"' +expect_invalid changed-after-verification \ + '(.payloads[] | select(.input_id=="input.producer-patch") | .data) += "tamper"' +expect_invalid missing-verified-payload 'del(.trust_context.verified_payloads[0])' +expect_invalid stale-resolved-ref \ + '.stage_request.content.body.resolved_profile_ref.sha256=("0"*64)' +expect_invalid duplicate-manifest '.manifests += [.manifests[0]]' +expect_invalid wrong-manifest-package \ + '(.resolved_profile.content.body.bindings[] | select(.binding.role=="forge") | + .binding.package_ref.object_id)=("0"*40)' +expect_invalid wrong-role '.stage_request.content.body.operation.role="producer"' +expect_invalid wrong-capability \ + '.stage_request.content.body.operation.capability_id="core.harness.produce.v1"' +expect_invalid wrong-permissions \ + '.stage_request.content.body.operation.permissions-=["core.perm.candidate-repository.write.v2"]' +expect_invalid_contract malformed-contract-data \ + '(.payloads[] | select(.input_id=="input.materialize") | .data)="{"' +expect_invalid_contract traversal-contract-path \ + '(.payloads[] | select(.input_id=="input.materialize") | .data) |= + (fromjson | .allowed_paths=["../escape"] | tojson)' +expect_invalid_contract git-contract-path \ + '(.payloads[] | select(.input_id=="input.materialize") | .data) |= + (fromjson | .allowed_paths=[".Git/config"] | tojson)' +expect_invalid_contract contract-mode-expansion \ + '(.payloads[] | select(.input_id=="input.materialize") | .data) |= + (fromjson | .allowed_modes += ["120000"] | tojson)' +expect_invalid_contract contract-zero-byte-limit \ + '(.payloads[] | select(.input_id=="input.materialize") | .data) |= + (fromjson | .max_patch_bytes=0 | tojson)' +expect_invalid_contract contract-change-limit-over-paths \ + '(.payloads[] | select(.input_id=="input.materialize") | .data) |= + (fromjson | .max_changed_paths=2 | tojson)' +expect_invalid_contract contract-allows-binary \ + '(.payloads[] | select(.input_id=="input.materialize") | .data) |= + (fromjson | .allow_binary_patch=true | tojson)' +expect_invalid_contract contract-allows-symlink \ + '(.payloads[] | select(.input_id=="input.materialize") | .data) |= + (fromjson | .allow_symlinks=true | tojson)' +expect_invalid_contract contract-allows-submodule \ + '(.payloads[] | select(.input_id=="input.materialize") | .data) |= + (fromjson | .allow_submodules=true | tojson)' +expect_invalid_contract contract-worktree-output \ + '(.payloads[] | select(.input_id=="input.materialize") | .data) |= + (fromjson | .candidate_repository_kind="worktree" | tojson)' +expect_invalid_contract contract-patch-byte-limit ' + (.payloads[] | select(.input_id=="input.materialize") | .data) |= + (fromjson | .max_patch_bytes=1 | tojson)' + +receipt="$tmp/receipt.json" +candidate_commit=$(printf '%040d' 0 | /usr/bin/tr 0 3) +candidate_tree=$(printf '%040d' 0 | /usr/bin/tr 0 4) +changed_paths_sha=$(printf '%064d' 0 | /usr/bin/tr 0 5) +receipt_args=( + --arg command receipt + --arg source_repository_id fixture.target + --arg source_hash_algorithm sha1 + --arg source_commit "$source_commit" + --arg source_tree "$source_tree" + --arg candidate_commit "$candidate_commit" + --arg candidate_tree "$candidate_tree" + --arg changed_path_count 1 + --arg changed_paths_sha256 "$changed_paths_sha" +) +projection_malformed="$tmp/projection-malformed.json" +"$jq_bin" -S -c '.unexpected=true' "$input" > "$projection_malformed" +for projection_command in contract patch receipt stage-result; do + projection_args=(--arg command "$projection_command") + case "$projection_command" in + receipt) projection_args=("${receipt_args[@]}") ;; + stage-result) + projection_args=(--arg command stage-result --arg outcome changed + --arg receipt_json '{}' --arg verified_receipt_json '{}') + ;; + esac + if "$jq_bin" -L "$modules" "${projection_args[@]}" -f "$protocol" \ + "$projection_malformed" >"$tmp/$projection_command-malformed.out" 2>/dev/null; then + fail "$projection_command projection accepted invalid envelope" + fi + pass "$projection_command validates the current envelope" +done +"$jq_bin" -S -c -L "$modules" "${receipt_args[@]}" -f "$protocol" "$input" > "$receipt" +"$jq_bin" -S -c -L "$modules" "${receipt_args[@]}" -f "$protocol" "$input" > "$tmp/receipt-repeat" +/usr/bin/cmp -s "$receipt" "$tmp/receipt-repeat" || fail receipt-repeat +if /usr/bin/grep -Fq "$tmp" "$receipt" || + "$jq_bin" -e '[..|objects|keys[]] | any(.=="authority" or .=="effects" or .=="qualification")' \ + "$receipt" >/dev/null; then + fail receipt-effect-surface +fi +pass 'canonical receipt is path-free and carries no authority or effect' + +expect_receipt_reject() { + local name=$1 repository_id=$2 commit_id=$3 tree_id=$4 changed_count=$5 + if "$jq_bin" -L "$modules" --arg command receipt \ + --arg source_repository_id "$repository_id" --arg source_hash_algorithm sha1 \ + --arg source_commit "$commit_id" --arg source_tree "$tree_id" \ + --arg candidate_commit "$candidate_commit" --arg candidate_tree "$candidate_tree" \ + --arg changed_path_count "$changed_count" \ + --arg changed_paths_sha256 "$changed_paths_sha" \ + -f "$protocol" "$input" >/dev/null 2>&1; then + fail "$name" + fi + pass "$name" +} +expect_receipt_reject source-repository-mismatch fixture.other \ + "$source_commit" "$source_tree" 1 +expect_receipt_reject source-commit-mismatch fixture.target \ + "$(printf '%040d' 0 | /usr/bin/tr 0 6)" "$source_tree" 1 +expect_receipt_reject source-tree-mismatch fixture.target \ + "$source_commit" "$(printf '%040d' 0 | /usr/bin/tr 0 7)" 1 +expect_receipt_reject changed-path-limit fixture.target \ + "$source_commit" "$source_tree" 2 + +receipt_sha=$(sha_file "$receipt") +verified_receipt="$tmp/verified-receipt.json" +"$jq_bin" -S -c -n --slurpfile receipt "$receipt" --arg sha "$receipt_sha" \ + '{content:$receipt[0],sha256:$sha}' > "$verified_receipt" +stage_result_args=( + --arg command stage-result + --arg outcome changed + --arg receipt_json "$(<"$receipt")" + --arg verified_receipt_json "$(<"$verified_receipt")" +) +result="$tmp/result.json" +"$jq_bin" -S -c -L "$modules" "${stage_result_args[@]}" \ + -f "$protocol" "$input" > "$result" +"$core" validate-stage-run "$fixture/stage-request.json" "$fixture/resolved-profile.json" \ + "$result" || fail core-stage-result +"$jq_bin" -e ' + .body.status=="completed" and .body.outcome=={family:"change",value:"changed"} and + .body.outputs[0].output_id=="candidate.repository" and + .body.evidence==[{evidence_id:"evidence.local-git-materialization",kind:"deterministic", + verdict:"passed",proof_ref:.body.outputs[0].ref}] and + ([..|objects|keys[]] | index("effects")==null) and + ([..|objects|keys[]] | index("qualification")==null) +' "$result" >/dev/null || fail result-surface +"$jq_bin" -S -c -L "$modules" "${stage_result_args[@]}" \ + -f "$protocol" "$input" > "$tmp/result-repeat" +/usr/bin/cmp -s "$result" "$tmp/result-repeat" || fail result-repeat +pass 'pure result projection passes the exact core v2 stage relation' + +make_verified_receipt() { + local source_file=$1 output_file=$2 digest + digest=$(sha_file "$source_file") + "$jq_bin" -S -c -n --slurpfile receipt "$source_file" --arg sha "$digest" \ + '{content:$receipt[0],sha256:$sha}' > "$output_file" +} +expect_stage_result_reject() { + local name=$1 outcome=$2 raw_file=$3 pair_file=$4 + if "$jq_bin" -L "$modules" --arg command stage-result --arg outcome "$outcome" \ + --arg receipt_json "$(<"$raw_file")" \ + --arg verified_receipt_json "$(<"$pair_file")" \ + -f "$protocol" "$input" >/dev/null 2>&1; then + fail "$name" + fi + pass "$name" +} + +moved_receipt="$tmp/moved-receipt.json" +"$jq_bin" -S -c '.request_ref.sha256=("6"*64)' "$receipt" > "$moved_receipt" +expect_stage_result_reject receipt-changed-after-verification changed \ + "$moved_receipt" "$verified_receipt" + +mismatched_request_pair="$tmp/mismatched-request-pair.json" +make_verified_receipt "$moved_receipt" "$mismatched_request_pair" +expect_stage_result_reject receipt-request-mismatch changed \ + "$moved_receipt" "$mismatched_request_pair" + +nested_source_input="$tmp/nested-source-input.json" +"$jq_bin" -S -c ' + (.stage_request.content.body.inputs[] | + select(.input_id=="input.source-tree") | .value.value.value.location) = + {kind:"path",value:"nested"} | + .stage_request.content.body.source.value.location={kind:"path",value:"nested"} | + .stage_request.sha256=("9"*64) +' "$input" > "$nested_source_input" +nested_source_receipt="$tmp/nested-source-receipt.json" +"$jq_bin" -S -c '.request_ref.sha256=("9"*64)' \ + "$receipt" > "$nested_source_receipt" +nested_source_pair="$tmp/nested-source-pair.json" +make_verified_receipt "$nested_source_receipt" "$nested_source_pair" +if "$jq_bin" -L "$modules" --arg command stage-result --arg outcome changed \ + --arg receipt_json "$(<"$nested_source_receipt")" \ + --arg verified_receipt_json "$(<"$nested_source_pair")" \ + -f "$protocol" "$nested_source_input" >/dev/null 2>&1; then + fail receipt-nested-source-tree +fi +pass 'receipt source must be the repository root tree' + +mismatched_attempt="$tmp/mismatched-attempt-receipt.json" +"$jq_bin" -S -c '.attempt.attempt_number += 1' "$receipt" > "$mismatched_attempt" +mismatched_attempt_pair="$tmp/mismatched-attempt-pair.json" +make_verified_receipt "$mismatched_attempt" "$mismatched_attempt_pair" +expect_stage_result_reject receipt-attempt-mismatch changed \ + "$mismatched_attempt" "$mismatched_attempt_pair" + +malformed_digest_pair="$tmp/malformed-receipt-digest-pair.json" +"$jq_bin" -S -c '.sha256="invalid"' "$verified_receipt" > "$malformed_digest_pair" +expect_stage_result_reject receipt-digest-shape changed \ + "$receipt" "$malformed_digest_pair" +expect_stage_result_reject receipt-outcome-mismatch no-change \ + "$receipt" "$verified_receipt" + +unchanged_commit_receipt="$tmp/unchanged-commit-receipt.json" +"$jq_bin" -S -c '.candidate.commit_id=.source.commit_id' \ + "$receipt" > "$unchanged_commit_receipt" +unchanged_commit_pair="$tmp/unchanged-commit-pair.json" +make_verified_receipt "$unchanged_commit_receipt" "$unchanged_commit_pair" +expect_stage_result_reject changed-with-unchanged-commit changed \ + "$unchanged_commit_receipt" "$unchanged_commit_pair" + +unchanged_tree_receipt="$tmp/unchanged-tree-receipt.json" +"$jq_bin" -S -c '.candidate.tree_id=.source.tree_id' \ + "$receipt" > "$unchanged_tree_receipt" +unchanged_tree_pair="$tmp/unchanged-tree-pair.json" +make_verified_receipt "$unchanged_tree_receipt" "$unchanged_tree_pair" +expect_stage_result_reject changed-with-unchanged-tree changed \ + "$unchanged_tree_receipt" "$unchanged_tree_pair" + +no_change_receipt="$tmp/no-change-receipt.json" +"$jq_bin" -S -c -L "$modules" --arg command receipt \ + --arg source_repository_id fixture.target --arg source_hash_algorithm sha1 \ + --arg source_commit "$source_commit" --arg source_tree "$source_tree" \ + --arg candidate_commit "$source_commit" --arg candidate_tree "$source_tree" \ + --arg changed_path_count 0 --arg changed_paths_sha256 "$changed_paths_sha" \ + -f "$protocol" "$input" > "$no_change_receipt" +no_change_pair="$tmp/no-change-pair.json" +make_verified_receipt "$no_change_receipt" "$no_change_pair" +no_change_result="$tmp/no-change-result.json" +"$jq_bin" -S -c -L "$modules" --arg command stage-result --arg outcome no-change \ + --arg receipt_json "$(<"$no_change_receipt")" \ + --arg verified_receipt_json "$(<"$no_change_pair")" \ + -f "$protocol" "$input" > "$no_change_result" +"$core" validate-stage-run "$fixture/stage-request.json" "$fixture/resolved-profile.json" \ + "$no_change_result" || fail core-no-change-result +"$jq_bin" -e '.body.outcome.value=="no-change" and .body.outputs==[] and + .body.evidence[0].proof_ref.sha256 == .body.execution.metadata.tools.source_ref.sha256' \ + "$no_change_result" >/dev/null || fail no-change-result-surface +pass 'verified no-change receipt binds the no-change result' + +if "$jq_bin" -L "$modules" --arg command receipt --arg source_hash_algorithm sha1 \ + --arg source_commit INVALID --arg source_tree "$source_tree" \ + --arg candidate_commit "$source_commit" --arg candidate_tree "$source_tree" \ + --arg changed_path_count 1 --arg changed_paths_sha256 "$(printf '%064d' 0)" \ + --arg source_repository_id fixture.target -f "$protocol" "$input" \ + > "$tmp/bad-receipt.out" 2> "$tmp/bad-receipt.err"; then + fail malformed-receipt-identity +fi +pass 'malformed receipt identity rejected' + +if /usr/bin/grep -Eq 'curl|wget|gh |glab |github[.]com|gitlab[.]com|system[(]|@sh|getenv' \ + "$protocol" "$fixture_builder"; then + fail execution-surface +fi +pass 'protocol and fixture builder have no product execution or network seam' + +printf 'local Git materializer protocol: %s focused checks passed\n' "$passed" diff --git a/scripts/test/portable-core-schema.test.sh b/scripts/test/portable-core-schema.test.sh index 7c8241b..8a9928b 100755 --- a/scripts/test/portable-core-schema.test.sh +++ b/scripts/test/portable-core-schema.test.sh @@ -825,6 +825,7 @@ schema_import_path_ok() { local import_path="$1" local test_path case "$import_path" in + adapters/local-git-materializer/v1/protocol.jq|\ adapters/deterministic-verifier/v1/normalize.jq|\ orchestrator/v1/reconciliation-plan.jq|orchestrator/v1/state-scanner.jq) ;; scripts/test/default-codex-native-reviewer-adapter.test.sh|\