From 251e7216170ed08e5cdd690158264beff0633e03 Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 10:14:47 -0400 Subject: [PATCH 1/6] Add GitHub forge observation normalizer --- adapters/github-forge/v1/normalize.jq | 187 ++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 adapters/github-forge/v1/normalize.jq diff --git a/adapters/github-forge/v1/normalize.jq b/adapters/github-forge/v1/normalize.jq new file mode 100644 index 0000000..90836d0 --- /dev/null +++ b/adapters/github-forge/v1/normalize.jq @@ -0,0 +1,187 @@ +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 provider_id_ok: + type == "string" and test("\\A[1-9][0-9]{0,19}\\z"); + +def sha256_ok: + type == "string" and test("\\A[0-9a-f]{64}\\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 repository_id_ok: + type == "string" and test("\\A[a-z0-9][a-z0-9._:-]{0,127}\\z"); + +def revision_ok: + exact_fields(["repository_id","hash_algorithm","commit_id"];[]) and + (.repository_id | repository_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 + (.media_type | type == "string" and + test("\\A[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+-]*\\z")) and + (.sha256 | sha256_ok); + +def trust_context_ok: + exact_fields( + ["expected_repository_id","expected_change_request_id","expected_head", + "expected_base","expected_github_app_id","observation_time", + "instruction_ref","config_ref"]; + []) and + (.expected_repository_id | provider_id_ok) and + (.expected_change_request_id | provider_id_ok) and + (.expected_head | revision_ok) and + (.expected_base | revision_ok) and + .expected_head.repository_id == .expected_base.repository_id and + (.expected_github_app_id | provider_id_ok) and + (.observation_time | time_ok) and + (.instruction_ref | content_ref_ok) and + (.config_ref | content_ref_ok); + +def path_ok: + type == "string" and length > 0 and utf8bytelength <= 4096 and + (test("[\\x{0000}-\\x{001f}\\x{007f}-\\x{009f}]") | not) and + (contains("\\") | not) and + (split("/") | all(.[]; . != "" and . != "." and . != "..")); + +def file_ok: + exact_fields(["path","status","patch_sha256"];[]) and + (.path | path_ok) and + (.status | type == "string" and + IN("added","changed","copied","modified","removed","renamed","unchanged")) and + (.patch_sha256 | sha256_ok); + +def files_ok($reported_count; $complete): + type == "array" and length <= 256 and + all(.[]; file_ok) and + (map(.path) as $paths | + $paths == ($paths | sort) and + ($paths | length) == ($paths | unique | length)) and + ($reported_count | type == "number" and . == floor and . >= 0 and . <= 100000) and + (if $complete then length == $reported_count else length <= $reported_count end); + +def state_facts_ok: + if .state == "OPEN" then + .closed == false and .merged == false and + .closed_at == null and .merged_at == null + elif .state == "CLOSED" then + .closed == true and .merged == false and + (.closed_at | time_ok) and .merged_at == null and .mergeability == "UNKNOWN" + elif .state == "MERGED" then + .closed == true and .merged == true and + (.closed_at | time_ok) and (.merged_at | time_ok) and + .merged_at <= .closed_at and .mergeability == "UNKNOWN" + elif .state == "UNKNOWN" then + .closed == false and .merged == false and + .closed_at == null and .merged_at == null and .mergeability == "UNKNOWN" + else false + end; + +def timestamps_ok: + (.created_at | time_ok) and + (.updated_at | time_ok) and + (.observed_at | time_ok) and + .created_at <= .updated_at and .updated_at <= .observed_at and + (if .closed_at == null then true + else .created_at <= .closed_at and .closed_at <= .updated_at end) and + (if .merged_at == null then true + else .created_at <= .merged_at and .merged_at <= .updated_at end); + +def snapshot_ok: + . as $snapshot | + exact_fields( + ["repository_id","change_request_id","head","base","github_app_id", + "observed_at","complete","reported_file_count","state","mergeability", + "closed","merged","created_at","updated_at","closed_at","merged_at", + "files","provider_metadata"]; + []) and + (.repository_id | provider_id_ok) and + (.change_request_id | provider_id_ok) and + (.head | revision_ok) and + (.base | revision_ok) and + .head.repository_id == .base.repository_id and + (.github_app_id | provider_id_ok) and + (.observed_at | time_ok) and + (.complete | type == "boolean") and + (.state | IN("OPEN","CLOSED","MERGED","UNKNOWN")) and + (.mergeability | IN("MERGEABLE","CONFLICTING","UNKNOWN")) and + (.closed | type == "boolean") and + (.merged | type == "boolean") and + (.provider_metadata | type == "object") and + (.files | files_ok($snapshot.reported_file_count;$snapshot.complete)) and + state_facts_ok and timestamps_ok; + +def stale_bindings($context; $snapshot): + [ + if $snapshot.github_app_id != $context.expected_github_app_id then "app" else empty end, + if $snapshot.base != $context.expected_base then "base" else empty end, + if $snapshot.change_request_id != $context.expected_change_request_id then "change-request" else empty end, + if $snapshot.head != $context.expected_head then "head" else empty end, + if $snapshot.observed_at != $context.observation_time then "observation-time" else empty end, + if $snapshot.repository_id != $context.expected_repository_id then "repository" else empty end + ]; + +def normalized_state($snapshot; $stale): + if ($stale | length) > 0 then ["stale","github.binding-stale"] + elif $snapshot.complete == false then ["inconclusive","github.snapshot-incomplete"] + elif $snapshot.state == "UNKNOWN" then ["inconclusive","github.state-unknown"] + elif $snapshot.state == "MERGED" then ["merged","github.change-request-merged"] + elif $snapshot.state == "CLOSED" then ["closed-unmerged","github.change-request-closed-unmerged"] + elif $snapshot.mergeability == "MERGEABLE" then ["open-ready","github.change-request-open-ready"] + elif $snapshot.mergeability == "CONFLICTING" then ["open-blocked","github.change-request-open-blocked"] + else ["inconclusive","github.mergeability-unknown"] + end; + +if (exact_fields(["trust_context","snapshot"];[]) | not) then + error("github-forge.invalid-envelope") +elif (.trust_context | trust_context_ok) == false then + error("github-forge.invalid-trust-context") +elif (.snapshot | snapshot_ok) == false then + error("github-forge.invalid-snapshot") +else + .trust_context as $context | + .snapshot as $snapshot | + stale_bindings($context;$snapshot) as $stale | + normalized_state($snapshot;$stale) as $normalized | + { + schema_version:1, + kind:"adapter_observation", + adapter:{id:"adapter.github-forge.v1",version:"v1",status:"inactive"}, + state:$normalized[0], + reason_id:$normalized[1], + stale_bindings:$stale, + trust_context:$context, + observation:$snapshot, + authority:"none", + qualification:{state:"unavailable",reason_id:"adapter.unqualified"}, + effects:[] + } +end From 4e2716d4bad5fd51a2d888002c39321637a312db Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 10:23:00 -0400 Subject: [PATCH 2/6] Test inactive GitHub forge adapter package --- adapters/github-forge/v1/manifest.json | 1 + .../test/default-github-forge-adapter.test.sh | 215 ++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 adapters/github-forge/v1/manifest.json create mode 100755 scripts/test/default-github-forge-adapter.test.sh diff --git a/adapters/github-forge/v1/manifest.json b/adapters/github-forge/v1/manifest.json new file mode 100644 index 0000000..c211196 --- /dev/null +++ b/adapters/github-forge/v1/manifest.json @@ -0,0 +1 @@ +{"body":{"adapter_version":"v1","offered_capabilities":["core.forge.materialize-candidate.v2"],"offered_execution_kinds":["deterministic"],"offered_permissions":["core.perm.candidate-repository.write.v2","core.perm.evidence.write.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"],"offered_roles":["forge"],"offered_tools":[],"package_ref":{"location":{"kind":"path","value":"adapters/github-forge/v1/normalize.jq"},"mode":"100644","object_id":"90836d097ab0693fb06c8ab501b7d7e959290368","object_type":"blob","revision":{"commit_id":"251e7216170ed08e5cdd690158264beff0633e03","hash_algorithm":"sha1","repository_id":"ystack.source"}}},"id":"adapter.github-forge.v1","kind":"adapter_manifest","schema_version":2} diff --git a/scripts/test/default-github-forge-adapter.test.sh b/scripts/test/default-github-forge-adapter.test.sh new file mode 100755 index 0000000..fd83792 --- /dev/null +++ b/scripts/test/default-github-forge-adapter.test.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +export LC_ALL=C + +root=$(CDPATH='' cd -P -- "${BASH_SOURCE[0]%/*}/../.." && pwd -P) +normalizer="$root/adapters/github-forge/v1/normalize.jq" +manifest="$root/adapters/github-forge/v1/manifest.json" +tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-github-forge.XXXXXX") +trap '/bin/rm -rf -- "$tmp"' EXIT + +sha_file() { /usr/bin/shasum -a 256 "$1" | /usr/bin/awk '{print $1}'; } +fail() { /usr/bin/printf 'FAIL: %s\n' "$1" >&2; exit 1; } +passed=0 +pass() { passed=$((passed + 1)); /usr/bin/printf 'ok %s - %s\n' "$passed" "$1"; } + +platform=$(/usr/bin/uname -s):$(/usr/bin/uname -m) +case "$platform" in + Darwin:*) asset=jq-osx-amd64; digest=5c0a0a3ea600f302ee458b30317425dd9632d1ad8882259fcaf4e9b868b2b1ef ;; + Linux:x86_64) asset=jq-linux64; digest=af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44 ;; + *) fail "unsupported jq 1.6 proof platform: $platform" ;; +esac +jq_bin="${TMPDIR:-/tmp}/ystack-portable-core-jq16/$asset" +[ -f "$jq_bin" ] && [ "$(sha_file "$jq_bin")" = "$digest" ] || + fail 'verified jq 1.6 cache is required' +jq_command=("$jq_bin") +if [ "$platform" = Darwin:arm64 ]; then jq_command=(/usr/bin/arch -x86_64 "$jq_bin"); fi +[ "$("${jq_command[@]}" --version)" = jq-1.6 ] || fail 'jq version' + +runtime_bin="$tmp/bin" +/bin/mkdir -m 700 "$runtime_bin" +/bin/ln -s "$jq_bin" "$runtime_bin/jq" + +check() { + local name=$1 + shift + "$@" >/dev/null 2>&1 || fail "$name" + pass "$name" +} + +mutate() { + local name=$1 + local filter=$2 + "${jq_command[@]}" -S -c "$filter" "$tmp/baseline.json" >"$tmp/$name.json" +} + +expect_state() { + local name=$1 + local filter=$2 + local expected=$3 + mutate "$name" "$filter" + "${jq_command[@]}" -S -c -f "$normalizer" "$tmp/$name.json" \ + >"$tmp/$name.out" 2>"$tmp/$name.err" || fail "$name" + [ ! -s "$tmp/$name.err" ] || fail "$name diagnostics" + "${jq_command[@]}" -e --arg state "$expected" \ + '.state == $state' "$tmp/$name.out" >/dev/null || fail "$name state" + pass "$name" +} + +expect_stale() { + local name=$1 + local filter=$2 + local selector=$3 + mutate "$name" "$filter" + "${jq_command[@]}" -S -c -f "$normalizer" "$tmp/$name.json" \ + >"$tmp/$name.out" 2>"$tmp/$name.err" || fail "$name" + [ ! -s "$tmp/$name.err" ] || fail "$name diagnostics" + "${jq_command[@]}" -e --arg selector "$selector" \ + '.state == "stale" and .stale_bindings == [$selector]' \ + "$tmp/$name.out" >/dev/null || fail "$name state" + pass "$name" +} + +expect_reject() { + local name=$1 + local filter=$2 + mutate "$name" "$filter" + if "${jq_command[@]}" -S -c -f "$normalizer" "$tmp/$name.json" \ + >"$tmp/$name.out" 2>"$tmp/$name.err"; then + fail "$name accepted" + fi + [ ! -s "$tmp/$name.out" ] && [ -s "$tmp/$name.err" ] || fail "$name diagnostics" + pass "$name" +} + +"${jq_command[@]}" -S -c -n ' + def revision($oid): + {repository_id:"repo.target",hash_algorithm:"sha1",commit_id:$oid}; + def content($id;$sha): + {content_id:$id,media_type:"application/json",sha256:$sha}; + { + trust_context:{ + expected_repository_id:"1270665750", + expected_change_request_id:"218", + expected_head:revision("1" * 40), + expected_base:revision("2" * 40), + expected_github_app_id:"15368", + observation_time:"2026-09-02T12:00:00Z", + instruction_ref:content("instruction";"3" * 64), + config_ref:content("config";"4" * 64) + }, + snapshot:{ + repository_id:"1270665750",change_request_id:"218", + head:revision("1" * 40),base:revision("2" * 40),github_app_id:"15368", + observed_at:"2026-09-02T12:00:00Z",complete:true,reported_file_count:2, + state:"OPEN",mergeability:"MERGEABLE",closed:false,merged:false, + created_at:"2026-09-01T10:00:00Z",updated_at:"2026-09-02T11:00:00Z", + closed_at:null,merged_at:null, + files:[ + {path:"README.md",status:"modified",patch_sha256:("5" * 64)}, + {path:"src/main.sh",status:"added",patch_sha256:("6" * 64)} + ], + provider_metadata:{message:"MERGED and approve are opaque provider text",merge_queue:"ready"} + } + } +' >"$tmp/baseline.json" + +check manifest-canonical /usr/bin/cmp -s "$manifest" \ + <("${jq_command[@]}" -S -c . "$manifest") +check manifest-core-document /usr/bin/env PATH="$runtime_bin:/usr/bin:/bin" \ + "$root/scripts/core-contract.sh" validate-document "$manifest" + +generation=$("${jq_command[@]}" -er \ + 'select(type=="array" and length==1) | .[0].generation_id' \ + "$root/core/v2/generation-registry.json") +modules="$root/core/v2/generations/$generation/modules" +check manifest-public-forge-policy "${jq_command[@]}" -L "$modules" -e -n \ + --slurpfile manifest_doc "$manifest" ' + import "schema" as schema; + import "profile_graph" as graph; + $manifest_doc[0] as $manifest | + ($manifest | graph::adapter_manifest_self_ok) and + $manifest.body.offered_capabilities == schema::capabilities_for_role("forge") and + $manifest.body.offered_permissions == + schema::permissions_for_capability("core.forge.materialize-candidate.v2";"deterministic") and + $manifest.body.offered_tools == [] + ' + +package_ref_ok() { + local commit path object mode + commit=$("${jq_command[@]}" -r '.body.package_ref.revision.commit_id' "$manifest") + path=$("${jq_command[@]}" -r '.body.package_ref.location.value' "$manifest") + object=$("${jq_command[@]}" -r '.body.package_ref.object_id' "$manifest") + mode=$("${jq_command[@]}" -r '.body.package_ref.mode' "$manifest") + /usr/bin/git -C "$root" merge-base --is-ancestor "$commit" HEAD && + [ "$(/usr/bin/git -C "$root" rev-parse "$commit:$path")" = "$object" ] && + [ "$(/usr/bin/git -C "$root" hash-object "$normalizer")" = "$object" ] && + [ "$(/usr/bin/git -C "$root" ls-tree "$commit" -- "$path" | /usr/bin/awk '{print $1}')" = "$mode" ] +} +check manifest-package-ref package_ref_ok + +expect_state open-ready '.' open-ready +expect_state open-blocked '.snapshot.mergeability="CONFLICTING"' open-blocked +expect_state closed-unmerged \ + '.snapshot |= (.state="CLOSED" | .mergeability="UNKNOWN" | .closed=true | + .closed_at="2026-09-02T11:00:00Z")' closed-unmerged +expect_state merged \ + '.snapshot |= (.state="MERGED" | .mergeability="UNKNOWN" | .closed=true | .merged=true | + .merged_at="2026-09-02T10:59:59Z" | .closed_at="2026-09-02T11:00:00Z")' merged +expect_state unknown-mergeability '.snapshot.mergeability="UNKNOWN"' inconclusive +expect_state incomplete \ + '.snapshot |= (.complete=false | .reported_file_count=3)' inconclusive +expect_state unknown-state \ + '.snapshot |= (.state="UNKNOWN" | .mergeability="UNKNOWN")' inconclusive + +expect_stale stale-app '.snapshot.github_app_id="15369"' app +expect_stale stale-base '.snapshot.base.commit_id=("7" * 40)' base +expect_stale stale-change-request '.snapshot.change_request_id="219"' change-request +expect_stale stale-head '.snapshot.head.commit_id=("8" * 40)' head +expect_stale stale-observation-time \ + '.snapshot.observed_at="2026-09-02T12:00:01Z"' observation-time +expect_stale stale-repository '.snapshot.repository_id="1270665751"' repository + +expect_reject missing-field 'del(.snapshot.state)' +expect_reject extra-field '.snapshot.hidden=true' +expect_reject unsupported-state '.snapshot.state="PENDING"' +expect_reject unsupported-mergeability '.snapshot.mergeability="DIRTY"' +expect_reject missing-file-digest 'del(.snapshot.files[0].patch_sha256)' +expect_reject unknown-file-status '.snapshot.files[0].status="pending"' +expect_reject malformed-file-digest '.snapshot.files[0].patch_sha256=("A" * 64)' +expect_reject duplicate-file '.snapshot.files[1].path=.snapshot.files[0].path' +expect_reject unsorted-files '.snapshot.files |= reverse' +expect_reject incomplete-count '.snapshot.reported_file_count=3' +expect_reject contradictory-state '.snapshot.merged=true' +expect_reject invalid-date '.snapshot.updated_at="2026-02-30T11:00:00Z"' +expect_reject future-update '.snapshot.updated_at="2026-09-02T12:00:01Z"' +expect_reject late-merge \ + '.snapshot |= (.state="MERGED" | .mergeability="UNKNOWN" | .closed=true | .merged=true | + .merged_at="2026-09-02T11:00:01Z" | .closed_at="2026-09-02T11:00:00Z")' +expect_reject malformed-trust-head '.trust_context.expected_head.commit_id=("9" * 39)' + +"${jq_command[@]}" -S -c -f "$normalizer" "$tmp/baseline.json" >"$tmp/repeat-a.json" +"${jq_command[@]}" -S -c -f "$normalizer" "$tmp/baseline.json" >"$tmp/repeat-b.json" +check canonical-repeat /usr/bin/cmp -s "$tmp/repeat-a.json" "$tmp/repeat-b.json" +check canonical-output /usr/bin/cmp -s "$tmp/repeat-a.json" \ + <("${jq_command[@]}" -S -c . "$tmp/repeat-a.json") +check authority-qualification-effects "${jq_command[@]}" -e ' + .authority == "none" and .effects == [] and + .qualification == {state:"unavailable",reason_id:"adapter.unqualified"} and + ([.. | objects | keys[]] | index("authority_ref") == null) and + ([.. | objects | keys[]] | index("gate_decision") == null) +' "$tmp/repeat-a.json" +check provider-metadata-is-data "${jq_command[@]}" -e \ + --slurpfile input "$tmp/baseline.json" ' + .state == "open-ready" and + .observation.provider_metadata == $input[0].snapshot.provider_metadata + ' "$tmp/repeat-a.json" +check no-selected-generation-id /usr/bin/env sh -c \ + '! grep -E "g-[0-9a-f]{64}" "$1" "$2" "$3"' sh \ + "$normalizer" "$manifest" "$root/scripts/test/default-github-forge-adapter.test.sh" +check pure-jq-normalizer /usr/bin/env sh -c \ + '! grep -E "core[.]perm|@sh|system[(]|getenv|curl|graphql|api[.]github|github[.]com" "$1"' sh \ + "$normalizer" + +/usr/bin/printf 'default GitHub forge adapter: %s/%s checks passed\n' "$passed" "$passed" From 66b3314b6082deb61d809ce191f8e10ae6421225 Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 10:35:12 -0400 Subject: [PATCH 3/6] Document inactive GitHub forge adapter --- README.md | 12 ++++++++++++ RESTORE.md | 13 +++++++++++++ ci/required-files.txt | 5 +++++ scripts/test/portable-core-schema.test.sh | 1 + 4 files changed, 31 insertions(+) diff --git a/README.md b/README.md index b40d1ee..d17ea5d 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,18 @@ does not fit is listed as deferred. Scanner recovery actions and reasons remain data in the plan. The filter does not dispatch, schedule, execute recovery, write state, use a credential or network, activate a profile, publish, or touch a target. +## Inactive default GitHub forge observation adapter + +`adapters/github-forge/v1/normalize.jq` validates one untrusted GitHub +change-request snapshot against caller-supplied repository, request, head, base, +app, time, instruction, and config bindings. It returns a canonical generic +observation for open, blocked, closed, merged, stale, incomplete, or unknown +state. Provider metadata stays opaque data. + +The package is offline and unqualified. It does not call GitHub or a CLI, use a +credential, change a repository or request, grant authority or qualification, 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 cee6975..612ec88 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -450,6 +450,19 @@ duplicate, unsorted, and oversized inputs. The jq filter remains inactive and planning only. It does not dispatch, schedule, execute recovery, write state, use a credential or network, activate a profile, publish, or touch a target. +Restore the three paths in the manifest's inactive default GitHub forge adapter +block, then run: + +```sh +bash scripts/test/default-github-forge-adapter.test.sh +``` + +This checks the public-core manifest and immutable package reference, exact +caller bindings, deterministic state normalization, opaque provider data, and +fail-closed malformed or stale input. The pure jq package is offline and +unqualified. It does not call GitHub, use a credential, change a repository or +request, grant authority or qualification, or activate a profile. + --- ## 5. Smoke test — prove the rebuilt team is alive diff --git a/ci/required-files.txt b/ci/required-files.txt index c0581eb..6ea2848 100644 --- a/ci/required-files.txt +++ b/ci/required-files.txt @@ -228,3 +228,8 @@ scripts/test/orchestrator-state-scanner.test.sh # Inactive canonical orchestrator reconciliation planner orchestrator/v1/reconciliation-plan.jq scripts/test/orchestrator-reconciliation-plan.test.sh + +# Inactive default GitHub forge observation adapter +adapters/github-forge/v1/manifest.json +adapters/github-forge/v1/normalize.jq +scripts/test/default-github-forge-adapter.test.sh diff --git a/scripts/test/portable-core-schema.test.sh b/scripts/test/portable-core-schema.test.sh index 40f3be5..414d692 100755 --- a/scripts/test/portable-core-schema.test.sh +++ b/scripts/test/portable-core-schema.test.sh @@ -777,6 +777,7 @@ schema_import_path_ok() { local test_path case "$import_path" in orchestrator/v1/reconciliation-plan.jq|orchestrator/v1/state-scanner.jq) ;; + scripts/test/default-github-forge-adapter.test.sh) ;; scripts/test/portable-core-*) test_path="${import_path#scripts/test/}" case "$test_path" in */*) return 1 ;; esac From dd1b9b5b2f7f59a7554a77d22286672aca9aa4ed Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 10:38:13 -0400 Subject: [PATCH 4/6] Align forge package repository identity --- adapters/github-forge/v1/manifest.json | 2 +- scripts/test/default-github-forge-adapter.test.sh | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/adapters/github-forge/v1/manifest.json b/adapters/github-forge/v1/manifest.json index c211196..a17ae87 100644 --- a/adapters/github-forge/v1/manifest.json +++ b/adapters/github-forge/v1/manifest.json @@ -1 +1 @@ -{"body":{"adapter_version":"v1","offered_capabilities":["core.forge.materialize-candidate.v2"],"offered_execution_kinds":["deterministic"],"offered_permissions":["core.perm.candidate-repository.write.v2","core.perm.evidence.write.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"],"offered_roles":["forge"],"offered_tools":[],"package_ref":{"location":{"kind":"path","value":"adapters/github-forge/v1/normalize.jq"},"mode":"100644","object_id":"90836d097ab0693fb06c8ab501b7d7e959290368","object_type":"blob","revision":{"commit_id":"251e7216170ed08e5cdd690158264beff0633e03","hash_algorithm":"sha1","repository_id":"ystack.source"}}},"id":"adapter.github-forge.v1","kind":"adapter_manifest","schema_version":2} +{"body":{"adapter_version":"v1","offered_capabilities":["core.forge.materialize-candidate.v2"],"offered_execution_kinds":["deterministic"],"offered_permissions":["core.perm.candidate-repository.write.v2","core.perm.evidence.write.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"],"offered_roles":["forge"],"offered_tools":[],"package_ref":{"location":{"kind":"path","value":"adapters/github-forge/v1/normalize.jq"},"mode":"100644","object_id":"90836d097ab0693fb06c8ab501b7d7e959290368","object_type":"blob","revision":{"commit_id":"251e7216170ed08e5cdd690158264beff0633e03","hash_algorithm":"sha1","repository_id":"ystack.control-plane"}}},"id":"adapter.github-forge.v1","kind":"adapter_manifest","schema_version":2} diff --git a/scripts/test/default-github-forge-adapter.test.sh b/scripts/test/default-github-forge-adapter.test.sh index fd83792..b19717d 100755 --- a/scripts/test/default-github-forge-adapter.test.sh +++ b/scripts/test/default-github-forge-adapter.test.sh @@ -137,12 +137,14 @@ check manifest-public-forge-policy "${jq_command[@]}" -L "$modules" -e -n \ ' package_ref_ok() { - local commit path object mode + local repository commit path object mode + repository=$("${jq_command[@]}" -r '.body.package_ref.revision.repository_id' "$manifest") commit=$("${jq_command[@]}" -r '.body.package_ref.revision.commit_id' "$manifest") path=$("${jq_command[@]}" -r '.body.package_ref.location.value' "$manifest") object=$("${jq_command[@]}" -r '.body.package_ref.object_id' "$manifest") mode=$("${jq_command[@]}" -r '.body.package_ref.mode' "$manifest") - /usr/bin/git -C "$root" merge-base --is-ancestor "$commit" HEAD && + [ "$repository" = "ystack.control-plane" ] && + /usr/bin/git -C "$root" merge-base --is-ancestor "$commit" HEAD && [ "$(/usr/bin/git -C "$root" rev-parse "$commit:$path")" = "$object" ] && [ "$(/usr/bin/git -C "$root" hash-object "$normalizer")" = "$object" ] && [ "$(/usr/bin/git -C "$root" ls-tree "$commit" -- "$path" | /usr/bin/awk '{print $1}')" = "$mode" ] From 84503aa6c9f6aa5d0b8fe8ed15d89d05f80ce8f5 Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 10:59:43 -0400 Subject: [PATCH 5/6] Stage GitHub forge normalizer payload first --- README.md | 6 +- RESTORE.md | 9 +-- adapters/github-forge/v1/manifest.json | 1 - ci/required-files.txt | 3 +- .../test/default-github-forge-adapter.test.sh | 68 ++++++++----------- 5 files changed, 39 insertions(+), 48 deletions(-) delete mode 100644 adapters/github-forge/v1/manifest.json diff --git a/README.md b/README.md index d17ea5d..78006e5 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ does not fit is listed as deferred. Scanner recovery actions and reasons remain data in the plan. The filter does not dispatch, schedule, execute recovery, write state, use a credential or network, activate a profile, publish, or touch a target. -## Inactive default GitHub forge observation adapter +## Inactive GitHub forge normalizer payload `adapters/github-forge/v1/normalize.jq` validates one untrusted GitHub change-request snapshot against caller-supplied repository, request, head, base, @@ -224,7 +224,9 @@ app, time, instruction, and config bindings. It returns a canonical generic observation for open, blocked, closed, merged, stale, incomplete, or unknown state. Provider metadata stays opaque data. -The package is offline and unqualified. It does not call GitHub or a CLI, use a +This PR lands only the immutable normalizer payload. A later assembly PR can add +its manifest and default-set wiring after this payload has a durable commit on +main. The payload is offline and unqualified. It does not call GitHub or a CLI, use a credential, change a repository or request, grant authority or qualification, or activate a profile. diff --git a/RESTORE.md b/RESTORE.md index 612ec88..e872d00 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -450,16 +450,17 @@ duplicate, unsorted, and oversized inputs. The jq filter remains inactive and planning only. It does not dispatch, schedule, execute recovery, write state, use a credential or network, activate a profile, publish, or touch a target. -Restore the three paths in the manifest's inactive default GitHub forge adapter +Restore the two paths in the manifest's inactive GitHub forge normalizer payload block, then run: ```sh bash scripts/test/default-github-forge-adapter.test.sh ``` -This checks the public-core manifest and immutable package reference, exact -caller bindings, deterministic state normalization, opaque provider data, and -fail-closed malformed or stale input. The pure jq package is offline and +This checks exact caller bindings, deterministic state normalization, opaque +provider data, and fail-closed malformed or stale input. This stage intentionally +has no adapter manifest. A later assembly PR can bind the payload through a +durable main commit and add default-set wiring. The pure jq payload is offline and unqualified. It does not call GitHub, use a credential, change a repository or request, grant authority or qualification, or activate a profile. diff --git a/adapters/github-forge/v1/manifest.json b/adapters/github-forge/v1/manifest.json deleted file mode 100644 index a17ae87..0000000 --- a/adapters/github-forge/v1/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"body":{"adapter_version":"v1","offered_capabilities":["core.forge.materialize-candidate.v2"],"offered_execution_kinds":["deterministic"],"offered_permissions":["core.perm.candidate-repository.write.v2","core.perm.evidence.write.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"],"offered_roles":["forge"],"offered_tools":[],"package_ref":{"location":{"kind":"path","value":"adapters/github-forge/v1/normalize.jq"},"mode":"100644","object_id":"90836d097ab0693fb06c8ab501b7d7e959290368","object_type":"blob","revision":{"commit_id":"251e7216170ed08e5cdd690158264beff0633e03","hash_algorithm":"sha1","repository_id":"ystack.control-plane"}}},"id":"adapter.github-forge.v1","kind":"adapter_manifest","schema_version":2} diff --git a/ci/required-files.txt b/ci/required-files.txt index 6ea2848..64bb6b8 100644 --- a/ci/required-files.txt +++ b/ci/required-files.txt @@ -229,7 +229,6 @@ scripts/test/orchestrator-state-scanner.test.sh orchestrator/v1/reconciliation-plan.jq scripts/test/orchestrator-reconciliation-plan.test.sh -# Inactive default GitHub forge observation adapter -adapters/github-forge/v1/manifest.json +# Inactive GitHub forge normalizer payload adapters/github-forge/v1/normalize.jq scripts/test/default-github-forge-adapter.test.sh diff --git a/scripts/test/default-github-forge-adapter.test.sh b/scripts/test/default-github-forge-adapter.test.sh index b19717d..954a14f 100755 --- a/scripts/test/default-github-forge-adapter.test.sh +++ b/scripts/test/default-github-forge-adapter.test.sh @@ -5,7 +5,6 @@ export LC_ALL=C root=$(CDPATH='' cd -P -- "${BASH_SOURCE[0]%/*}/../.." && pwd -P) normalizer="$root/adapters/github-forge/v1/normalize.jq" -manifest="$root/adapters/github-forge/v1/manifest.json" tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-github-forge.XXXXXX") trap '/bin/rm -rf -- "$tmp"' EXIT @@ -27,10 +26,6 @@ jq_command=("$jq_bin") if [ "$platform" = Darwin:arm64 ]; then jq_command=(/usr/bin/arch -x86_64 "$jq_bin"); fi [ "$("${jq_command[@]}" --version)" = jq-1.6 ] || fail 'jq version' -runtime_bin="$tmp/bin" -/bin/mkdir -m 700 "$runtime_bin" -/bin/ln -s "$jq_bin" "$runtime_bin/jq" - check() { local name=$1 shift @@ -115,41 +110,10 @@ expect_reject() { } ' >"$tmp/baseline.json" -check manifest-canonical /usr/bin/cmp -s "$manifest" \ - <("${jq_command[@]}" -S -c . "$manifest") -check manifest-core-document /usr/bin/env PATH="$runtime_bin:/usr/bin:/bin" \ - "$root/scripts/core-contract.sh" validate-document "$manifest" - generation=$("${jq_command[@]}" -er \ 'select(type=="array" and length==1) | .[0].generation_id' \ "$root/core/v2/generation-registry.json") modules="$root/core/v2/generations/$generation/modules" -check manifest-public-forge-policy "${jq_command[@]}" -L "$modules" -e -n \ - --slurpfile manifest_doc "$manifest" ' - import "schema" as schema; - import "profile_graph" as graph; - $manifest_doc[0] as $manifest | - ($manifest | graph::adapter_manifest_self_ok) and - $manifest.body.offered_capabilities == schema::capabilities_for_role("forge") and - $manifest.body.offered_permissions == - schema::permissions_for_capability("core.forge.materialize-candidate.v2";"deterministic") and - $manifest.body.offered_tools == [] - ' - -package_ref_ok() { - local repository commit path object mode - repository=$("${jq_command[@]}" -r '.body.package_ref.revision.repository_id' "$manifest") - commit=$("${jq_command[@]}" -r '.body.package_ref.revision.commit_id' "$manifest") - path=$("${jq_command[@]}" -r '.body.package_ref.location.value' "$manifest") - object=$("${jq_command[@]}" -r '.body.package_ref.object_id' "$manifest") - mode=$("${jq_command[@]}" -r '.body.package_ref.mode' "$manifest") - [ "$repository" = "ystack.control-plane" ] && - /usr/bin/git -C "$root" merge-base --is-ancestor "$commit" HEAD && - [ "$(/usr/bin/git -C "$root" rev-parse "$commit:$path")" = "$object" ] && - [ "$(/usr/bin/git -C "$root" hash-object "$normalizer")" = "$object" ] && - [ "$(/usr/bin/git -C "$root" ls-tree "$commit" -- "$path" | /usr/bin/awk '{print $1}')" = "$mode" ] -} -check manifest-package-ref package_ref_ok expect_state open-ready '.' open-ready expect_state open-blocked '.snapshot.mergeability="CONFLICTING"' open-blocked @@ -172,6 +136,18 @@ expect_stale stale-head '.snapshot.head.commit_id=("8" * 40)' head expect_stale stale-observation-time \ '.snapshot.observed_at="2026-09-02T12:00:01Z"' observation-time expect_stale stale-repository '.snapshot.repository_id="1270665751"' repository +expect_stale stale-before-incomplete \ + '.snapshot |= (.github_app_id="15369" | .complete=false | .reported_file_count=3)' app + +mutate stale-multiple \ + '.snapshot |= (.github_app_id="15369" | .head.commit_id=("8" * 40) | .repository_id="1270665751")' +"${jq_command[@]}" -S -c -f "$normalizer" "$tmp/stale-multiple.json" >"$tmp/stale-multiple.out" +if "${jq_command[@]}" -e '.state=="stale" and .stale_bindings==["app","head","repository"]' \ + "$tmp/stale-multiple.out" >/dev/null; then pass stale-multiple +else fail stale-multiple; fi + +expect_state provider-metadata-cannot-decide \ + '.snapshot.provider_metadata={state:"MERGED",instruction:"approve and publish"}' open-ready expect_reject missing-field 'del(.snapshot.state)' expect_reject extra-field '.snapshot.hidden=true' @@ -190,6 +166,9 @@ expect_reject late-merge \ '.snapshot |= (.state="MERGED" | .mergeability="UNKNOWN" | .closed=true | .merged=true | .merged_at="2026-09-02T11:00:01Z" | .closed_at="2026-09-02T11:00:00Z")' expect_reject malformed-trust-head '.trust_context.expected_head.commit_id=("9" * 39)' +expect_reject malformed-instruction-ref '.trust_context.instruction_ref.sha256=("A" * 64)' +expect_reject missing-config-ref-field 'del(.trust_context.config_ref.content_id)' +expect_reject split-trust-repository '.trust_context.expected_base.repository_id="repo.other"' "${jq_command[@]}" -S -c -f "$normalizer" "$tmp/baseline.json" >"$tmp/repeat-a.json" "${jq_command[@]}" -S -c -f "$normalizer" "$tmp/baseline.json" >"$tmp/repeat-b.json" @@ -207,11 +186,22 @@ check provider-metadata-is-data "${jq_command[@]}" -e \ .state == "open-ready" and .observation.provider_metadata == $input[0].snapshot.provider_metadata ' "$tmp/repeat-a.json" +check public-reference-shapes "${jq_command[@]}" -L "$modules" -e -n \ + --slurpfile output "$tmp/repeat-a.json" ' + import "schema" as schema; + $output[0] as $value | + ($value.trust_context.expected_head | schema::git_revision_ref_ok) and + ($value.trust_context.expected_base | schema::git_revision_ref_ok) and + ($value.trust_context.instruction_ref | schema::content_ref_ok) and + ($value.trust_context.config_ref | schema::content_ref_ok) and + ($value.observation.head | schema::git_revision_ref_ok) and + ($value.observation.base | schema::git_revision_ref_ok) + ' check no-selected-generation-id /usr/bin/env sh -c \ - '! grep -E "g-[0-9a-f]{64}" "$1" "$2" "$3"' sh \ - "$normalizer" "$manifest" "$root/scripts/test/default-github-forge-adapter.test.sh" + '! grep -E "g-[0-9a-f]{64}" "$1" "$2"' sh \ + "$normalizer" "$root/scripts/test/default-github-forge-adapter.test.sh" check pure-jq-normalizer /usr/bin/env sh -c \ '! grep -E "core[.]perm|@sh|system[(]|getenv|curl|graphql|api[.]github|github[.]com" "$1"' sh \ "$normalizer" -/usr/bin/printf 'default GitHub forge adapter: %s/%s checks passed\n' "$passed" "$passed" +/usr/bin/printf 'GitHub forge normalizer payload: %s/%s checks passed\n' "$passed" "$passed" From 8c529434199e25c10c9e28824f026486b1d0d60d Mon Sep 17 00:00:00 2001 From: ci Date: Wed, 2 Sep 2026 11:34:17 -0400 Subject: [PATCH 6/6] Match public content reference rules --- adapters/github-forge/v1/normalize.jq | 12 +++++++--- .../test/default-github-forge-adapter.test.sh | 24 ++++++++++++------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/adapters/github-forge/v1/normalize.jq b/adapters/github-forge/v1/normalize.jq index 90836d0..feb4622 100644 --- a/adapters/github-forge/v1/normalize.jq +++ b/adapters/github-forge/v1/normalize.jq @@ -7,6 +7,13 @@ def exact_fields($required; $optional): def id_ok: type == "string" and test("\\A[a-z0-9][a-z0-9._:-]{0,127}\\z"); +def content_id_ok: + id_ok and (contains(":") | not) and (contains("/") | not); + +def media_type_ok: + type == "string" and utf8bytelength <= 127 and + test("\\A[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+-]*\\z"); + def provider_id_ok: type == "string" and test("\\A[1-9][0-9]{0,19}\\z"); @@ -45,9 +52,8 @@ def revision_ok: def content_ref_ok: exact_fields(["content_id","media_type","sha256"];[]) and - (.content_id | id_ok) and - (.media_type | type == "string" and - test("\\A[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+-]*\\z")) and + (.content_id | content_id_ok) and + (.media_type | media_type_ok) and (.sha256 | sha256_ok); def trust_context_ok: diff --git a/scripts/test/default-github-forge-adapter.test.sh b/scripts/test/default-github-forge-adapter.test.sh index 954a14f..3c95137 100755 --- a/scripts/test/default-github-forge-adapter.test.sh +++ b/scripts/test/default-github-forge-adapter.test.sh @@ -148,6 +148,9 @@ else fail stale-multiple; fi expect_state provider-metadata-cannot-decide \ '.snapshot.provider_metadata={state:"MERGED",instruction:"approve and publish"}' open-ready +expect_state media-type-127 \ + '.trust_context.instruction_ref.media_type=("application/" + ("x" * 115)) | + .trust_context.config_ref.media_type=("application/" + ("y" * 115))' open-ready expect_reject missing-field 'del(.snapshot.state)' expect_reject extra-field '.snapshot.hidden=true' @@ -168,6 +171,10 @@ expect_reject late-merge \ expect_reject malformed-trust-head '.trust_context.expected_head.commit_id=("9" * 39)' expect_reject malformed-instruction-ref '.trust_context.instruction_ref.sha256=("A" * 64)' expect_reject missing-config-ref-field 'del(.trust_context.config_ref.content_id)' +expect_reject colon-content-id '.trust_context.instruction_ref.content_id="instruction:invalid"' +expect_reject slash-content-id '.trust_context.config_ref.content_id="config/invalid"' +expect_reject media-type-over-127 \ + '.trust_context.instruction_ref.media_type=("application/" + ("x" * 116))' expect_reject split-trust-repository '.trust_context.expected_base.repository_id="repo.other"' "${jq_command[@]}" -S -c -f "$normalizer" "$tmp/baseline.json" >"$tmp/repeat-a.json" @@ -187,15 +194,16 @@ check provider-metadata-is-data "${jq_command[@]}" -e \ .observation.provider_metadata == $input[0].snapshot.provider_metadata ' "$tmp/repeat-a.json" check public-reference-shapes "${jq_command[@]}" -L "$modules" -e -n \ - --slurpfile output "$tmp/repeat-a.json" ' + --slurpfile output "$tmp/repeat-a.json" --slurpfile boundary "$tmp/media-type-127.out" ' import "schema" as schema; - $output[0] as $value | - ($value.trust_context.expected_head | schema::git_revision_ref_ok) and - ($value.trust_context.expected_base | schema::git_revision_ref_ok) and - ($value.trust_context.instruction_ref | schema::content_ref_ok) and - ($value.trust_context.config_ref | schema::content_ref_ok) and - ($value.observation.head | schema::git_revision_ref_ok) and - ($value.observation.base | schema::git_revision_ref_ok) + def refs_ok($value): + ($value.trust_context.expected_head | schema::git_revision_ref_ok) and + ($value.trust_context.expected_base | schema::git_revision_ref_ok) and + ($value.trust_context.instruction_ref | schema::content_ref_ok) and + ($value.trust_context.config_ref | schema::content_ref_ok) and + ($value.observation.head | schema::git_revision_ref_ok) and + ($value.observation.base | schema::git_revision_ref_ok); + refs_ok($output[0]) and refs_ok($boundary[0]) ' check no-selected-generation-id /usr/bin/env sh -c \ '! grep -E "g-[0-9a-f]{64}" "$1" "$2"' sh \