From 9af8689327f342bfebf7bb5752c1dbfe0a8629dd Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 13 Oct 2026 16:55:37 +0200 Subject: [PATCH] security: close sync deadline fix and checkpoint scoped forge expansion --- .../agents/custodian-codex/supervision.json | 23 +++ .../CUST-WP-0080/expand_forge_profiles.py | 169 +++++++++++++++++ .../forge-expansion-operations.md | 46 +++++ .../forge-expansion-proposal.json | 178 ++++++++++++++++++ .../CUST-WP-0080/inventory_forge_coverage.py | 86 +++++++++ .../CUST-WP-0080/probe_push_deadline.py | 67 +++++++ .../CUST-WP-0080/receive-probe-veto-expansion | 11 ++ .../test_expand_forge_profiles.py | 57 ++++++ .../forge-expansion-inventory.json | 21 +++ .../forge-expansion-preflight.json | 10 + .../forge-gitlink-binding.json | 27 +++ .../push-deadline.json | 78 ++++++++ ...gdom-security-blueprint-and-gap-closure.md | 24 +++ 13 files changed, 797 insertions(+) create mode 100644 docs/changes/CUST-WP-0080/expand_forge_profiles.py create mode 100644 docs/changes/CUST-WP-0080/forge-expansion-operations.md create mode 100644 docs/changes/CUST-WP-0080/forge-expansion-proposal.json create mode 100644 docs/changes/CUST-WP-0080/inventory_forge_coverage.py create mode 100644 docs/changes/CUST-WP-0080/probe_push_deadline.py create mode 100644 docs/changes/CUST-WP-0080/receive-probe-veto-expansion create mode 100644 docs/changes/CUST-WP-0080/test_expand_forge_profiles.py create mode 100644 docs/evidence/2026-10-13-netkingdom-security/forge-expansion-inventory.json create mode 100644 docs/evidence/2026-10-13-netkingdom-security/forge-expansion-preflight.json create mode 100644 docs/evidence/2026-10-13-netkingdom-security/forge-gitlink-binding.json create mode 100644 docs/evidence/2026-10-13-netkingdom-security/push-deadline.json diff --git a/.kaizen/agents/custodian-codex/supervision.json b/.kaizen/agents/custodian-codex/supervision.json index 8955503..b8c7f1b 100644 --- a/.kaizen/agents/custodian-codex/supervision.json +++ b/.kaizen/agents/custodian-codex/supervision.json @@ -631,6 +631,29 @@ "original_digest_semantics": "SHA256 of exactly reconstructed executed JSON bytes; not the archive envelope", "triage_ref": "docs/evidence/2026-10-13-netkingdom-security/forge-custody-metadata-triage.json" } + }, + { + "proposal_id": "FORGE6-EXPAND-001", + "disposition": "unscored", + "outcome": "unverified", + "original_revision": 1, + "original_digest_sha256": "508d540e578b8cb93a187de644f15d5ef404e2c0a90fdb96ef22cdbb2bb94c78", + "proposal_ref": "docs/changes/CUST-WP-0080/forge-expansion-proposal.json", + "authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md", + "reason": "Standing founder blue-lane authorization after exact metadata/hook preflight and atomic-ownership regression checks; no exact-revision human approval or autopilot claim.", + "target": "143 numeric repository IDs bound to private inventory digest on existing Forgejo pod", + "expected_result": "All143native targets have exact original generated hooks plus admitted profile. Each actual SSH clean and synthetic secret probe emits correct scanner result under a ref-specific veto; no probe refs/quarantined commits remain; vetoes and inert ownership links removed. Existing23profiles untouched. One incompatible gitlink repository remains explicit work, not an exemption.", + "verification_ref": "docs/evidence/2026-10-13-netkingdom-security/forge-expansion-activation.json", + "rollback": "Same executor --rollback with exact proposal, private inventory and apply receipt. Removes only exact installed hook/veto hashes on attempted IDs; automatic recovery additionally requires original ownership hard-links. Preserve all preexisting generated hooks. Unknown concurrent changes abort removal, retain unverified outcome for abort owner.", + "abort_owner": "railiance-forge; executor custodian-codex", + "cost_estimate_eur": 0, + "risk_estimate": "Additive reversible hook configuration, no rollout/new resources or identity/policy scope expansion. Probe traffic is serialized and scoped to a temporary ref; veto leaves normal refs unaffected. Unsupported or secret-bearing writes may refuse by design. Limits and initial-history/import coverage remain explicit. Abort on precondition or acceptance failure and roll back attempted cohort; no actual credential content used.", + "prepared_at": "2026-10-13T14:48:05.689089+00:00", + "promotion_evidence": false, + "refinement_or_rescue": false, + "executed_revision": 1, + "executed_digest_sha256": "508d540e578b8cb93a187de644f15d5ef404e2c0a90fdb96ef22cdbb2bb94c78", + "execution_note": "Serialized expansion running; checkpoint receipt records attempted and verified numeric IDs. Do not replay or infer completion while its process is live." } ] } diff --git a/docs/changes/CUST-WP-0080/expand_forge_profiles.py b/docs/changes/CUST-WP-0080/expand_forge_profiles.py new file mode 100644 index 0000000..1ecc6ec --- /dev/null +++ b/docs/changes/CUST-WP-0080/expand_forge_profiles.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Admit an exact private inventory, add hooks, and verify vetoed SSH pushes.""" +import argparse +import datetime +import hashlib +import importlib.util +import json +from pathlib import Path +import re +import shlex + +HERE=Path(__file__).parent +s=importlib.util.spec_from_file_location('template',HERE/'activate_forge_template.py') +t=importlib.util.module_from_spec(s) +s.loader.exec_module(t) + + +def repo_path(row): + if row['owner']!='coulomb' or not re.fullmatch(r'[a-z0-9-]+',row['name']): + raise ValueError('scope-identity-refused') + return '/data/git/gitea-repositories/coulomb/'+row['name']+'.git' + + +def inspect(pod,rows,expected): + commands=['set -euo pipefail'] + for row in rows: + commands += ['repo='+shlex.quote(repo_path(row)), + 'printf "REPO '+str(row['id'])+'\\n"', + 'test -d "$repo" && test ! -L "$repo" && test ! -L "$repo/hooks" && test ! -L "$repo/hooks/pre-receive.d"', + 'test -z "$(find "$repo/hooks" -maxdepth 2 -type l -print -quit)"', + 'find "$repo/hooks" -maxdepth 2 -type f ! -name "*.sample" -exec sha256sum {} \\;', + 'test "$(git --git-dir="$repo" rev-parse --is-shallow-repository)" = false', + 'test "$(git --git-dir="$repo" rev-parse --show-object-format)" = sha1', + 'config=$(git --git-dir="$repo" config --local --includes --name-only --list)', + 'if printf "%s\\n" "$config" | grep -Eiq "(^extensions.partialclone$|^remote\\..*\\.(promisor|partialclonefilter)$)"; then exit 21; fi', + 'if git --git-dir="$repo" rev-parse --verify HEAD >/dev/null 2>&1; then git --git-dir="$repo" ls-tree -r --format="%(objectmode)" HEAD | awk \'$0=="160000" {bad=1} END {exit bad}\'; fi'] + raw=t.native.execute(pod,'bash','-s',payload=('\n'.join(commands)+'\n').encode(),timeout=180).decode() + found={}; current=None; by_id={r['id']:r for r in rows} + for line in raw.splitlines(): + if line.startswith('REPO '): current=int(line[5:]);found[current]={} + else: + digest,path=line.split(' ',1);prefix=repo_path(by_id[current])+'/hooks/' + if not path.startswith(prefix) or not re.fullmatch('[a-f0-9]{64}',digest):raise ValueError('unexpected-hook-output') + found[current][path[len(prefix):]]=digest + if set(found)!=set(by_id) or any(hooks!=expected for hooks in found.values()): + raise ValueError('hook-inventory-changed') + + +def install(pod,path,content,digest): + # Temporary executable is outside the dispatch directory. Hard-link creation + # atomically exposes a complete file and refuses any existing destination. + script='''set -eu +umask 077 +target=$1 +temporary=$2 +test ! -e "$target" && test ! -L "$target" +test ! -e "$temporary" && test ! -L "$temporary" +set -C +cat > "$temporary" +test "$(sha256sum "$temporary" | cut -d ' ' -f 1)" = "$3" +chmod 755 "$temporary" +ln "$temporary" "$target" +''' + temporary=path.rsplit('/hooks/pre-receive.d/',1)[0]+'/.custodian-expand-'+Path(path).name + t.native.execute(pod,'sh','-c',script,'sh',path,temporary,digest,payload=content) + + +def remove_owned(pod,path,digest,require_marker=False): + marker=path.rsplit('/hooks/pre-receive.d/',1)[0]+'/.custodian-expand-'+Path(path).name + t.native.execute(pod,'sh','-c', + 'set -eu; if test -e "$1" || test -L "$1"; then test -f "$1" && test ! -L "$1"; ' + 'test "$(sha256sum "$1" | cut -d " " -f 1)" = "$2"; ' + 'if test "$4" = yes; then test "$1" -ef "$3"; fi; rm -- "$1"; fi; ' + 'if test -e "$3" || test -L "$3"; then test -f "$3" && test ! -L "$3"; ' + 'test "$(sha256sum "$3" | cut -d " " -f 1)" = "$2"; rm -- "$3"; fi', + 'sh',path,digest,marker,'yes' if require_marker else 'no') + + +def rollback(pod,rows,original,proposal,require_marker=True): + for row in rows: + for name,digest in [(original['hook_name'],original['hook_sha256']), + (proposal['veto_name'],proposal['veto_sha256'])]: + remove_owned(pod,repo_path(row)+'/hooks/pre-receive.d/'+name,digest,require_marker) + inspect(pod,rows,original['existing_hook_hashes']) + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--proposal',type=Path,required=True) + parser.add_argument('--inventory',type=Path,required=True) + parser.add_argument('--receipt',type=Path,required=True) + mode=parser.add_mutually_exclusive_group() + mode.add_argument('--apply',action='store_true') + mode.add_argument('--rollback',action='store_true') + args=parser.parse_args() + p=json.loads(args.proposal.read_text()) + for file,digest in p['executor_hashes'].items(): + if hashlib.sha256((HERE/file).read_bytes()).hexdigest()!=digest:raise ValueError('executor-changed') + if hashlib.sha256(args.inventory.read_bytes()).hexdigest()!=p['inventory_sha256']:raise ValueError('scope-changed') + inventory=json.loads(args.inventory.read_text());by_id={r['id']:r for r in inventory['repositories']} + rows=[by_id[i] for i in p['target_ids']] + if len(rows)!=p['target_count'] or len(set(p['target_ids']))!=len(rows):raise ValueError('scope-count-changed') + if any(r['profile']!='builtin-only' or not r['head_compatible'] for r in rows):raise ValueError('inadmissible-scope') + original=json.loads((HERE/'receive-hook-proposal.json').read_text()) + pod=t.target_pod(t.deployment(),original) + if pod['pod_uid']!=p['pod_uid']:raise ValueError('pod-changed') + t.check_artifacts(pod) + for a in original['artifacts'][:2]: + if t.native.execute(pod,'stat','-c','%a %u %g',original['version_directory']+'/'+a['name']).decode().strip()!='755 1000 1000': + raise ValueError('artifact-custody-changed') + if args.rollback: + receipt=json.loads(args.receipt.read_text()) + if receipt['proposal_sha256']!=hashlib.sha256(args.proposal.read_bytes()).hexdigest():raise ValueError('receipt-changed') + rollback(pod,[by_id[i] for i in receipt['attempted_ids']],original,p, + require_marker=receipt['result']!='verified') + receipt['rollback']='verified-original-profiles-restored' + args.receipt.write_text(json.dumps(receipt,indent=2)+'\n');return + inspect(pod,rows,original['existing_hook_hashes']) + fixture=Path(p['fixture_key']) + if hashlib.sha256(fixture.read_bytes()).hexdigest()!=p['fixture_sha256']:raise ValueError('fixture-changed') + hook=(HERE/'receive-profile-hook').read_bytes();veto=(HERE/'receive-probe-veto-expansion').read_bytes() + assert hashlib.sha256(hook).hexdigest()==original['hook_sha256'] + assert hashlib.sha256(veto).hexdigest()==p['veto_sha256'] + receipt={'proposal_id':p['proposal_id'],'proposal_sha256':hashlib.sha256(args.proposal.read_bytes()).hexdigest(), + 'pod_uid':pod['pod_uid'],'result':'preflight-verified','target_count':len(rows),'attempted_ids':[], + 'verified_ids':[],'at':datetime.datetime.now(datetime.timezone.utc).isoformat()} + def save():args.receipt.write_text(json.dumps(receipt,indent=2)+'\n') + save() + if not args.apply:print(json.dumps(receipt));return + try: + receipt['result']='applying';save() + for row in rows: + receipt['attempted_ids'].append(row['id']);save() + path=repo_path(row)+'/hooks/pre-receive.d/' + # No sample ref can be accepted during its probe, even if scanning fails. + install(pod,path+p['veto_name'],veto,p['veto_sha256']) + install(pod,path+original['hook_name'],hook,original['hook_sha256']) + proof=dict(pod,repositories=[row['name']],fixture_key=str(fixture),fixture_sha256=p['fixture_sha256'], + probe_ref=p['probe_ref'],proposal_id=p['proposal_id']) + t.activation.verify_pushes(t.native,proof) + remove_owned(pod,path+p['veto_name'],p['veto_sha256'],require_marker=True) + expected=dict(original['existing_hook_hashes']);expected['pre-receive.d/'+original['hook_name']]=original['hook_sha256'] + inspect(pod,[row],expected) + receipt['verified_ids'].append(row['id']);save() + if len(receipt['verified_ids'])%5==0:print('Verified scanner profiles:',len(receipt['verified_ids']),'of',len(rows),flush=True) + inspect(pod,rows,expected) + receipt.update(result='verified',clean_scanner_and_synthetic_refusal_under_veto=True, + no_probe_refs_or_quarantined_commits=True,vetoes_removed=True, + original_dispatchers_preserved=True) + save() + except Exception as error: + receipt.update(result='failed',failure=type(error).__name__);save() + try: + rollback(pod,[by_id[i] for i in receipt['attempted_ids']],original,p) + receipt['rollback']='verified-original-profiles-restored' + except Exception as recovery: + receipt['rollback']='unverified:'+type(recovery).__name__ + save();raise SystemExit('Expansion failed; inspect value-free receipt and rollback status.') + # After successful acceptance, receipt-bound rollback can use exact hashes. + # Cleanup failure leaves only inert ownership links outside the hook directory. + for row in rows: + target=repo_path(row)+'/hooks/pre-receive.d/'+original['hook_name'] + marker=repo_path(row)+'/.custodian-expand-'+original['hook_name'] + t.native.execute(pod,'sh','-c','set -eu; test "$1" -ef "$2"; rm -- "$2"','sh',target,marker) + receipt['ownership_markers_removed']=True;save() + print('All exact target profiles verified.',flush=True) + + +if __name__=='__main__':main() diff --git a/docs/changes/CUST-WP-0080/forge-expansion-operations.md b/docs/changes/CUST-WP-0080/forge-expansion-operations.md new file mode 100644 index 0000000..5456b57 --- /dev/null +++ b/docs/changes/CUST-WP-0080/forge-expansion-operations.md @@ -0,0 +1,46 @@ +# Exact existing-repository expansion + +FORGE6-EXPAND-001 binds 143 numeric repository IDs to a private database/directory +inventory digest and the current pod UID. It adds the already admitted scanner +hook, preserving all generated hooks and the 23 profiles found at preflight. +The one incompatible repository with four external gitlinks remains open in +FORGE-WP-0006-T02 and CUST-WP-0080-T03. It is not an accepted exception. + +`expand_forge_profiles.py` checkpoints attempted and verified IDs after each +repository. Each pair of actual SSH pushes must produce the scanner's clean and +blocked verdicts under a temporary veto on the exact probe ref. That ref and its +quarantined synthetic commit must be absent afterward. This proves scanner +execution and refusal, not ordinary clean-push acceptance on every repository. + +Only aggregate counts and numeric IDs belong in committed receipts. The exact +private inventory and unbound synthetic fixture are retained in ignored +`.private-backups/forge-expansion-20261013/` (directory 0700, files 0600), with +digests in the proposal. Temporary working copies are under `/tmp`. These are +not production credentials. Verify hashes before restoring a missing copy. + +During execution, ownership hard-links live at repository roots, outside hook +dispatch directories. Installation uses an exclusive temporary file and an +atomic hard-link that cannot overwrite an existing destination. Automatic +recovery requires the target to match its original ownership link and hash; +unexpected concurrent changes refuse removal. Successful completion removes +vetoes and then the inert ownership links. Four local installer/recovery tests +exercise existing destinations, changed contents and identical unowned files. + +On interruption, inspect the live process/session and checkpoint first. A polling +timeout does not mean that the process stopped; do not start another apply. +If the process has actually terminated with incomplete acceptance, retain that +failed/unverified attempt and use the receipt-bound rollback: + +```sh +python3 docs/changes/CUST-WP-0080/expand_forge_profiles.py \ + --proposal docs/changes/CUST-WP-0080/forge-expansion-proposal.json \ + --inventory /tmp/forge-coverage-expansion-20261013.json \ + --receipt docs/evidence/2026-10-13-netkingdom-security/forge-expansion-activation.json \ + --rollback +``` + +The rollback removes only exact owned additions on attempted IDs and verifies +the original generated-hook inventory. Successful finalized receipt rollback +uses its exact hashes after ownership-link cleanup. A changed pod or artifact +requires a reviewed revision; never edit the original proposal to replay it. +The abort owner is railiance-forge, with custodian-codex executing this trial. diff --git a/docs/changes/CUST-WP-0080/forge-expansion-proposal.json b/docs/changes/CUST-WP-0080/forge-expansion-proposal.json new file mode 100644 index 0000000..66139e5 --- /dev/null +++ b/docs/changes/CUST-WP-0080/forge-expansion-proposal.json @@ -0,0 +1,178 @@ +{ + "proposal_id": "FORGE6-EXPAND-001", + "revision": 1, + "pod_uid": "d4728052-b8df-4574-8a3e-74490a04472d", + "inventory_sha256": "8ec5bd3b06dba517d86b276d301d9bbcbf6ad9f706c422469f553f420aca4057", + "target_ids": [ + 1, + 6, + 7, + 8, + 10, + 11, + 12, + 13, + 14, + 15, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 56, + 57, + 58, + 59, + 61, + 62, + 63, + 64, + 65, + 66, + 68, + 69, + 70, + 72, + 73, + 74, + 75, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 103, + 104, + 105, + 106, + 107, + 109, + 110, + 112, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 125, + 126, + 127, + 128, + 130, + 132, + 133, + 134, + 135, + 136, + 140, + 141, + 142, + 143, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 158, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171 + ], + "target_count": 143, + "excluded_head_gitlink_ids": [ + 55 + ], + "veto_name": "zz-custodian-expansion-veto", + "veto_sha256": "f4b6710277af845b6c68ee45ba23720f33cc34c731238a9135e277da7ac6bb39", + "probe_ref": "refs/heads/custodian-g07-probe-20261013-expansion", + "fixture_key": "/tmp/forge-expansion-synthetic-20261013", + "fixture_sha256": "8d4f4fcc8c602572bd68b02c79243a9e178187a29013d900ce25842e88634564", + "executor_hashes": { + "expand_forge_profiles.py": "15d111cc5a8897fff992f304c6b72ebbbe8e758b2fc3d949a0df8ac0d09848c0", + "activate_forge_template.py": "ef2b38f655d5520d69dcd69cdb4997eaad4b50773f32c1d798d16331b204c789", + "activate_receive_hooks.py": "9b01ee383e16463981112226e19156b0de59af2d03dd71b9403e144e280a9fb3", + "verify_receive_native.py": "c9120122c6f6f0b7c41fe4fa975e5a2a340e776885d852b861b8c13b7664adcd", + "receive-profile-hook": "b1d7cf8cd13feafe44e45876b1de3fdd9c88339d8d45791fa2b9f72ca832932b", + "receive-probe-veto-expansion": "f4b6710277af845b6c68ee45ba23720f33cc34c731238a9135e277da7ac6bb39" + }, + "authorization_ref": "canon/standards/build-mode-change-authorization_v0.1.md", + "diff_ref": "Existing admitted receive-profile-hook added to143exact builtin-only repositories. Exact inventory and default-HEAD compatibility preflight; no generated hook replacement.", + "rollback": "Same executor --rollback with exact proposal, private inventory and apply receipt. Removes only exact installed hook/veto hashes on attempted IDs; automatic recovery additionally requires original ownership hard-links. Preserve all preexisting generated hooks. Unknown concurrent changes abort removal, retain unverified outcome for abort owner.", + "abort_owner": "railiance-forge; executor custodian-codex", + "receipt_path": "docs/evidence/2026-10-13-netkingdom-security/forge-expansion-activation.json", + "expected_result": "All143native targets have exact original generated hooks plus admitted profile. Each actual SSH clean and synthetic secret probe emits correct scanner result under a ref-specific veto; no probe refs/quarantined commits remain; vetoes and inert ownership links removed. Existing23profiles untouched. One incompatible gitlink repository remains explicit work, not an exemption.", + "cost_estimate_eur": 0, + "risk_estimate": "Additive reversible hook configuration, no rollout/new resources or identity/policy scope expansion. Probe traffic is serialized and scoped to a temporary ref; veto leaves normal refs unaffected. Unsupported or secret-bearing writes may refuse by design. Limits and initial-history/import coverage remain explicit. Abort on precondition or acceptance failure and roll back attempted cohort; no actual credential content used.", + "verification_limit": "Default-HEAD compatibility is not all-history scanning; clean scanner verdict under veto is not ordinary accepted source push. Current identities used for SSH only. Production HTTP/SSO/frontend, gitlink compatibility, imports/CI/response remain open.", + "promotion_evidence": false +} diff --git a/docs/changes/CUST-WP-0080/inventory_forge_coverage.py b/docs/changes/CUST-WP-0080/inventory_forge_coverage.py new file mode 100644 index 0000000..a5e7137 --- /dev/null +++ b/docs/changes/CUST-WP-0080/inventory_forge_coverage.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Read-only database/directory/hook reconciliation; commit aggregates only.""" +import argparse +import datetime +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import re +import shlex + +HERE = Path(__file__).parent +s=importlib.util.spec_from_file_location('template', HERE/'activate_forge_template.py') +t=importlib.util.module_from_spec(s) +s.loader.exec_module(t) + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--private-output',type=Path,required=True) + parser.add_argument('--receipt',type=Path,required=True) + args=parser.parse_args() + original=json.loads((HERE/'receive-hook-proposal.json').read_text()) + pod=t.target_pod(t.deployment(),original) + t.check_artifacts(pod) + for artifact in original['artifacts'][:2]: + mode=t.native.execute(pod,'stat','-c','%a %u %g',original['version_directory']+'/'+artifact['name']).decode().strip() + if mode!='755 1000 1000':raise ValueError('artifact-custody-changed') + query=('SELECT json_agg(row_to_json(x)) FROM (SELECT r.id,u.lower_name AS owner,r.lower_name AS name,' + 'r.is_private,r.is_empty,r.is_archived,r.is_mirror,r.is_fork,r.created_unix,r.git_size,r.lfs_size ' + 'FROM repository r JOIN public.user u ON u.id=r.owner_id ORDER BY r.id) x') + raw=t.native.command(['-n','databases','exec','forgejo-db-1','--','psql','-U','postgres','-d','forgejo','-At','-c',query]) + rows=json.loads(raw) + statements=['set -euo pipefail'] + for r in rows: + if not isinstance(r['id'],int) or any(not re.fullmatch(r'[a-z0-9_.-]+',r[k]) or r[k] in ('.','..') for k in ('owner','name')): + raise ValueError('repository-identity-refused') + path='/data/git/gitea-repositories/'+r['owner']+'/'+r['name']+'.git' + statements.extend(['repo='+shlex.quote(path), 'printf "REPO '+str(r['id'])+'\\n"', + 'test -d "$repo" && test ! -L "$repo" && test ! -L "$repo/hooks" && test ! -L "$repo/hooks/pre-receive.d"', + 'test -z "$(find "$repo/hooks" -maxdepth 2 -type l -print -quit)"', + 'find "$repo/hooks" -maxdepth 2 -type f ! -name "*.sample" -exec sha256sum {} \\;', + 'printf "SHALLOW "; git --git-dir="$repo" rev-parse --is-shallow-repository', + 'if git --git-dir="$repo" config --local --includes --name-only --list | grep -Eiq "(^extensions.partialclone$|^remote\\..*\\.(promisor|partialclonefilter)$)"; then printf "PARTIAL true\\n"; else printf "PARTIAL false\\n"; fi', + 'if git --git-dir="$repo" rev-parse --verify HEAD >/dev/null 2>&1; then printf "GITLINKS "; git --git-dir="$repo" ls-tree -r --format="%(objectmode)" HEAD | awk \'$0 == "160000" { n++ } END { print n+0 }\'; else printf "GITLINKS unborn\\n"; fi']) + observed=t.native.execute(pod,'bash','-s',payload=('\n'.join(statements)+'\n').encode(),timeout=180).decode() + by_id={r['id']:r for r in rows}; current=None + for line in observed.splitlines(): + if line.startswith('REPO '): + current=by_id[int(line[5:])]; current['hooks']={} + elif line.startswith('SHALLOW '):current['shallow']=line[8:]=='true' + elif line.startswith('PARTIAL '):current['partial']=line[8:]=='true' + elif line.startswith('GITLINKS '):current['head_gitlinks']=None if line[9:]=='unborn' else int(line[9:]) + else: + digest,path=line.split(' ',1) + prefix='/data/git/gitea-repositories/'+current['owner']+'/'+current['name']+'.git/hooks/' + if not re.fullmatch(r'[a-f0-9]{64}',digest) or not path.startswith(prefix):raise ValueError('unexpected-hook-output') + current['hooks'][path[len(prefix):]]=digest + default=original['existing_hook_hashes']; admitted=dict(default) + admitted['pre-receive.d/'+original['hook_name']]=original['hook_sha256'] + for r in rows: + r['profile']='admitted' if r['hooks']==admitted else 'builtin-only' if r['hooks']==default else 'unknown' + r['head_compatible']=not r['shallow'] and not r['partial'] and r['head_gitlinks'] in (None,0) + directories=t.native.execute(pod,'find','/data/git/gitea-repositories','-mindepth','2','-maxdepth','2','-type','d','-name','*.git').decode().splitlines() + expected={'/data/git/gitea-repositories/'+r['owner']+'/'+r['name']+'.git' for r in rows} + if set(directories)!=expected:raise ValueError('database-directory-mismatch') + private={'observed_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'pod_uid':pod['pod_uid'],'repositories':rows} + content=(json.dumps(private,indent=2)+'\n').encode() + fd=os.open(args.private_output,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600) + with os.fdopen(fd,'wb') as output:output.write(content) + receipt={'result':'reconciled','observed_at':private['observed_at'],'pod_uid':pod['pod_uid'], + 'repositories':len(rows),'repository_directories':len(directories), + 'profiles':{kind:sum(r['profile']==kind for r in rows) for kind in ('admitted','builtin-only','unknown')}, + 'builtin_only_head_compatible':sum(r['profile']=='builtin-only' and r['head_compatible'] for r in rows), + 'repositories_with_head_gitlinks':sum(bool(r['head_gitlinks']) for r in rows), + 'shallow':sum(r['shallow'] for r in rows),'partial':sum(r['partial'] for r in rows), + 'mirrors':sum(r['is_mirror'] for r in rows),'private':sum(r['is_private'] for r in rows), + 'lfs_nonzero_size':sum(bool(r['lfs_size']) for r in rows), + 'private_inventory_sha256':hashlib.sha256(content).hexdigest(), + 'scope':'Current database/directory reconciliation, exact hook content, shallow/partial and default-HEAD gitlink compatibility; not historical scanning or authenticated route acceptance.'} + args.receipt.write_text(json.dumps(receipt,indent=2)+'\n') + print(json.dumps(receipt)) + + +if __name__=='__main__':main() diff --git a/docs/changes/CUST-WP-0080/probe_push_deadline.py b/docs/changes/CUST-WP-0080/probe_push_deadline.py new file mode 100644 index 0000000..11842e6 --- /dev/null +++ b/docs/changes/CUST-WP-0080/probe_push_deadline.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Real disposable receive hooks outlasting both historical client deadlines.""" +from concurrent.futures import ThreadPoolExecutor +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import tempfile +import time + +SOURCES = { + 'state-hub': Path('/home/worsch/state-hub/scripts/repo_sync.py'), + 'repo-manager': Path('/home/worsch/repo-manager/src/repo_manager/gitops.py'), +} + + +def probe(item): + name, source = item + spec = importlib.util.spec_from_file_location(name, source) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + with tempfile.TemporaryDirectory(prefix='push-deadline-') as temporary: + base = Path(temporary) + remote, local = base / 'remote.git', base / 'client' + + def git(*args): + p = subprocess.run(['git', *map(str, args)], capture_output=True, timeout=15) + if p.returncode: + raise ValueError('probe-git-failed') + return p.stdout.decode().strip() + + git('init', '--bare', '-q', remote) + git('clone', '-q', remote, local) + git('-C', local, 'config', 'user.name', 'Deadline probe') + git('-C', local, 'config', 'user.email', 'probe@example.test') + git('-C', local, 'config', 'core.hooksPath', base / 'no-client-hooks') + git('-C', local, 'commit', '--allow-empty', '-qm', 'isolated deadline fixture') + hook = remote / 'hooks/pre-receive' + hook.write_text('#!/bin/sh\nsleep 65\nexit 0\n') + hook.chmod(0o755) + start = time.monotonic() + ok, _ = module.push_ff(local) + elapsed = time.monotonic() - start + head = git('-C', local, 'rev-parse', 'HEAD') + remote_head = git('--git-dir', remote, 'rev-parse', 'HEAD') + accepted = ok and head == remote_head and elapsed >= 65 + hook.write_text('#!/bin/sh\nexit 1\n') + git('-C', local, 'commit', '--allow-empty', '-qm', 'isolated rejection fixture') + rejected, _ = module.push_ff(local) + refusal = not rejected and git('--git-dir', remote, 'rev-parse', 'HEAD') == head + return {'repo': name, 'source_sha256': hashlib.sha256(source.read_bytes()).hexdigest(), + 'configured_seconds': module.PUSH_TIMEOUT_SECONDS, 'elapsed_seconds': round(elapsed, 2), + 'receive_hook_65_seconds_accepted': accepted, 'refusal_preserved_ref': refusal} + + +if __name__ == '__main__': + os.environ.update(GIT_CONFIG_GLOBAL='/dev/null', GIT_CONFIG_NOSYSTEM='1', GIT_TERMINAL_PROMPT='0') + with ThreadPoolExecutor(max_workers=2) as pool: + results = list(pool.map(probe, SOURCES.items())) + passed = all(x['receive_hook_65_seconds_accepted'] and x['refusal_preserved_ref'] for x in results) + receipt = {'result': 'verified' if passed else 'failed', 'checks': results, + 'scope': 'Disposable local Git repositories; synthetic delayed/refusing receive hooks, no production delay injected.'} + Path('docs/evidence/2026-10-13-netkingdom-security/push-deadline.json').write_text(json.dumps(receipt, indent=2) + '\n') + print(json.dumps(receipt)) + assert passed diff --git a/docs/changes/CUST-WP-0080/receive-probe-veto-expansion b/docs/changes/CUST-WP-0080/receive-probe-veto-expansion new file mode 100644 index 0000000..6340def --- /dev/null +++ b/docs/changes/CUST-WP-0080/receive-probe-veto-expansion @@ -0,0 +1,11 @@ +#!/bin/sh +set +vx +set -eu +blocked=0 +while read -r old new ref; do + if [ "$ref" = "refs/heads/custodian-g07-probe-20261013-expansion" ]; then blocked=1; fi +done +if [ "$blocked" = 1 ]; then + echo CUSTODIAN_G07_PROBE_VETO >&2 + exit 1 +fi diff --git a/docs/changes/CUST-WP-0080/test_expand_forge_profiles.py b/docs/changes/CUST-WP-0080/test_expand_forge_profiles.py new file mode 100644 index 0000000..cd2d6ce --- /dev/null +++ b/docs/changes/CUST-WP-0080/test_expand_forge_profiles.py @@ -0,0 +1,57 @@ +import hashlib +import importlib.util +from pathlib import Path +import subprocess +import tempfile +import unittest + +s=importlib.util.spec_from_file_location('expand',Path(__file__).with_name('expand_forge_profiles.py')) +m=importlib.util.module_from_spec(s) +s.loader.exec_module(m) + + +class AtomicOwnership(unittest.TestCase): + def setUp(self): + self.tmp=tempfile.TemporaryDirectory() + self.path=Path(self.tmp.name)/'repo/hooks/pre-receive.d/10-test' + self.path.parent.mkdir(parents=True) + self.body=b'#!/bin/sh\nexit 0\n' + self.digest=hashlib.sha256(self.body).hexdigest() + self.saved=m.t.native.execute + + def execute(pod,*args,payload=None,**kwargs): + result=subprocess.run(args,input=payload,capture_output=True,timeout=5) + if result.returncode:raise ValueError('refused') + return result.stdout + + m.t.native.execute=execute + + def tearDown(self): + m.t.native.execute=self.saved + self.tmp.cleanup() + + def test_install_and_owned_rollback(self): + m.install({},str(self.path),self.body,self.digest) + self.assertEqual(self.path.read_bytes(),self.body) + self.assertEqual(self.path.stat().st_mode&0o777,0o755) + m.remove_owned({},str(self.path),self.digest,require_marker=True) + self.assertFalse(self.path.exists()) + + def test_existing_destination_never_overwritten(self): + self.path.write_bytes(b'preexisting') + with self.assertRaises(ValueError):m.install({},str(self.path),self.body,self.digest) + self.assertEqual(self.path.read_bytes(),b'preexisting') + + def test_changed_content_not_removed(self): + m.install({},str(self.path),self.body,self.digest) + self.path.write_bytes(b'changed') + with self.assertRaises(ValueError):m.remove_owned({},str(self.path),self.digest,require_marker=True) + self.assertTrue(self.path.exists()) + + def test_identical_unowned_file_not_removed_during_recovery(self): + self.path.write_bytes(self.body) + with self.assertRaises(ValueError):m.remove_owned({},str(self.path),self.digest,require_marker=True) + self.assertTrue(self.path.exists()) + + +if __name__=='__main__':unittest.main() diff --git a/docs/evidence/2026-10-13-netkingdom-security/forge-expansion-inventory.json b/docs/evidence/2026-10-13-netkingdom-security/forge-expansion-inventory.json new file mode 100644 index 0000000..1a0db05 --- /dev/null +++ b/docs/evidence/2026-10-13-netkingdom-security/forge-expansion-inventory.json @@ -0,0 +1,21 @@ +{ + "result": "reconciled", + "observed_at": "2026-10-13T14:40:09.875180+00:00", + "pod_uid": "d4728052-b8df-4574-8a3e-74490a04472d", + "repositories": 167, + "repository_directories": 167, + "profiles": { + "admitted": 23, + "builtin-only": 144, + "unknown": 0 + }, + "builtin_only_head_compatible": 143, + "repositories_with_head_gitlinks": 1, + "shallow": 0, + "partial": 0, + "mirrors": 0, + "private": 12, + "lfs_nonzero_size": 0, + "private_inventory_sha256": "8ec5bd3b06dba517d86b276d301d9bbcbf6ad9f706c422469f553f420aca4057", + "scope": "Current database/directory reconciliation, exact hook content, shallow/partial and default-HEAD gitlink compatibility; not historical scanning or authenticated route acceptance." +} diff --git a/docs/evidence/2026-10-13-netkingdom-security/forge-expansion-preflight.json b/docs/evidence/2026-10-13-netkingdom-security/forge-expansion-preflight.json new file mode 100644 index 0000000..d33981b --- /dev/null +++ b/docs/evidence/2026-10-13-netkingdom-security/forge-expansion-preflight.json @@ -0,0 +1,10 @@ +{ + "proposal_id": "FORGE6-EXPAND-001", + "proposal_sha256": "508d540e578b8cb93a187de644f15d5ef404e2c0a90fdb96ef22cdbb2bb94c78", + "pod_uid": "d4728052-b8df-4574-8a3e-74490a04472d", + "result": "preflight-verified", + "target_count": 143, + "attempted_ids": [], + "verified_ids": [], + "at": "2026-10-13T14:47:17.699066+00:00" +} diff --git a/docs/evidence/2026-10-13-netkingdom-security/forge-gitlink-binding.json b/docs/evidence/2026-10-13-netkingdom-security/forge-gitlink-binding.json new file mode 100644 index 0000000..5b523b6 --- /dev/null +++ b/docs/evidence/2026-10-13-netkingdom-security/forge-gitlink-binding.json @@ -0,0 +1,27 @@ +{ + "repository_id": 55, + "gitlink_count": 4, + "module_metadata": "inspected-with-value-free-output", + "targets": [ + { + "path_has_config": true, + "url_has_userinfo": false, + "target_is_managed_forge_repository": false + }, + { + "path_has_config": true, + "url_has_userinfo": false, + "target_is_managed_forge_repository": false + }, + { + "path_has_config": true, + "url_has_userinfo": false, + "target_is_managed_forge_repository": false + }, + { + "path_has_config": true, + "url_has_userinfo": false, + "target_is_managed_forge_repository": false + } + ] +} diff --git a/docs/evidence/2026-10-13-netkingdom-security/push-deadline.json b/docs/evidence/2026-10-13-netkingdom-security/push-deadline.json new file mode 100644 index 0000000..fee6786 --- /dev/null +++ b/docs/evidence/2026-10-13-netkingdom-security/push-deadline.json @@ -0,0 +1,78 @@ +{ + "result": "verified", + "checks": [ + { + "repo": "state-hub", + "source_sha256": "e02ea7983ce5accd0f5b0a35bbe6dba5572ec36b5b51cfd5c60e337146e3fda2", + "configured_seconds": 120, + "elapsed_seconds": 65.04, + "receive_hook_65_seconds_accepted": true, + "refusal_preserved_ref": true + }, + { + "repo": "repo-manager", + "source_sha256": "b337b8d3c584e218901143e89a14bc1c27403357bc4c212eea39b52fc999860b", + "configured_seconds": 120, + "elapsed_seconds": 65.04, + "receive_hook_65_seconds_accepted": true, + "refusal_preserved_ref": true + } + ], + "scope": "Disposable local Git repositories; synthetic delayed/refusing receive hooks, no production delay injected.", + "tests": { + "state_hub": 34, + "repo_manager": 36 + }, + "installed_cli": { + "state_hub_source_matches": true, + "state_hub_deadline_seconds": 120, + "repo_manager_editable_source": true + }, + "owner_sync": { + "state-hub": { + "commit": "f6839b52360629e0697151cce127a870df0474b9", + "status": "applied", + "expected_commit": "f6839b52360629e0697151cce127a870df0474b9", + "derived_commit": "f6839b52360629e0697151cce127a870df0474b9", + "counts": { + "created": 1, + "updated": 67, + "retired": 0, + "refused": 0, + "released": 0, + "created_tasks": 2, + "updated_tasks": 2, + "cancelled_tasks": 0, + "created_edges": 0, + "removed_edges": 2, + "unresolved_edges": 0, + "external_edges": 3 + } + }, + "repo-manager": { + "commit": "5a7e838d343b3507856bf65b2a9ba8b8aa4b53d8", + "status": "applied", + "expected_commit": "5a7e838d343b3507856bf65b2a9ba8b8aa4b53d8", + "derived_commit": "5a7e838d343b3507856bf65b2a9ba8b8aa4b53d8", + "counts": { + "created": 0, + "updated": 25, + "retired": 0, + "refused": 0, + "released": 0, + "created_tasks": 0, + "updated_tasks": 1, + "cancelled_tasks": 0, + "created_edges": 0, + "removed_edges": 0, + "unresolved_edges": 0, + "external_edges": 0 + } + } + }, + "closed_workplan": "STATE-WP-ADHOC-2026-10-13", + "hub_readback": { + "workplan": "finished", + "task": "done" + } +} diff --git a/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md b/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md index 56c6e76..a31144a 100644 --- a/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md +++ b/workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md @@ -283,6 +283,30 @@ Evidence: `forge-custody-repair.json`, `forge-custody-rollout.json` and `forge-custody-restart.json` in the October 13 evidence directory. This resolves an availability regression without weakening scanner refusal or widening access. +2026-10-13 push compatibility closed: STATE-WP-ADHOC-2026-10-13-T01 is +finished/done in file and hub. State Hub `7183f59` (identifier projection commit +`f6839b5`) and Repo Manager `5a7e838` extend client push deadlines to 120 seconds +for the admitted server scanner's 90-second budget. Timeouts report unverified +remote outcome without retry or child diagnostic disclosure. Seventy targeted +tests and two real 65-second receive-hook acceptance/refusal sequences pass; +the installed State Hub packaged module matches source. Both owner projections +applied against exact pushed commits. Receipt: `push-deadline.json`. + +Fresh read-only inventory now reconciles 167 database repositories/directories: +23 admitted profiles, 144 builtin-only, zero unknown hooks. A repository created +since the earlier inventory inherited the exact hook. One builtin-only repository +has four external submodule references; 143 others pass default-HEAD compatibility. +Exact proposal FORGE6-EXPAND-001 is executing serialized additive activation and +actual SSH clean/synthetic refusal probes under a ref-specific veto. Its current +receipt records attempted and verified numeric IDs; do not infer full acceptance +until `result: verified` and cleanup are confirmed. The submodule case remains +required work, not an exemption. Original 23 profiles and generated dispatchers +are preserved. Private scope mapping and synthetic fixture are retained under +ignored mode-0700 `.private-backups/forge-expansion-20261013/`; their digests are +bound in the proposal. No credential value is committed. Evidence: +`forge-expansion-inventory.json`, `forge-gitlink-binding.json`, and the pending +`forge-expansion-activation.json` in the October 13 directory. + ## Verify native closure against every requirement ```task