security: close sync deadline fix and checkpoint scoped forge expansion
This commit is contained in:
parent
8f770e1eea
commit
9af8689327
13 changed files with 797 additions and 0 deletions
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
169
docs/changes/CUST-WP-0080/expand_forge_profiles.py
Normal file
169
docs/changes/CUST-WP-0080/expand_forge_profiles.py
Normal file
|
|
@ -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()
|
||||
46
docs/changes/CUST-WP-0080/forge-expansion-operations.md
Normal file
46
docs/changes/CUST-WP-0080/forge-expansion-operations.md
Normal file
|
|
@ -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.
|
||||
178
docs/changes/CUST-WP-0080/forge-expansion-proposal.json
Normal file
178
docs/changes/CUST-WP-0080/forge-expansion-proposal.json
Normal file
|
|
@ -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
|
||||
}
|
||||
86
docs/changes/CUST-WP-0080/inventory_forge_coverage.py
Normal file
86
docs/changes/CUST-WP-0080/inventory_forge_coverage.py
Normal file
|
|
@ -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()
|
||||
67
docs/changes/CUST-WP-0080/probe_push_deadline.py
Normal file
67
docs/changes/CUST-WP-0080/probe_push_deadline.py
Normal file
|
|
@ -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
|
||||
11
docs/changes/CUST-WP-0080/receive-probe-veto-expansion
Normal file
11
docs/changes/CUST-WP-0080/receive-probe-veto-expansion
Normal file
|
|
@ -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
|
||||
57
docs/changes/CUST-WP-0080/test_expand_forge_profiles.py
Normal file
57
docs/changes/CUST-WP-0080/test_expand_forge_profiles.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue