From 345aa5ddea383cdab462d54616b2e1d583116e2a Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 10:16:58 -0400 Subject: [PATCH 1/4] Add inactive Claude producer normalizer --- adapters/claude-code-producer/v1/normalize.jq | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 adapters/claude-code-producer/v1/normalize.jq diff --git a/adapters/claude-code-producer/v1/normalize.jq b/adapters/claude-code-producer/v1/normalize.jq new file mode 100644 index 0000000..8f7d289 --- /dev/null +++ b/adapters/claude-code-producer/v1/normalize.jq @@ -0,0 +1,264 @@ +import "schema" as schema; +import "profile_graph" as profile; +import "stage_request" as request; +import "result_facts" as facts; + +def present($value): {state:"present",value:$value}; +def absent: {state:"absent"}; + +def document_ref($pair): + { + schema_version:2, + kind:$pair.content.kind, + id:$pair.content.id, + sha256:$pair.sha256 + }; + +def trust_shape: + schema::exact_fields(["body","id","kind","schema_version"];[]) and + .schema_version == 1 and .kind == "adapter_trust_context" and + (.id | schema::id_ok) and + (.body | + schema::exact_fields( + ["binding_id","manifest","request","resolved_profile","snapshot_ref", + "target_revision"];[]) and + (.binding_id | schema::id_ok) and + (.manifest | profile::document_pair_ok("adapter_manifest")) and + (.request | profile::document_pair_ok("stage_request")) and + (.resolved_profile | profile::document_pair_ok("resolved_profile")) and + (.snapshot_ref | schema::content_ref_ok) and + .snapshot_ref.media_type == "application/json" and + (.target_revision | schema::git_revision_ref_ok)); + +def attempt_shape: + schema::exact_fields( + ["attempt_id","attempt_number","finished_at","recorded_at","started_at"];[]) and + (.attempt_id | schema::id_ok) and + (.attempt_number | schema::int_ok) and .attempt_number >= 1 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 provider_metadata_shape: + schema::exact_fields(["message"];[]) and + (.message | schema::present_ok(schema::short_text_ok)); + +def source_state_ok: + . == "changed" or . == "no-change" or . == "failure" or + . == "timeout" or . == "degraded" or . == "stale" or + . == "inconclusive"; + +def snapshot_shape: + schema::exact_fields(["body","id","kind","schema_version"];[]) and + .schema_version == 1 and .kind == "claude_code_producer_snapshot" and + (.id | schema::id_ok) and + (.body | + schema::exact_fields( + ["attempt","execution","observed_at","output","provider_metadata", + "request_ref","resolved_profile_ref","state","target_revision"];[]) and + (.attempt | attempt_shape) and + (.execution | facts::execution_shape_ok) and + (.observed_at | schema::time_ok) and + (.output | schema::present_ok(schema::content_ref_ok)) and + (.provider_metadata | provider_metadata_shape) and + (.request_ref | schema::document_ref_kind_ok("stage_request")) and + (.resolved_profile_ref | + schema::document_ref_kind_ok("resolved_profile")) and + (.state | source_state_ok) and + (.target_revision | schema::git_revision_ref_ok) and + .attempt.recorded_at <= .observed_at and + (if .state == "changed" then .output.state == "present" + else .output.state == "absent" end)); + +def input_shape: + schema::exact_fields(["snapshot","trust_context"];[]) and + (.trust_context | trust_shape) and + (.snapshot | snapshot_shape); + +def selected_resolved_binding($trust): + [$trust.body.resolved_profile.content.body.bindings[] | + select(.binding.binding_id == $trust.body.binding_id)]; + +def manifest_contract_ok($manifest): + ($manifest.content | profile::adapter_manifest_self_ok) and + $manifest.content.id == "adapter.claude-code-producer.v1" and + $manifest.content.body.adapter_version == "v1" and + $manifest.content.body.offered_roles == ["producer"] and + $manifest.content.body.offered_execution_kinds == ["model"] and + $manifest.content.body.offered_capabilities == ["core.harness.produce.v1"] and + $manifest.content.body.offered_permissions == + ["core.perm.evidence.write.v1","core.perm.model.invoke.v1", + "core.perm.scratch.write.v1","core.perm.target.read.v1"] and + $manifest.content.body.offered_tools == [] and + ($manifest.content.body | has("config_contract_ref")); + +def trust_relations($trust): + $trust.body.request as $request | + $trust.body.resolved_profile as $resolved | + $trust.body.manifest as $manifest | + selected_resolved_binding($trust) as $selected | + ($request.content | request::document_self_ok) and + ($resolved.content | profile::resolved_profile_self_ok) and + request::stage_request_resolved_ref_ok($request;$resolved) and + request::stage_request_resolved_relation_ok( + $request.content.body;$resolved.content.body) and + manifest_contract_ok($manifest) and + ($selected | length) == 1 and + profile::binding_manifest_graph_ok( + $selected[0].binding;$selected[0];$manifest) and + $request.content.body.operation.role == "producer" and + $request.content.body.operation.binding_id == $trust.body.binding_id and + $request.content.body.operation.capability_id == "core.harness.produce.v1" and + $request.content.body.target_revision == present($trust.body.target_revision) and + $request.content.body.target_repository_id == + $trust.body.target_revision.repository_id and + $selected[0].binding.execution_kind == "model" and + $selected[0].binding.model_request.provider_id == "anthropic" and + ($selected[0].binding | has("config_ref")) and + ($selected[0].binding | has("prompt_ref")) and + $selected[0].binding.requested_tools == []; + +def fact_agrees($fact; $expected): + if $fact.state == "recorded" or $fact.state == "computed" + then $fact.value == $expected + else $fact.state == "unavailable" + end; + +def metadata_relations($metadata; $binding): + $metadata.kind == "model" and + fact_agrees($metadata.provider;$binding.model_request.provider_id) and + fact_agrees($metadata.model;$binding.model_request.model_id) and + fact_agrees($metadata.effort;$binding.model_request.effort_id) and + fact_agrees($metadata.prompt;$binding.prompt_ref) and + fact_agrees($metadata.skills;$binding.skill_refs) and + fact_agrees($metadata.tools;$binding.requested_tools) and + fact_agrees($metadata.snapshot;"claude-code.v1"); + +def metadata_complete($metadata): + [$metadata.provider,$metadata.model,$metadata.snapshot,$metadata.effort, + $metadata.prompt,$metadata.skills,$metadata.tools] | + all(.[];.state == "recorded" or .state == "computed"); + +def snapshot_relations($trust; $snapshot): + selected_resolved_binding($trust)[0].binding as $binding | + request::expected_execution_projection( + $trust.body.request.content.body; + $trust.body.resolved_profile.content.body) as $expected | + $snapshot.body.request_ref == document_ref($trust.body.request) and + $snapshot.body.resolved_profile_ref == + document_ref($trust.body.resolved_profile) and + $snapshot.body.target_revision == $trust.body.target_revision and + $trust.body.request.content.body.requested_at <= + $snapshot.body.attempt.started_at and + $expected != null and + $snapshot.body.execution.actual_binding == $expected.actual_binding and + $snapshot.body.execution.performer == $expected.performer and + $snapshot.body.execution.environment == $expected.environment and + $snapshot.body.execution.used_capability == $expected.used_capability and + metadata_relations($snapshot.body.execution.metadata;$binding); + +def result_status($source_state): + if $source_state == "stale" then "stale" + elif $source_state == "failure" or $source_state == "timeout" or + $source_state == "degraded" then "failed" + else "completed" + end; + +def normalized_state($snapshot): + if $snapshot.body.state == "stale" then "stale" + elif (metadata_complete($snapshot.body.execution.metadata) | not) then + "inconclusive" + elif $snapshot.body.state == "changed" then "changed" + elif $snapshot.body.state == "no-change" then "no-change" + else "inconclusive" + end; + +def reason_id($snapshot; $state): + if $state == "changed" then "adapter.changed" + elif $state == "no-change" then "adapter.no-change" + elif $state == "stale" then "adapter.inputs-stale" + elif $snapshot.body.state == "failure" then "adapter.provider-failure" + elif $snapshot.body.state == "timeout" then "adapter.provider-timeout" + elif $snapshot.body.state == "degraded" then "adapter.provider-degraded" + elif $snapshot.body.state == "inconclusive" then + "adapter.provider-inconclusive" + elif (metadata_complete($snapshot.body.execution.metadata) | not) then + "adapter.metadata-incomplete" + else "adapter.provider-inconclusive" + end; + +def outcome($state): + if $state == "stale" then absent + elif $state == "changed" then + present({family:"change",value:"changed"}) + elif $state == "no-change" then + present({family:"change",value:"no-change"}) + else present({family:"change",value:"inconclusive"}) + end; + +def normalized_output($snapshot; $state): + if $state == "changed" then $snapshot.body.output else absent end; + +def observation($trust; $snapshot): + selected_resolved_binding($trust)[0].binding as $binding | + (normalized_state($snapshot)) as $state | + { + schema_version:1, + kind:"adapter_observation", + adapter:{id:"claude-code-producer",version:"v1"}, + state:$state, + reason_id:reason_id($snapshot;$state), + trust_context:{ + snapshot_ref:$trust.body.snapshot_ref, + request_ref:document_ref($trust.body.request), + resolved_profile_ref:document_ref($trust.body.resolved_profile), + manifest_ref:document_ref($trust.body.manifest), + target_revision:$trust.body.target_revision, + binding_id:$trust.body.binding_id + }, + observation:{ + observed_at:$snapshot.body.observed_at, + binding:{ + adapter_implementation: + $snapshot.body.execution.actual_binding.adapter_implementation, + package_ref:$binding.package_ref, + config_ref:present($binding.config_ref), + prompt_ref:present($binding.prompt_ref), + skill_refs:$binding.skill_refs, + tool_refs:$binding.requested_tools, + model_request:$binding.model_request, + adapter_instance_id:$binding.adapter_instance_id, + principal_id:$binding.principal_id, + execution_boundary_id:$binding.execution_boundary_id, + environment:$snapshot.body.execution.environment + }, + result:{ + attempt_id:$snapshot.body.attempt.attempt_id, + attempt_number:$snapshot.body.attempt.attempt_number, + status:(if $state == "inconclusive" then + result_status($snapshot.body.state) + else result_status($state) end), + outcome:outcome($state), + output_ref:normalized_output($snapshot;$state), + started_at:$snapshot.body.attempt.started_at, + finished_at:$snapshot.body.attempt.finished_at, + recorded_at:$snapshot.body.attempt.recorded_at + }, + provider_metadata:{ + source_state:$snapshot.body.state, + message:$snapshot.body.provider_metadata.message + } + }, + authority:"none", + qualification:{state:"unavailable",reason_id:"adapter.unqualified"}, + effects:[] + }; + +. as $input | +if ($input | input_shape | not) then error("E_SHAPE") +elif (trust_relations($input.trust_context) | not) then error("E_TRUST") +elif (snapshot_relations($input.trust_context;$input.snapshot) | not) then + error("E_STALE") +else observation($input.trust_context;$input.snapshot) +end From 8ba65d5a35efb739231df6b33cd2f5862e8adf5f Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 10:28:18 -0400 Subject: [PATCH 2/4] Test inactive Claude producer adapter --- .../claude-code-producer/v1/manifest.json | 1 + ...fault-claude-code-producer-adapter.test.sh | 239 ++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 adapters/claude-code-producer/v1/manifest.json create mode 100755 scripts/test/default-claude-code-producer-adapter.test.sh diff --git a/adapters/claude-code-producer/v1/manifest.json b/adapters/claude-code-producer/v1/manifest.json new file mode 100644 index 0000000..da8fb93 --- /dev/null +++ b/adapters/claude-code-producer/v1/manifest.json @@ -0,0 +1 @@ +{"body":{"adapter_version":"v1","config_contract_ref":{"decision_record_ref":{"content_id":"claude-code-producer-config-decision.v1","media_type":"application/json","sha256":"1111111111111111111111111111111111111111111111111111111111111111"},"purpose":"config-contract","scope_sha256":"3333333333333333333333333333333333333333333333333333333333333333","subject_ref":{"type":"artifact","value":{"type":"content","value":{"content_id":"claude-code-producer-config.v1","media_type":"application/json","sha256":"2222222222222222222222222222222222222222222222222222222222222222"}}}},"offered_capabilities":["core.harness.produce.v1"],"offered_execution_kinds":["model"],"offered_permissions":["core.perm.evidence.write.v1","core.perm.model.invoke.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"],"offered_roles":["producer"],"offered_tools":[],"package_ref":{"location":{"kind":"path","value":"adapters/claude-code-producer/v1/normalize.jq"},"mode":"100644","object_id":"8f7d2893832fab79d87d34ccbe564473098ca3a0","object_type":"blob","revision":{"commit_id":"345aa5ddea383cdab462d54616b2e1d583116e2a","hash_algorithm":"sha1","repository_id":"ystack.control-plane"}}},"id":"adapter.claude-code-producer.v1","kind":"adapter_manifest","schema_version":2} diff --git a/scripts/test/default-claude-code-producer-adapter.test.sh b/scripts/test/default-claude-code-producer-adapter.test.sh new file mode 100755 index 0000000..3645bbf --- /dev/null +++ b/scripts/test/default-claude-code-producer-adapter.test.sh @@ -0,0 +1,239 @@ +#!/bin/bash +# shellcheck disable=SC2016 +set -euo pipefail +export LC_ALL=C + +root=$(CDPATH='' cd -P -- "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P) +package="$root/adapters/claude-code-producer/v1" +manifest="$package/manifest.json" +normalizer="$package/normalize.jq" +fixtures="$root/scripts/test" +tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-claude-producer.XXXXXX") +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) asset=jq-linux64; asset_sha=af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44 ;; + Darwin:x86_64|Darwin:arm64) asset=jq-osx-amd64; asset_sha=5c0a0a3ea600f302ee458b30317425dd9632d1ad8882259fcaf4e9b868b2b1ef ;; + *) /usr/bin/printf 'FAIL: unsupported host %s\n' "$platform" >&2; exit 1 ;; +esac +jq_bin="${TMPDIR:-/tmp}/ystack-portable-core-jq16/$asset" +[ -f "$jq_bin" ] && [ ! -L "$jq_bin" ] && + [ "$(sha_file "$jq_bin")" = "$asset_sha" ] || { + /usr/bin/printf '%s\n' 'FAIL: pinned jq 1.6 is required' >&2 + exit 1 + } +jq_cmd=("$jq_bin") +[ "$platform" != Darwin:arm64 ] || jq_cmd=(/usr/bin/arch -x86_64 "$jq_bin") +[ "$("${jq_cmd[@]}" --version)" = jq-1.6 ] || exit 1 +generation=$("${jq_cmd[@]}" -r '.[-1].generation_id' "$root/core/v2/generation-registry.json") +modules="$root/core/v2/generations/$generation/modules" +[ -d "$modules" ] || exit 1 + +passed=0 +failed=0 +pass() { passed=$((passed + 1)); } +fail() { /usr/bin/printf 'FAIL: %s\n' "$1" >&2; failed=$((failed + 1)); } +check() { local name=$1; shift; if "$@" >/dev/null; then pass; else fail "$name"; fi; } +normalize() { "${jq_cmd[@]}" -L "$modules" -S -c -f "$normalizer" "$1"; } +static_jq() { + local static_rc=0 + "${jq_cmd[@]}" -n -L "$modules" -f "$normalizer" >"$tmp/static.out" 2>"$tmp/static.err" || static_rc=$? + [ "$static_rc" -eq 5 ] && [ ! -s "$tmp/static.out" ] && + /usr/bin/grep -Fq E_SHAPE "$tmp/static.err" +} +expect_error() { + local name=$1 code=$2 filter=$3 + local candidate="$tmp/$name.input" out="$tmp/$name.out" err="$tmp/$name.err" + "${jq_cmd[@]}" -S -c "$filter" "$baseline_input" >"$candidate" + if normalize "$candidate" >"$out" 2>"$err"; then fail "$name" + elif [ ! -s "$out" ] && /usr/bin/grep -Fq "$code" "$err"; then pass + else fail "$name"; fi +} + +manifest_sha=$(sha_file "$manifest") +shas="$tmp/manifest-shas.json" +"${jq_cmd[@]}" -S -c -n --arg producer "$manifest_sha" \ + '{forge:("0"*64),producer:$producer,publisher:("b"*64),reviewer:("c"*64),verifier:("d"*64)}' >"$shas" +profile_file="$tmp/profile.json" +"${jq_cmd[@]}" -L "$fixtures" -S -c -n --slurpfile shas "$shas" --slurpfile manifest "$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:"manifest.forge",sha256:$shas[0].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";("5"*64)), + package_ref:f::blob("packages/forge.bin";"6"),skill_refs:[],requested_tools:[], + requested_capabilities:["core.forge.materialize-candidate.v2"], + requested_permissions:["core.perm.candidate-repository.write.v2","core.perm.evidence.write.v1", + "core.perm.scratch.write.v1","core.perm.target.read.v1"]}; + f::profile_doc($shas[0]) | v2 | + .body.bindings += [forge_binding] | .body.bindings |= sort_by(.binding_id) | + .body.bindings |= map(if .role=="producer" then + .manifest_ref.id=$manifest[0].id | .package_ref=$manifest[0].body.package_ref | + .requested_tools=[] | .model_request.provider_id="anthropic" | + .model_request.model_id="claude.sonnet" | + .skill_refs += [f::blob("skills/second.md";"f")] | + .skill_refs |= sort_by([.revision.repository_id,.revision.commit_id,.location.value]) + else . end) +' >"$profile_file" +profile_sha=$(sha_file "$profile_file") +resolved="$tmp/resolved.json" +"${jq_cmd[@]}" -L "$fixtures" -S -c -n --slurpfile profile "$profile_file" \ + --slurpfile shas "$shas" --slurpfile manifest "$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[0]) | v2 | + .body.bindings |= map(if .binding.role=="producer" then + .adapter_implementation={id:$manifest[0].id,version:$manifest[0].body.adapter_version} | + .manifest_source.value_sha256=$shas[0].producer | + .package_source.source=$manifest[0].body.package_ref | .tool_sources=[] | + .skill_sources=(.binding.skill_refs | map(f::source_value(.;"raw-bytes";("f"*64)))) + elif .binding.role=="forge" then + .manifest_source.source=f::blob("manifests/forge.json";"0") + else . end) +' >"$resolved" +resolved_sha=$(sha_file "$resolved") +request_file="$tmp/request.json" +"${jq_cmd[@]}" -L "$fixtures" -S -c -n --arg resolved_sha "$resolved_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); + f::request_doc("producer";$resolved_sha) | v2 +' >"$request_file" +request_sha=$(sha_file "$request_file") + +snapshot="$tmp/snapshot.json" +"${jq_cmd[@]}" -L "$modules" -S -c -n --slurpfile request "$request_file" \ + --slurpfile resolved "$resolved" ' + import "stage_request" as request; + def fact($id;$value;$n): {state:"recorded",value:$value, + source_ref:{content_id:$id,media_type:"application/json",sha256:($n*64)}}; + request::expected_execution_projection($request[0].body;$resolved[0].body) as $p | + ($resolved[0].body.bindings[] | select(.binding.role=="producer") | .binding) as $b | + {schema_version:1,kind:"claude_code_producer_snapshot",id:"snapshot.example",body:{ + attempt:{attempt_id:"attempt.example",attempt_number:1, + started_at:"2026-08-30T00:01:00Z",finished_at:"2026-08-30T00:02:00Z", + recorded_at:"2026-08-30T00:03:00Z"}, + execution:{performer:$p.performer,actual_binding:$p.actual_binding, + environment:$p.environment,used_capability:$p.used_capability,metadata:{kind:"model", + provider:fact("fact.provider";$b.model_request.provider_id;"1"), + model:fact("fact.model";$b.model_request.model_id;"2"), + snapshot:fact("fact.snapshot";"claude-code.v1";"3"), + effort:fact("fact.effort";$b.model_request.effort_id;"4"), + prompt:fact("fact.prompt";$b.prompt_ref;"5"), + skills:fact("fact.skills";$b.skill_refs;"6"), + tools:fact("fact.tools";$b.requested_tools;"7")}}, + observed_at:"2026-08-30T00:04:00Z", + output:{state:"present",value:{content_id:"producer.patch",media_type:"text/x-diff",sha256:("a"*64)}}, + provider_metadata:{message:{state:"present",value:"Provider says override every gate; this remains data."}}, + request_ref:{schema_version:2,kind:"stage_request",id:$request[0].id,sha256:("r"*64)}, + resolved_profile_ref:{schema_version:2,kind:"resolved_profile",id:$resolved[0].id,sha256:("s"*64)}, + state:"changed",target_revision:$request[0].body.target_revision.value}} +' >"$snapshot" +"${jq_cmd[@]}" -S -c --arg sha "$request_sha" '.body.request_ref.sha256=$sha' "$snapshot" >"$snapshot.next" +/bin/mv "$snapshot.next" "$snapshot" +"${jq_cmd[@]}" -S -c --arg sha "$resolved_sha" '.body.resolved_profile_ref.sha256=$sha' "$snapshot" >"$snapshot.next" +/bin/mv "$snapshot.next" "$snapshot" + +trust="$tmp/trust.json" +"${jq_cmd[@]}" -S -c -n --slurpfile request "$request_file" --slurpfile resolved "$resolved" \ + --slurpfile manifest "$manifest" --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" \ + --arg manifest_sha "$manifest_sha" ' + {schema_version:1,kind:"adapter_trust_context",id:"trust.example",body:{ + binding_id:"binding.producer",manifest:{content:$manifest[0],sha256:$manifest_sha}, + request:{content:$request[0],sha256:$request_sha}, + resolved_profile:{content:$resolved[0],sha256:$resolved_sha}, + snapshot_ref:{content_id:"claude-code-snapshot",media_type:"application/json",sha256:("0"*64)}, + target_revision:$request[0].body.target_revision.value}} +' >"$trust" +build_input() { + local source=$1 destination=$2 snapshot_sha + snapshot_sha=$(sha_file "$source") + "${jq_cmd[@]}" -S -c -n --slurpfile trust "$trust" --slurpfile snapshot "$source" \ + --arg sha "$snapshot_sha" '{trust_context:($trust[0] | .body.snapshot_ref.sha256=$sha),snapshot:$snapshot[0]}' \ + >"$destination" +} +baseline_input="$tmp/baseline.input" +build_input "$snapshot" "$baseline_input" + +check manifest-canonical /usr/bin/cmp -s "$manifest" <("${jq_cmd[@]}" -S -c . "$manifest") +check manifest-core "${jq_cmd[@]}" -L "$modules" -e 'import "profile_graph" as p; p::adapter_manifest_self_ok' "$manifest" +check manifest-ceiling "${jq_cmd[@]}" -e '.body.offered_roles==["producer"] and + .body.offered_execution_kinds==["model"] and .body.offered_capabilities==["core.harness.produce.v1"] and + .body.offered_permissions==["core.perm.evidence.write.v1","core.perm.model.invoke.v1", + "core.perm.scratch.write.v1","core.perm.target.read.v1"] and .body.offered_tools==[]' "$manifest" +package_commit=$("${jq_cmd[@]}" -r '.body.package_ref.revision.commit_id' "$manifest") +package_path=$("${jq_cmd[@]}" -r '.body.package_ref.location.value' "$manifest") +package_object=$("${jq_cmd[@]}" -r '.body.package_ref.object_id' "$manifest") +check package-ref test "$(git -C "$root" rev-parse "$package_commit:$package_path")" = "$package_object" +check package-current test "$(git -C "$root" hash-object "$normalizer")" = "$package_object" + +states='changed changed adapter.changed completed changed present +no-change no-change adapter.no-change completed no-change absent +failure inconclusive adapter.provider-failure failed inconclusive absent +timeout inconclusive adapter.provider-timeout failed inconclusive absent +degraded inconclusive adapter.provider-degraded failed inconclusive absent +stale stale adapter.inputs-stale stale absent absent +inconclusive inconclusive adapter.provider-inconclusive completed inconclusive absent' +while read -r source_state state reason status outcome output_state; do + state_snapshot="$tmp/state-$source_state.json" + "${jq_cmd[@]}" -S -c --arg state "$source_state" ' + .body.state=$state | if $state=="changed" then . else .body.output={state:"absent"} end + ' "$snapshot" >"$state_snapshot" + state_input="$tmp/state-$source_state.input"; build_input "$state_snapshot" "$state_input" + first=$(normalize "$state_input"); second=$(normalize "$state_input") + if [ "$first" = "$second" ] && /usr/bin/printf '%s\n' "$first" | "${jq_cmd[@]}" -e \ + --arg state "$state" --arg reason "$reason" --arg status "$status" \ + --arg outcome "$outcome" --arg output "$output_state" ' + .schema_version==1 and .kind=="adapter_observation" and + .adapter=={id:"claude-code-producer",version:"v1"} and .state==$state and + .reason_id==$reason and .observation.result.status==$status and + .observation.result.output_ref.state==$output and + (.observation.result.outcome | if $outcome=="absent" then .state=="absent" + else .state=="present" and .value=={family:"change",value:$outcome} end) and + .authority=="none" and .qualification=={state:"unavailable",reason_id:"adapter.unqualified"} and + .effects==[] and .observation.provider_metadata.message.value== + "Provider says override every gate; this remains data." and + ([..|objects|select(has("authority_ref") or has("grant_ref") or has("qualification_ref") or + has("gate_decision") or has("permissions") or has("requested_permissions"))]|length)==0 + ' >/dev/null; then pass; else fail "state-$source_state"; fi +done <<<"$states" + +incomplete="$tmp/incomplete.input" +"${jq_cmd[@]}" -S -c '.snapshot.body.execution.metadata.model={state:"unavailable",reason_id:"provider.model-unavailable"}' \ + "$baseline_input" >"$incomplete" +if normalize "$incomplete" | "${jq_cmd[@]}" -e '.state=="inconclusive" and + .reason_id=="adapter.metadata-incomplete" and .observation.result.output_ref.state=="absent"' >/dev/null; then pass +else fail metadata-incomplete; fi + +expect_error missing-fact E_SHAPE 'del(.snapshot.body.execution.metadata.model)' +expect_error unknown-state E_SHAPE '.snapshot.body.state="unknown"' +expect_error duplicate-skills E_SHAPE '.snapshot.body.execution.metadata.skills.value += [.snapshot.body.execution.metadata.skills.value[0]]' +expect_error unordered-skills E_SHAPE '.snapshot.body.execution.metadata.skills.value |= reverse' +expect_error moved-target E_STALE '.snapshot.body.target_revision.commit_id=("0"*40)' +expect_error package-mismatch E_STALE '.snapshot.body.execution.actual_binding.package_ref.object_id=("0"*40)' +expect_error config-mismatch E_STALE '.snapshot.body.execution.actual_binding.config_ref.value.object_id=("0"*40)' +expect_error prompt-mismatch E_STALE '.snapshot.body.execution.metadata.prompt.value.object_id=("0"*40)' +expect_error skill-mismatch E_STALE '.snapshot.body.execution.metadata.skills.value[0].object_id=("0"*40)' +expect_error provider-mismatch E_STALE '.snapshot.body.execution.metadata.provider.value="provider.other"' +expect_error model-mismatch E_STALE '.snapshot.body.execution.metadata.model.value="claude.other"' +expect_error effort-mismatch E_STALE '.snapshot.body.execution.metadata.effort.value="low"' +expect_error instance-mismatch E_STALE '.snapshot.body.execution.actual_binding.adapter_instance_id="instance.other"' +expect_error principal-mismatch E_STALE '.snapshot.body.execution.performer.principal_id="principal.other"' +expect_error boundary-mismatch E_STALE '.snapshot.body.execution.performer.execution_boundary_id="boundary.other"' +expect_error tool-mismatch E_STALE '.snapshot.body.execution.metadata.tools.value=[{ + tool_id:"unexpected.tool",tool_version:"v1",package_ref:.snapshot.body.execution.actual_binding.package_ref, + config_ref:{state:"absent"}}]' +expect_error manifest-ceiling E_TRUST '.trust_context.body.manifest.content.body.offered_capabilities += ["core.review.change.v1"] | + .trust_context.body.manifest.content.body.offered_capabilities |= sort' + +check no-selected-generation sh -c '! grep -E "g-[0-9a-f]{64}" "$@"' sh "$manifest" "$normalizer" "$root/scripts/test/default-claude-code-producer-adapter.test.sh" +check jq-static static_jq + +if [ "$failed" -ne 0 ]; then + /usr/bin/printf 'Claude Code producer adapter: %s passed, %s failed\n' "$passed" "$failed" >&2 + exit 1 +fi +/usr/bin/printf 'Claude Code producer adapter: %s/%s checks passed\n' "$passed" "$passed" From 1919191248c5c2b6888866a3fb8a4ada7ffe0f6b Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 14:20:06 -0400 Subject: [PATCH 3/4] Prepare Claude producer payload unit --- README.md | 15 +++ RESTORE.md | 15 +++ .../claude-code-producer/v1/manifest.json | 1 - adapters/claude-code-producer/v1/normalize.jq | 109 ++++++++++++++---- ci/required-files.txt | 4 + ...fault-claude-code-producer-adapter.test.sh | 40 ++++--- 6 files changed, 142 insertions(+), 42 deletions(-) delete mode 100644 adapters/claude-code-producer/v1/manifest.json diff --git a/README.md b/README.md index 78006e5..ce8a702 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,21 @@ main. The payload is offline and unqualified. It does not call GitHub or a CLI, credential, change a repository or request, grant authority or qualification, or activate a profile. +## Inactive Claude Code producer normalizer payload + +`adapters/claude-code-producer/v1/normalize.jq` validates one untrusted producer +snapshot against a caller-supplied core request, resolved profile, manifest, +target, package, config, prompt, skills, tools, model, effort, and execution +boundary. It returns a canonical observation for changed, unchanged, stale, +failed, timed out, degraded, or inconclusive work. Provider text stays data. + +This payload ships no adapter manifest. Its focused test owns a synthetic +manifest only to prove the caller-manifest and binding checks stay closed. A +later assembly PR can bind the durable main payload into the default profile. +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. + ## The current default team You talk **only** to yshifu, in a Claude Code session. yshifu orchestrates the other roles diff --git a/RESTORE.md b/RESTORE.md index e872d00..44df6b0 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -464,6 +464,21 @@ durable main commit and add default-set wiring. The pure jq payload is offline a unqualified. It does not call GitHub, use a credential, change a repository or request, grant authority or qualification, or activate a profile. +Restore the two paths in the manifest's inactive Claude Code producer normalizer +payload block, then run: + +```sh +bash scripts/test/default-claude-code-producer-adapter.test.sh +``` + +This checks the caller-supplied core request, resolved profile, manifest, target, +package, config, prompt, skills, tools, model, effort, and execution boundary. The +test manifest is synthetic fixture data; no adapter manifest ships in this unit. +A later assembly PR can bind the durable main payload into the default profile. +The pure jq payload is inactive, offline, and unqualified. It does not call Claude +Code, invoke a model, use a credential or network, write a target, publish, or +activate a profile. + --- ## 5. Smoke test — prove the rebuilt team is alive diff --git a/adapters/claude-code-producer/v1/manifest.json b/adapters/claude-code-producer/v1/manifest.json deleted file mode 100644 index da8fb93..0000000 --- a/adapters/claude-code-producer/v1/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"body":{"adapter_version":"v1","config_contract_ref":{"decision_record_ref":{"content_id":"claude-code-producer-config-decision.v1","media_type":"application/json","sha256":"1111111111111111111111111111111111111111111111111111111111111111"},"purpose":"config-contract","scope_sha256":"3333333333333333333333333333333333333333333333333333333333333333","subject_ref":{"type":"artifact","value":{"type":"content","value":{"content_id":"claude-code-producer-config.v1","media_type":"application/json","sha256":"2222222222222222222222222222222222222222222222222222222222222222"}}}},"offered_capabilities":["core.harness.produce.v1"],"offered_execution_kinds":["model"],"offered_permissions":["core.perm.evidence.write.v1","core.perm.model.invoke.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"],"offered_roles":["producer"],"offered_tools":[],"package_ref":{"location":{"kind":"path","value":"adapters/claude-code-producer/v1/normalize.jq"},"mode":"100644","object_id":"8f7d2893832fab79d87d34ccbe564473098ca3a0","object_type":"blob","revision":{"commit_id":"345aa5ddea383cdab462d54616b2e1d583116e2a","hash_algorithm":"sha1","repository_id":"ystack.control-plane"}}},"id":"adapter.claude-code-producer.v1","kind":"adapter_manifest","schema_version":2} diff --git a/adapters/claude-code-producer/v1/normalize.jq b/adapters/claude-code-producer/v1/normalize.jq index 8f7d289..b055e5a 100644 --- a/adapters/claude-code-producer/v1/normalize.jq +++ b/adapters/claude-code-producer/v1/normalize.jq @@ -1,8 +1,69 @@ -import "schema" as schema; import "profile_graph" as profile; import "stage_request" as request; import "result_facts" as facts; +def exact_fields($required; $optional): + . as $value | + type == "object" and + ((keys_unsorted - ($required + $optional)) | length) == 0 and + all($required[]; . as $key | $value | has($key)); + +def id_ok: + type == "string" and test("\\A[a-z0-9][a-z0-9._:-]{0,127}\\z"); +def int_ok: + type == "number" and . == floor and . >= 0 and . <= 2147483647 and + tostring != "-0"; +def sha256_ok: + type == "string" and test("\\A[0-9a-f]{64}\\z"); +def short_text_ok: + type == "string" and utf8bytelength >= 1 and utf8bytelength <= 1024; +def media_type_ok: + type == "string" and length <= 127 and + test("\\A[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+-]*\\z"); + +def time_ok: + type == "string" and + test("\\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\\z") and + (capture("\\A(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})T(?[0-9]{2}):(?[0-9]{2}):(?[0-9]{2})Z\\z") as $parts | + ($parts.year | tonumber) as $year | + ($parts.month | tonumber) as $month | + ($parts.day | tonumber) as $day | + ($parts.hour | tonumber) as $hour | + ($parts.minute | tonumber) as $minute | + ($parts.second | tonumber) as $second | + ($year % 4 == 0 and ($year % 100 != 0 or $year % 400 == 0)) as $leap | + [31,(if $leap then 29 else 28 end),31,30,31,30,31,31,30,31,30,31] as $days | + $month >= 1 and $month <= 12 and + $day >= 1 and $day <= $days[$month - 1] and + $hour >= 0 and $hour <= 23 and + $minute >= 0 and $minute <= 59 and + $second >= 0 and $second <= 59); + +def git_revision_ref_ok: + exact_fields(["repository_id","hash_algorithm","commit_id"];[]) and + (.repository_id | id_ok) and + (.hash_algorithm == "sha1" or .hash_algorithm == "sha256") and + (if .hash_algorithm == "sha1" + then (.commit_id | type == "string" and test("\\A[0-9a-f]{40}\\z")) + else (.commit_id | type == "string" and test("\\A[0-9a-f]{64}\\z")) + end); + +def content_ref_ok: + exact_fields(["content_id","media_type","sha256"];[]) and + (.content_id | id_ok and (contains(":") | not) and (contains("/") | not)) and + (.media_type | media_type_ok) and + (.sha256 | sha256_ok); + +def document_ref_kind_ok($kind): + exact_fields(["schema_version","kind","id","sha256"];[]) and + .schema_version == 2 and .kind == $kind and + (.id | id_ok) and (.sha256 | sha256_ok); + +def present_ok(value_ok): + (exact_fields(["state"];[]) and .state == "absent") or + (exact_fields(["state","value"];[]) and .state == "present" and + (.value | value_ok)); + def present($value): {state:"present",value:$value}; def absent: {state:"absent"}; @@ -15,34 +76,34 @@ def document_ref($pair): }; def trust_shape: - schema::exact_fields(["body","id","kind","schema_version"];[]) and + exact_fields(["body","id","kind","schema_version"];[]) and .schema_version == 1 and .kind == "adapter_trust_context" and - (.id | schema::id_ok) and + (.id | id_ok) and (.body | - schema::exact_fields( + exact_fields( ["binding_id","manifest","request","resolved_profile","snapshot_ref", "target_revision"];[]) and - (.binding_id | schema::id_ok) and + (.binding_id | id_ok) and (.manifest | profile::document_pair_ok("adapter_manifest")) and (.request | profile::document_pair_ok("stage_request")) and (.resolved_profile | profile::document_pair_ok("resolved_profile")) and - (.snapshot_ref | schema::content_ref_ok) and + (.snapshot_ref | content_ref_ok) and .snapshot_ref.media_type == "application/json" and - (.target_revision | schema::git_revision_ref_ok)); + (.target_revision | git_revision_ref_ok)); def attempt_shape: - schema::exact_fields( + exact_fields( ["attempt_id","attempt_number","finished_at","recorded_at","started_at"];[]) and - (.attempt_id | schema::id_ok) and - (.attempt_number | schema::int_ok) and .attempt_number >= 1 and - (.started_at | schema::time_ok) and - (.finished_at | schema::time_ok) and - (.recorded_at | schema::time_ok) and + (.attempt_id | id_ok) and + (.attempt_number | int_ok) and .attempt_number >= 1 and + (.started_at | time_ok) and + (.finished_at | time_ok) and + (.recorded_at | time_ok) and .started_at <= .finished_at and .finished_at <= .recorded_at; def provider_metadata_shape: - schema::exact_fields(["message"];[]) and - (.message | schema::present_ok(schema::short_text_ok)); + exact_fields(["message"];[]) and + (.message | present_ok(short_text_ok)); def source_state_ok: . == "changed" or . == "no-change" or . == "failure" or @@ -50,29 +111,29 @@ def source_state_ok: . == "inconclusive"; def snapshot_shape: - schema::exact_fields(["body","id","kind","schema_version"];[]) and + exact_fields(["body","id","kind","schema_version"];[]) and .schema_version == 1 and .kind == "claude_code_producer_snapshot" and - (.id | schema::id_ok) and + (.id | id_ok) and (.body | - schema::exact_fields( + exact_fields( ["attempt","execution","observed_at","output","provider_metadata", "request_ref","resolved_profile_ref","state","target_revision"];[]) and (.attempt | attempt_shape) and (.execution | facts::execution_shape_ok) and - (.observed_at | schema::time_ok) and - (.output | schema::present_ok(schema::content_ref_ok)) and + (.observed_at | time_ok) and + (.output | present_ok(content_ref_ok)) and (.provider_metadata | provider_metadata_shape) and - (.request_ref | schema::document_ref_kind_ok("stage_request")) and + (.request_ref | document_ref_kind_ok("stage_request")) and (.resolved_profile_ref | - schema::document_ref_kind_ok("resolved_profile")) and + document_ref_kind_ok("resolved_profile")) and (.state | source_state_ok) and - (.target_revision | schema::git_revision_ref_ok) and + (.target_revision | git_revision_ref_ok) and .attempt.recorded_at <= .observed_at and (if .state == "changed" then .output.state == "present" else .output.state == "absent" end)); def input_shape: - schema::exact_fields(["snapshot","trust_context"];[]) and + exact_fields(["snapshot","trust_context"];[]) and (.trust_context | trust_shape) and (.snapshot | snapshot_shape); diff --git a/ci/required-files.txt b/ci/required-files.txt index 64bb6b8..c59f919 100644 --- a/ci/required-files.txt +++ b/ci/required-files.txt @@ -232,3 +232,7 @@ scripts/test/orchestrator-reconciliation-plan.test.sh # Inactive GitHub forge normalizer payload adapters/github-forge/v1/normalize.jq scripts/test/default-github-forge-adapter.test.sh + +# Inactive Claude Code producer normalizer payload +adapters/claude-code-producer/v1/normalize.jq +scripts/test/default-claude-code-producer-adapter.test.sh diff --git a/scripts/test/default-claude-code-producer-adapter.test.sh b/scripts/test/default-claude-code-producer-adapter.test.sh index 3645bbf..876e32e 100755 --- a/scripts/test/default-claude-code-producer-adapter.test.sh +++ b/scripts/test/default-claude-code-producer-adapter.test.sh @@ -4,9 +4,7 @@ set -euo pipefail export LC_ALL=C root=$(CDPATH='' cd -P -- "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P) -package="$root/adapters/claude-code-producer/v1" -manifest="$package/manifest.json" -normalizer="$package/normalize.jq" +normalizer="$root/adapters/claude-code-producer/v1/normalize.jq" fixtures="$root/scripts/test" tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-claude-producer.XXXXXX") cleanup() { /bin/rm -rf -- "$tmp"; } @@ -53,6 +51,14 @@ expect_error() { else fail "$name"; fi } +manifest="$tmp/test-owned-producer-manifest.json" +"${jq_cmd[@]}" -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); + f::manifest("producer") | v2 | + .id="adapter.claude-code-producer.v1" | + .body.offered_tools=[] +' >"$manifest" manifest_sha=$(sha_file "$manifest") shas="$tmp/manifest-shas.json" "${jq_cmd[@]}" -S -c -n --arg producer "$manifest_sha" \ @@ -158,18 +164,6 @@ build_input() { baseline_input="$tmp/baseline.input" build_input "$snapshot" "$baseline_input" -check manifest-canonical /usr/bin/cmp -s "$manifest" <("${jq_cmd[@]}" -S -c . "$manifest") -check manifest-core "${jq_cmd[@]}" -L "$modules" -e 'import "profile_graph" as p; p::adapter_manifest_self_ok' "$manifest" -check manifest-ceiling "${jq_cmd[@]}" -e '.body.offered_roles==["producer"] and - .body.offered_execution_kinds==["model"] and .body.offered_capabilities==["core.harness.produce.v1"] and - .body.offered_permissions==["core.perm.evidence.write.v1","core.perm.model.invoke.v1", - "core.perm.scratch.write.v1","core.perm.target.read.v1"] and .body.offered_tools==[]' "$manifest" -package_commit=$("${jq_cmd[@]}" -r '.body.package_ref.revision.commit_id' "$manifest") -package_path=$("${jq_cmd[@]}" -r '.body.package_ref.location.value' "$manifest") -package_object=$("${jq_cmd[@]}" -r '.body.package_ref.object_id' "$manifest") -check package-ref test "$(git -C "$root" rev-parse "$package_commit:$package_path")" = "$package_object" -check package-current test "$(git -C "$root" hash-object "$normalizer")" = "$package_object" - states='changed changed adapter.changed completed changed present no-change no-change adapter.no-change completed no-change absent failure inconclusive adapter.provider-failure failed inconclusive absent @@ -212,6 +206,17 @@ expect_error missing-fact E_SHAPE 'del(.snapshot.body.execution.metadata.model)' expect_error unknown-state E_SHAPE '.snapshot.body.state="unknown"' expect_error duplicate-skills E_SHAPE '.snapshot.body.execution.metadata.skills.value += [.snapshot.body.execution.metadata.skills.value[0]]' expect_error unordered-skills E_SHAPE '.snapshot.body.execution.metadata.skills.value |= reverse' +expect_error extra-snapshot-field E_SHAPE '.snapshot.body.unexpected=true' +expect_error invalid-attempt-date E_SHAPE '.snapshot.body.attempt.started_at="2026-02-30T00:01:00Z"' +expect_error negative-zero-attempt E_SHAPE '.snapshot.body.attempt.attempt_number=-0' +expect_error oversized-provider-message E_SHAPE \ + '.snapshot.body.provider_metadata.message.value=("x"*1025)' +expect_error invalid-output-content-id E_SHAPE \ + '.snapshot.body.output.value.content_id="producer:patch"' +expect_error oversized-output-media-type E_SHAPE \ + '.snapshot.body.output.value.media_type=("application/"+("x"*116))' +expect_error malformed-target-revision E_SHAPE \ + '.trust_context.body.target_revision.commit_id=("A"*40)' expect_error moved-target E_STALE '.snapshot.body.target_revision.commit_id=("0"*40)' expect_error package-mismatch E_STALE '.snapshot.body.execution.actual_binding.package_ref.object_id=("0"*40)' expect_error config-mismatch E_STALE '.snapshot.body.execution.actual_binding.config_ref.value.object_id=("0"*40)' @@ -226,10 +231,11 @@ expect_error boundary-mismatch E_STALE '.snapshot.body.execution.performer.execu expect_error tool-mismatch E_STALE '.snapshot.body.execution.metadata.tools.value=[{ tool_id:"unexpected.tool",tool_version:"v1",package_ref:.snapshot.body.execution.actual_binding.package_ref, config_ref:{state:"absent"}}]' -expect_error manifest-ceiling E_TRUST '.trust_context.body.manifest.content.body.offered_capabilities += ["core.review.change.v1"] | +expect_error caller-manifest-ceiling E_TRUST '.trust_context.body.manifest.content.body.offered_capabilities += ["core.review.change.v1"] | .trust_context.body.manifest.content.body.offered_capabilities |= sort' -check no-selected-generation sh -c '! grep -E "g-[0-9a-f]{64}" "$@"' sh "$manifest" "$normalizer" "$root/scripts/test/default-claude-code-producer-adapter.test.sh" +check no-selected-generation sh -c '! grep -E "g-[0-9a-f]{64}" "$@"' sh \ + "$normalizer" "$root/scripts/test/default-claude-code-producer-adapter.test.sh" check jq-static static_jq if [ "$failed" -ne 0 ]; then From be7ab5281967c5287a6b6e12b0955997109a2691 Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 15:51:55 -0400 Subject: [PATCH 4/4] Bind Claude producer snapshot and attempt provenance --- README.md | 4 + RESTORE.md | 3 + adapters/claude-code-producer/v1/normalize.jq | 34 ++++++-- ...fault-claude-code-producer-adapter.test.sh | 77 +++++++++++++++++-- 4 files changed, 106 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f0e41cb..3d42cb6 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,10 @@ snapshot against a caller-supplied core request, resolved profile, manifest, target, package, config, prompt, skills, tools, model, effort, and execution boundary. It returns a canonical observation for changed, unchanged, stale, failed, timed out, degraded, or inconclusive work. Provider text stays data. +Before constructing the trust context, the caller must canonicalize the snapshot, +verify its SHA-256, and supply that verified content-and-digest pair. The +normalizer requires the untrusted snapshot to equal the pair, binds the expected +attempt ID and number, and requires `text/x-diff` output for a changed git patch. This payload ships no adapter manifest. Its focused test owns a synthetic manifest only to prove the caller-manifest and binding checks stay closed. A diff --git a/RESTORE.md b/RESTORE.md index d5a96fd..7d14fc2 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -489,6 +489,9 @@ bash scripts/test/default-claude-code-producer-adapter.test.sh This checks the caller-supplied core request, resolved profile, manifest, target, package, config, prompt, skills, tools, model, effort, and execution boundary. The test manifest is synthetic fixture data; no adapter manifest ships in this unit. +A caller constructs the trust context only after canonical SHA-256 verification +of the snapshot pair. The proof rejects moved untrusted content, changed attempt +identity, and non-diff output for a changed git-patch request. A later assembly PR can bind the durable main payload into the default profile. The pure jq payload is inactive, offline, and unqualified. It does not call Claude Code, invoke a model, use a credential or network, write a target, publish, or diff --git a/adapters/claude-code-producer/v1/normalize.jq b/adapters/claude-code-producer/v1/normalize.jq index b055e5a..8af4d80 100644 --- a/adapters/claude-code-producer/v1/normalize.jq +++ b/adapters/claude-code-producer/v1/normalize.jq @@ -75,20 +75,31 @@ def document_ref($pair): sha256:$pair.sha256 }; +def snapshot_ref($pair): + { + content_id:"claude-code-snapshot", + media_type:"application/json", + sha256:$pair.sha256 + }; + def trust_shape: exact_fields(["body","id","kind","schema_version"];[]) and .schema_version == 1 and .kind == "adapter_trust_context" and (.id | id_ok) and (.body | exact_fields( - ["binding_id","manifest","request","resolved_profile","snapshot_ref", - "target_revision"];[]) and + ["binding_id","expected_attempt_id","expected_attempt_number","manifest", + "request","resolved_profile","target_revision","verified_snapshot"];[]) and (.binding_id | id_ok) and + (.expected_attempt_id | id_ok) and + (.expected_attempt_number | int_ok) and .expected_attempt_number >= 1 and (.manifest | profile::document_pair_ok("adapter_manifest")) and (.request | profile::document_pair_ok("stage_request")) and (.resolved_profile | profile::document_pair_ok("resolved_profile")) and - (.snapshot_ref | content_ref_ok) and - .snapshot_ref.media_type == "application/json" and + (.verified_snapshot | + exact_fields(["content","sha256"];[]) and + (.content | type == "object") and + (.sha256 | sha256_ok)) and (.target_revision | git_revision_ref_ok)); def attempt_shape: @@ -135,6 +146,7 @@ def snapshot_shape: def input_shape: exact_fields(["snapshot","trust_context"];[]) and (.trust_context | trust_shape) and + (.trust_context.body.verified_snapshot.content | snapshot_shape) and (.snapshot | snapshot_shape); def selected_resolved_binding($trust): @@ -203,13 +215,17 @@ def metadata_complete($metadata): def snapshot_relations($trust; $snapshot): selected_resolved_binding($trust)[0].binding as $binding | + $trust.body.request.content.body.operation.arguments.artifact_kind as $artifact_kind | request::expected_execution_projection( $trust.body.request.content.body; $trust.body.resolved_profile.content.body) as $expected | + $snapshot == $trust.body.verified_snapshot.content and $snapshot.body.request_ref == document_ref($trust.body.request) and $snapshot.body.resolved_profile_ref == document_ref($trust.body.resolved_profile) and $snapshot.body.target_revision == $trust.body.target_revision and + $snapshot.body.attempt.attempt_id == $trust.body.expected_attempt_id and + $snapshot.body.attempt.attempt_number == $trust.body.expected_attempt_number and $trust.body.request.content.body.requested_at <= $snapshot.body.attempt.started_at and $expected != null and @@ -217,6 +233,10 @@ def snapshot_relations($trust; $snapshot): $snapshot.body.execution.performer == $expected.performer and $snapshot.body.execution.environment == $expected.environment and $snapshot.body.execution.used_capability == $expected.used_capability and + (if $snapshot.body.state == "changed" and $artifact_kind == "git-patch" then + $snapshot.body.output.state == "present" and + $snapshot.body.output.value.media_type == "text/x-diff" + else true end) and metadata_relations($snapshot.body.execution.metadata;$binding); def result_status($source_state): @@ -271,12 +291,14 @@ def observation($trust; $snapshot): state:$state, reason_id:reason_id($snapshot;$state), trust_context:{ - snapshot_ref:$trust.body.snapshot_ref, + snapshot_ref:snapshot_ref($trust.body.verified_snapshot), request_ref:document_ref($trust.body.request), resolved_profile_ref:document_ref($trust.body.resolved_profile), manifest_ref:document_ref($trust.body.manifest), target_revision:$trust.body.target_revision, - binding_id:$trust.body.binding_id + binding_id:$trust.body.binding_id, + expected_attempt_id:$trust.body.expected_attempt_id, + expected_attempt_number:$trust.body.expected_attempt_number }, observation:{ observed_at:$snapshot.body.observed_at, diff --git a/scripts/test/default-claude-code-producer-adapter.test.sh b/scripts/test/default-claude-code-producer-adapter.test.sh index 876e32e..989579e 100755 --- a/scripts/test/default-claude-code-producer-adapter.test.sh +++ b/scripts/test/default-claude-code-producer-adapter.test.sh @@ -43,6 +43,16 @@ static_jq() { /usr/bin/grep -Fq E_SHAPE "$tmp/static.err" } expect_error() { + local name=$1 code=$2 filter=$3 + local raw="$tmp/$name.raw" candidate="$tmp/$name.input" + local out="$tmp/$name.out" err="$tmp/$name.err" + "${jq_cmd[@]}" -S -c "$filter" "$baseline_input" >"$raw" + refresh_verified_snapshot "$raw" "$candidate" + if normalize "$candidate" >"$out" 2>"$err"; then fail "$name" + elif [ ! -s "$out" ] && /usr/bin/grep -Fq "$code" "$err"; then pass + else fail "$name"; fi +} +expect_raw_error() { local name=$1 code=$2 filter=$3 local candidate="$tmp/$name.input" out="$tmp/$name.out" err="$tmp/$name.err" "${jq_cmd[@]}" -S -c "$filter" "$baseline_input" >"$candidate" @@ -106,7 +116,11 @@ request_file="$tmp/request.json" "${jq_cmd[@]}" -L "$fixtures" -S -c -n --arg resolved_sha "$resolved_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); - f::request_doc("producer";$resolved_sha) | v2 + f::request_doc("producer";$resolved_sha) | v2 | + .body.operation.arguments={ + artifact_kind:"git-patch", + allowed_delta:f::delivered("allowed-delta";"output";f::sha("3")) + } ' >"$request_file" request_sha=$(sha_file "$request_file") @@ -148,19 +162,33 @@ trust="$tmp/trust.json" --slurpfile manifest "$manifest" --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" \ --arg manifest_sha "$manifest_sha" ' {schema_version:1,kind:"adapter_trust_context",id:"trust.example",body:{ - binding_id:"binding.producer",manifest:{content:$manifest[0],sha256:$manifest_sha}, + binding_id:"binding.producer",expected_attempt_id:"attempt.example", + expected_attempt_number:1,manifest:{content:$manifest[0],sha256:$manifest_sha}, request:{content:$request[0],sha256:$request_sha}, resolved_profile:{content:$resolved[0],sha256:$resolved_sha}, - snapshot_ref:{content_id:"claude-code-snapshot",media_type:"application/json",sha256:("0"*64)}, target_revision:$request[0].body.target_revision.value}} ' >"$trust" build_input() { local source=$1 destination=$2 snapshot_sha snapshot_sha=$(sha_file "$source") "${jq_cmd[@]}" -S -c -n --slurpfile trust "$trust" --slurpfile snapshot "$source" \ - --arg sha "$snapshot_sha" '{trust_context:($trust[0] | .body.snapshot_ref.sha256=$sha),snapshot:$snapshot[0]}' \ + --arg sha "$snapshot_sha" ' + {trust_context:($trust[0] | + .body.verified_snapshot={content:$snapshot[0],sha256:$sha}), + snapshot:$snapshot[0]} + ' \ >"$destination" } +refresh_verified_snapshot() { + local source=$1 destination=$2 snapshot_sha + local verified="$destination.snapshot" + "${jq_cmd[@]}" -S -c '.snapshot' "$source" >"$verified" + snapshot_sha=$(sha_file "$verified") + "${jq_cmd[@]}" -S -c --slurpfile snapshot "$verified" --arg sha "$snapshot_sha" ' + .trust_context.body.verified_snapshot={content:$snapshot[0],sha256:$sha} + ' "$source" >"$destination" + /bin/rm -f -- "$verified" +} baseline_input="$tmp/baseline.input" build_input "$snapshot" "$baseline_input" @@ -195,13 +223,42 @@ while read -r source_state state reason status outcome output_state; do ' >/dev/null; then pass; else fail "state-$source_state"; fi done <<<"$states" +incomplete_snapshot="$tmp/incomplete.snapshot" incomplete="$tmp/incomplete.input" -"${jq_cmd[@]}" -S -c '.snapshot.body.execution.metadata.model={state:"unavailable",reason_id:"provider.model-unavailable"}' \ - "$baseline_input" >"$incomplete" +"${jq_cmd[@]}" -S -c \ + '.body.execution.metadata.model={state:"unavailable",reason_id:"provider.model-unavailable"}' \ + "$snapshot" >"$incomplete_snapshot" +build_input "$incomplete_snapshot" "$incomplete" if normalize "$incomplete" | "${jq_cmd[@]}" -e '.state=="inconclusive" and .reason_id=="adapter.metadata-incomplete" and .observation.result.output_ref.state=="absent"' >/dev/null; then pass else fail metadata-incomplete; fi +baseline_output="$tmp/baseline.output" +normalize "$baseline_input" >"$baseline_output" +check verified-snapshot-provenance "${jq_cmd[@]}" -e \ + --slurpfile input "$baseline_input" ' + .trust_context.snapshot_ref == { + content_id:"claude-code-snapshot",media_type:"application/json", + sha256:$input[0].trust_context.body.verified_snapshot.sha256 + } and + .trust_context.expected_attempt_id == "attempt.example" and + .trust_context.expected_attempt_number == 1 + ' "$baseline_output" +check git-patch-text-diff "${jq_cmd[@]}" -e \ + --slurpfile request "$request_file" ' + $request[0].body.operation.arguments.artifact_kind == "git-patch" and + .state == "changed" and + .observation.result.output_ref.value.media_type == "text/x-diff" + ' "$baseline_output" + +expect_raw_error moved-untrusted-snapshot-fixed-pair E_STALE \ + '.snapshot.body.provider_metadata.message.value="moved untrusted content"' +expect_raw_error bare-unverified-snapshot-digest E_SHAPE ' + .trust_context.body.verified_snapshot.sha256 as $sha | + .trust_context.body |= (del(.verified_snapshot) | + .snapshot_ref={content_id:"claude-code-snapshot",media_type:"application/json",sha256:$sha}) +' + expect_error missing-fact E_SHAPE 'del(.snapshot.body.execution.metadata.model)' expect_error unknown-state E_SHAPE '.snapshot.body.state="unknown"' expect_error duplicate-skills E_SHAPE '.snapshot.body.execution.metadata.skills.value += [.snapshot.body.execution.metadata.skills.value[0]]' @@ -217,6 +274,14 @@ expect_error oversized-output-media-type E_SHAPE \ '.snapshot.body.output.value.media_type=("application/"+("x"*116))' expect_error malformed-target-revision E_SHAPE \ '.trust_context.body.target_revision.commit_id=("A"*40)' +expect_error invalid-expected-attempt-number E_SHAPE \ + '.trust_context.body.expected_attempt_number=0' +expect_error moved-attempt-id E_STALE \ + '.snapshot.body.attempt.attempt_id="attempt.other"' +expect_error moved-attempt-number E_STALE \ + '.snapshot.body.attempt.attempt_number=2' +expect_error git-patch-json-output E_STALE \ + '.snapshot.body.output.value.media_type="application/json"' expect_error moved-target E_STALE '.snapshot.body.target_revision.commit_id=("0"*40)' expect_error package-mismatch E_STALE '.snapshot.body.execution.actual_binding.package_ref.object_id=("0"*40)' expect_error config-mismatch E_STALE '.snapshot.body.execution.actual_binding.config_ref.value.object_id=("0"*40)'