Record approved scanner revision 5 and verified cached-stream correction
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Reject generated-only commits / generated-commit-guard (push) Successful in 13s

This commit is contained in:
codex 2026-11-02 11:31:44 +01:00
parent fa81fcf88d
commit dfd7ee5f30
25 changed files with 10369 additions and 23 deletions

View file

@ -37,3 +37,11 @@ The founder executed approved revision3: the missing-fragment repair and171-repo
Founder approval recorded for exact revision4 packet `64c73100f2347470fda1b8b3194dbf6a8009291d9d80223af463b3c5ecab52fd` via User: Approve exact revision4; request_user_input_async call_f68b7e7de81a4c089e8864b3d66773a9 item0,2026-11-02. Attended execution pending at `/tmp/forge-scanner-rollout-r4.sh`; approval receipt `docs/changes/2026-11-01-scanner-json-digest/native-r4-approval.json`. Earlier failures and exact approvals remain preserved; no execution success is inferred from approval.
Current revision4 execution attempt failed at first get:nodes, before mutation. Packet/approval are unchanged and its failed receipt is preserved in `attempt-r4-access/`. The local bridge/API transport responds, public server certificate is not expired, and a tested read-only access classifier is ready at `bash /tmp/forge-scanner-access-diagnose.sh`. Await selected founder-context status before any unchanged retry; no renewed approval is requested.
## Current revision5 proposal
Revision4's first access failure was followed by successful expected-node/admin diagnostics; the failed receipt was preserved before an unchanged approved retry. That retry passed preflight/dry-run/live tool self-test but cached staging timed out. Old-policy rollback/probes/metadata verified across171. Read-only native tests isolated unread input: empty input completed, full-input cached verification reached success but clients stalled on both protocols, and consuming/counting the full input completed both.
[Revision5](native-r5/PLAN.md), packet SHA256 `95c2724ea65ee3f4ce42b96bbb2b839b02169d62faa4c0a9a6c320cf65754643`, adds only cached-stage input consumption/counting and a fresh receipt/source gate while reusing all sealed revision4 controls. Four pinned-image staging cases and two envelope tests passed; live read-only drain controls passed both protocols. Same binary/profile and six source hashes. Native revision5 awaits exact review via `/tmp/forge-scanner-rollout-r5.sh`; no new source/detector exception or deployment-field change.
Exact revision5 packet `95c2724ea65ee3f4ce42b96bbb2b839b02169d62faa4c0a9a6c320cf65754643` approved by founder via User: Approve exact revision5; request_user_input_async call_dc0f52ef1e344100916de075e794e113 item0,2026-11-02. Approval receipt: `docs/changes/2026-11-01-scanner-json-digest/native-r5-approval.json`; ADMINISTER @ realm:kubernetes/railiance01, activation=APPROVED. Native execution pending at `/tmp/forge-scanner-rollout-r5.sh`. All earlier failed outcomes remain preserved; approval is not verified success.

View file

@ -0,0 +1,25 @@
{
"read_only": true,
"native_mutation": false,
"rollout_packet_sha256": "64c73100f2347470fda1b8b3194dbf6a8009291d9d80223af463b3c5ecab52fd",
"catalog_route": "kubernetes-railiance01-admin",
"admin_file_present": true,
"admin_file_readable": true,
"checks": [
{
"operation": "get-nodes",
"returncode": 0,
"category": "passed",
"node_count": 1,
"target_node_matches": true
},
{
"operation": "self-identity",
"returncode": 0,
"category": "passed",
"self_identity_read": true,
"identity_is_builder": false,
"identity_in_system_masters": true
}
]
}

View file

@ -0,0 +1,13 @@
{
"proposal_id": "SYNC-SCANNER-JSON-DIGEST-20261101",
"revision": 4,
"packet_sha256": "64c73100f2347470fda1b8b3194dbf6a8009291d9d80223af463b3c5ecab52fd",
"approved_code_unchanged": true,
"failed_attempt_native_mutation": false,
"failed_receipt_sha256": "8c876dd2ac8d21a1c4b2a1823802f8a318328d8e16ebb7191160d87525bd50f6",
"failed_receipt_archive": "/tmp/forge-scanner-rollout-r4-get-nodes-failed-20261102T092654Z.json",
"archive_verified": true,
"access_now_matches_target_and_admin": true,
"renewed_approval_requested": false,
"ready_for_unchanged_founder_retry": true
}

View file

@ -0,0 +1,21 @@
{
"read_only": true,
"native_mutation": false,
"rollout_packet_sha256": "64c73100f2347470fda1b8b3194dbf6a8009291d9d80223af463b3c5ecab52fd",
"synthetic_payload_bytes": 3437906,
"input_count_required": true,
"checks": [
{
"protocol": "WebSocket",
"returncode": 0,
"category": "completed",
"cache_and_full_input_verified": true
},
{
"protocol": "SPDY",
"returncode": 0,
"category": "completed",
"cache_and_full_input_verified": true
}
]
}

View file

@ -0,0 +1,36 @@
"""Read-only confirmation: consume and count synthetic input before returning."""
from pathlib import Path
import importlib.util,json,os,subprocess
HERE=Path(__file__).resolve().parent
s=importlib.util.spec_from_file_location('stream_probe',HERE/'diagnose_stream.py');d=importlib.util.module_from_spec(s);s.loader.exec_module(d)
OUTPUT=Path('/tmp/forge-scanner-drain-diagnostic-receipt.json')
def main():
s=importlib.util.spec_from_file_location('reviewed_r4',d.R4/'rollout.py');m=importlib.util.module_from_spec(s);s.loader.exec_module(m)
if m.verify_packet()!=d.EXPECTED:raise RuntimeError('approved-packet-drift')
c=json.loads((d.R4.parent/'native-r2/inputs.json').read_bytes());env=dict(os.environ,KUBECONFIG='/home/worsch/.kube/config-railiance01')
for kind,name,uid in [('deployment',c['deployment_name'],c['deployment_uid']),('pod',c['pod'],c['pod_uid'])]:
q=subprocess.run(['kubectl','--request-timeout=8s','-n','forgejo','get',kind,name,'-o','jsonpath={.metadata.uid}'],capture_output=True,timeout=12,env=env)
if q.returncode or q.stdout.decode()!=uid:raise RuntimeError('target-read-or-identity-failed; raw withheld')
payload=bytes(Path('/tmp/sync-scanner-candidate/receive-secret-scan-candidate').stat().st_size)
script=d.check_script(c);needle="printf '{\"cached_artifacts_verified\":true}\\n'"
if script.count(needle)!=1:raise RuntimeError('probe-script-shape-drift')
script=script.replace(needle,'received=$(wc -c)\ntest "$received" -eq "$5"\n'+needle)
result={'read_only':True,'native_mutation':False,'rollout_packet_sha256':d.EXPECTED,'synthetic_payload_bytes':len(payload),'input_count_required':True,'checks':[]}
for protocol in ['true','false']:
local=dict(env,KUBECTL_REMOTE_COMMAND_WEBSOCKETS=protocol)
argv=['kubectl','--request-timeout=8s','-n','forgejo','exec','-i',c['pod'],'-c',c['container'],'--','sh','-c',script,'sh',c['old_dir'],c['new_dir'],c['new_adapter_sha256'],c['scanner_sha256'],str(len(payload))]
try:
q=subprocess.run(argv,input=payload,capture_output=True,timeout=12,env=local)
row={'protocol':'WebSocket' if protocol=='true' else 'SPDY','returncode':q.returncode,'category':'completed' if q.returncode==0 else 'command-failed','cache_and_full_input_verified':d.marker(q.stdout)}
except subprocess.TimeoutExpired as e:row={'protocol':'WebSocket' if protocol=='true' else 'SPDY','category':'client-timeout','cache_and_full_input_verified':d.marker(e.stdout)}
result['checks'].append(row);OUTPUT.write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(row),flush=True)
print('receipt:',str(OUTPUT))
if __name__=='__main__':
try:main()
except Exception as e:
print(json.dumps({'result':'read-only-diagnostic-refused','reason':str(e) if isinstance(e,RuntimeError) else type(e).__name__}))
raise SystemExit(1)

View file

@ -0,0 +1,77 @@
"""Founder-run read-only cached-artifact and exec-stream comparison.
Only reads known public scanner artifacts and target identities. The payload is
synthetic zero bytes, never a credential or repository data. No rollout, file
creation, chmod, patch, stage, rollback or receipt reset is performed.
"""
from pathlib import Path
import importlib.util
import json
import os
import subprocess
HERE=Path(__file__).resolve().parent
R4=HERE.parent/'native-r4'
EXPECTED='64c73100f2347470fda1b8b3194dbf6a8009291d9d80223af463b3c5ecab52fd'
OUTPUT=Path('/tmp/forge-scanner-stream-diagnostic-receipt.json')
def marker(raw):
for line in (raw or b'').splitlines():
try:
d=json.loads(line)
if d=={'cached_artifacts_verified':True}:return True
except (ValueError,TypeError):pass
return False
def check_script(c):
return '''set +vx
set -eu
export PATH=/usr/bin:/bin
test "$(id -u)" = 1000
for parent in /data /data/security /data/security/receive-secret-scan "$1" "$2"; do
test -d "$parent" && test ! -L "$parent"
done
verify() {
test -f "$1" && test ! -L "$1"
test "$(stat -c %u "$1")" = 1000
test "$(sha256sum "$1" | cut -d ' ' -f 1)" = "$2"
test "$(stat -c %a "$1")" = 755
}
verify "$1/gitleaks" "$4"
verify "$2/receive-secret-scan" "$3"
verify "$2/gitleaks" "$4"
test -f "$2/.migration-owner" && test ! -L "$2/.migration-owner"
test "$(cat "$2/.migration-owner")" = SYNC-SCANNER-JSON-DIGEST-20261101-R2
printf '{"cached_artifacts_verified":true}\\n'
'''
def main():
spec=importlib.util.spec_from_file_location('approved_r4',R4/'rollout.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
if m.verify_packet()!=EXPECTED:raise RuntimeError('approved-packet-drift')
c=json.loads((R4.parent/'native-r2/inputs.json').read_bytes())
env=dict(os.environ,KUBECONFIG='/home/worsch/.kube/config-railiance01')
for resource,name,uid in [('deployment',c['deployment_name'],c['deployment_uid']),('pod',c['pod'],c['pod_uid'])]:
q=subprocess.run(['kubectl','--request-timeout=8s','-n','forgejo','get',resource,name,'-o','jsonpath={.metadata.uid}'],capture_output=True,timeout=12,env=env)
if q.returncode or q.stdout.decode()!=uid:raise RuntimeError('target-read-or-identity-failed; raw diagnostics withheld')
payload=bytes(Path('/tmp/sync-scanner-candidate/receive-secret-scan-candidate').stat().st_size)
result={'read_only':True,'native_mutation':False,'rollout_packet_sha256':EXPECTED,'synthetic_payload_bytes':len(payload),'checks':[]}
for label,protocol,data in [('empty-default','true',b''),('payload-websocket','true',payload),('payload-spdy','false',payload)]:
probe_env=dict(env,KUBECTL_REMOTE_COMMAND_WEBSOCKETS=protocol)
argv=['kubectl','--request-timeout=8s','-n','forgejo','exec','-i',c['pod'],'-c',c['container'],'--','sh','-c',check_script(c),'sh',c['old_dir'],c['new_dir'],c['new_adapter_sha256'],c['scanner_sha256']]
try:
q=subprocess.run(argv,input=data,capture_output=True,timeout=12,env=probe_env)
item={'probe':label,'protocol':'WebSocket' if protocol=='true' else 'SPDY','returncode':q.returncode,'category':'completed' if q.returncode==0 else 'command-failed','remote_cache_marker_received':marker(q.stdout)}
except subprocess.TimeoutExpired as e:
item={'probe':label,'protocol':'WebSocket' if protocol=='true' else 'SPDY','category':'client-timeout','remote_cache_marker_received':marker(e.stdout)}
result['checks'].append(item);OUTPUT.write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(item),flush=True)
print('receipt:',str(OUTPUT))
if __name__=='__main__':
try:main()
except Exception as e:
print(json.dumps({'result':'read-only-diagnostic-refused','reason':str(e) if isinstance(e,RuntimeError) else type(e).__name__}))
raise SystemExit(1)

View file

@ -0,0 +1,27 @@
{
"read_only": true,
"native_mutation": false,
"rollout_packet_sha256": "64c73100f2347470fda1b8b3194dbf6a8009291d9d80223af463b3c5ecab52fd",
"synthetic_payload_bytes": 3437906,
"checks": [
{
"probe": "empty-default",
"protocol": "WebSocket",
"returncode": 0,
"category": "completed",
"remote_cache_marker_received": true
},
{
"probe": "payload-websocket",
"protocol": "WebSocket",
"category": "client-timeout",
"remote_cache_marker_received": true
},
{
"probe": "payload-spdy",
"protocol": "SPDY",
"category": "client-timeout",
"remote_cache_marker_received": true
}
]
}

View file

@ -0,0 +1,15 @@
{
"proposal_id": "SYNC-SCANNER-JSON-DIGEST-20261101",
"revision": 4,
"attempt": 2,
"packet_sha256": "64c73100f2347470fda1b8b3194dbf6a8009291d9d80223af463b3c5ecab52fd",
"outcome": "artifact-staging timeout; rollback verified",
"failed_receipt_sha256": "901ee9914c24eebb1285d454e7e29a6d20779b4e986d8ad555cdd244c4787c26",
"deployment_patch_executed": false,
"old_policy_coverage_verified": 171,
"source_candidates_applied": false,
"diagnosis": "cached command verified artifacts and returned without consuming sent input; full-input clients stalled on both transports",
"confirmation": "read-only controls consuming/counting3437906 input bytes completed on both transports",
"planned_revision5": "95c2724ea65ee3f4ce42b96bbb2b839b02169d62faa4c0a9a6c320cf65754643",
"prior_access_failure_preserved": true
}

View file

@ -0,0 +1,14 @@
from pathlib import Path
import importlib.util,unittest
p=Path(__file__).resolve().parent/'diagnose_stream.py';s=importlib.util.spec_from_file_location('stream',p);d=importlib.util.module_from_spec(s);s.loader.exec_module(d)
class Tests(unittest.TestCase):
def test_only_exact_public_marker_is_accepted(self):
self.assertTrue(d.marker(b'{"cached_artifacts_verified":true}\n'))
for raw in [None,b'PRIVATE_SENTINEL',b'{"cached_artifacts_verified":false}',b'{"cached_artifacts_verified":true,"extra":"PRIVATE_SENTINEL"}']:
self.assertFalse(d.marker(raw))
def test_probe_does_not_read_input_or_modify_artifacts(self):
script=d.check_script({})
for command in ['chmod ','mkdir ','cp ','mv ','rm ','cat >','cat >/']:
self.assertNotIn(command,script)
self.assertIn('.migration-owner',script)
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,12 @@
{
"proposal_id": "SYNC-SCANNER-JSON-DIGEST-20261101",
"revision": 5,
"packet_sha256": "95c2724ea65ee3f4ce42b96bbb2b839b02169d62faa4c0a9a6c320cf65754643",
"approved_by": "Bernd Worsch",
"approved_at": "2026-11-02T10:27:53.120490+00:00",
"approval_ref": "User: Approve exact revision5; request_user_input_async call_dc0f52ef1e344100916de075e794e113 item0,2026-11-02",
"mode_authority": "ADMINISTER @ realm:kubernetes/railiance01",
"activation": "APPROVED",
"status": "native execution pending",
"prior_failures_preserved": true
}

View file

@ -0,0 +1,38 @@
# Cached-stage input completion, revision 5
Stable proposal `SYNC-SCANNER-JSON-DIGEST-20261101`, revision 5. Revision 4 remains sealed at SHA256 `64c73100f2347470fda1b8b3194dbf6a8009291d9d80223af463b3c5ecab52fd`; its complete scope, controls, estimates and recovery remain the baseline. Revision 5 has not run natively and needs exact review because its cached-staging code changes.
## Verified diagnosis
The first revision-4 access failure made no native changes. Subsequent founder diagnostics authenticated to the expected node with the founder admin group. Its receipt was preserved before the unchanged authorized retry.
The retry passed preflight, server dry-run and live tool self-test, then timed out while sending the binary to the existing-artifact staging command. Rollback verified the old policy/template, all 171 fragments, exact hook metadata and real receiver probes; repaired markitect-main coverage remains intact. No deployment patch or six-file source application occurred.
Read-only tests on the actual pod verified the cached artifacts. With empty input, verification completed normally. With 3,437,906 synthetic input bytes, both WebSocket and SPDY reached successful remote cache verification but the client timed out. When the same read-only check consumed and counted all input before returning, both transports completed successfully and verified the entire byte count. Receipts are sealed here. This confirms the input-handling distinction; no claim about a particular upstream bug is needed.
## Only revised staging behavior
The existing fast path returned without reading the caller's already-sent binary. Revision 5 adds only these lines before the unchanged cached success response:
```sh
received=$(wc -c)
test "$received" -eq "$(stat -c %s "$new/receive-secret-scan")"
```
This completes the input stream and refuses a short/empty upload. Cached binaries still must pass all existing identity/hash/mode/marker checks; the cached path writes no artifact. Fresh staging is byte-for-byte unchanged. The adapter binary, pinned Gitleaks, detection profile, two deployment fields, resource/image/strategy/security-context values and six source postimages are unchanged.
A small reviewed envelope substitutes this one stage script when the sealed revision-4 controller invokes it. Every other controller operation delegates unchanged to revision 4, including live runtime self-test, immutable ConfigMap reuse/UID pinning, portable hook migration, exact known-mode restoration, complete inventories, conditional patch, Ready wait, pre/post receiver probes and guarded rollback. Errors retain the artifact-staging label. Fresh receipt: `/tmp/forge-scanner-rollout-r5-receipt.json`; earlier receipts are not overwritten or removed. Source followthrough uses that receipt and the revision-5 approval digest with the same six exact candidates.
Founder launcher after exact review: `/tmp/forge-scanner-rollout-r5.sh`. The agent does not access the admin kubeconfig. The acknowledgement binds reviewed bytes and grants no separate credential or authority.
## Verification and recovery
Four cases passed in the pinned Forgejo image: fresh staging, cached staging with the complete actual binary, cached empty-input refusal, and cached short-input refusal. Cached binary/scanner hashes remained unchanged. Two envelope tests verify that only the stage call changes, arguments/input/timeouts pass through, and the only stage delta is consumption/counting. Native read-only consuming-input controls passed on both transports. Revision-4's eight image cases, full 171-repository/1,539-hook controller tests, runtime self-test and three controller checks remain applicable; scanner code/binary and source candidates are unchanged.
The same guarded revision-4 rollback restores old deployment fields and old hook bytes/modes, preserves protected content, verifies old-policy probes, and retains the completed coverage repair and all staged public artifacts. Failed/unverified outcomes remain in the same trial. Unknown concurrent drift is retained for founder review; recovery is not guaranteed. Installing coverage does not retroactively admit historical objects or establish full Forgejo write/lifecycle coverage.
## Cost, interruption and authorization
Same estimate as revision 4: infrastructure increment EUR 0, 20–35 attended minutes, expected 1–3 minute Recreate interruption with bounded rollback/probes; not an availability guarantee. Founder time/existing compute and complete security/recovery loss remain unpriced, with illustrative one-hour recovery EUR 100. No autopilot grant, new detector exception, promotion or unchanged-success credit.
The existing revision-4 approval is preserved. This new cached-input behavior needs exact revision-5 review under `docs/agent-autonomy-decision.md`: "Approval is bound to the reviewed proposal revision; changing that revision requires review again."

View file

@ -0,0 +1,21 @@
{
"read_only": true,
"native_mutation": false,
"rollout_packet_sha256": "64c73100f2347470fda1b8b3194dbf6a8009291d9d80223af463b3c5ecab52fd",
"synthetic_payload_bytes": 3437906,
"input_count_required": true,
"checks": [
{
"protocol": "WebSocket",
"returncode": 0,
"category": "completed",
"cache_and_full_input_verified": true
},
{
"protocol": "SPDY",
"returncode": 0,
"category": "completed",
"cache_and_full_input_verified": true
}
]
}

View file

@ -0,0 +1,27 @@
{
"read_only": true,
"native_mutation": false,
"rollout_packet_sha256": "64c73100f2347470fda1b8b3194dbf6a8009291d9d80223af463b3c5ecab52fd",
"synthetic_payload_bytes": 3437906,
"checks": [
{
"probe": "empty-default",
"protocol": "WebSocket",
"returncode": 0,
"category": "completed",
"remote_cache_marker_received": true
},
{
"probe": "payload-websocket",
"protocol": "WebSocket",
"category": "client-timeout",
"remote_cache_marker_received": true
},
{
"probe": "payload-spdy",
"protocol": "SPDY",
"category": "client-timeout",
"remote_cache_marker_received": true
}
]
}

View file

@ -0,0 +1,17 @@
{
"proposal_id": "SYNC-SCANNER-JSON-DIGEST-20261101",
"revision": 5,
"reused_r4_packet_sha256": "64c73100f2347470fda1b8b3194dbf6a8009291d9d80223af463b3c5ecab52fd",
"files": {
"PLAN.md": "09e704df15b760481b35a2d8783dd636266e98e6a16fde1da25f6c081ac8602f",
"live-consuming-stream-check.json": "ae7a71fab9331d3ced8407b521b5d82412017b58e7e5d80878f6302dc10dd1ae",
"live-nonconsuming-stream-check.json": "29fcea52c5259ed313671802c888fe777b117f1a3fec3f21c9c6307223332d05",
"proposal.json": "05795f7c373b131c628ca47e67e0b90a7e5f5055aa2c74c23290fc2b2007e97c",
"rollout.py": "c9cc55a03f8f95aeb4c960ff1f1192530eb2b47b8612ed9255bc2cfe5c8e5b05",
"source_followthrough.py": "6c3188a3bfd5957cca50744c3aa74ca490f880cc484ad3203fdd4a60ff47ac71",
"stage-image-tests.json": "652a98619b4a47e9bc42774f271bb4c95204ccc6f87b808c3d382eb4855796d3",
"stage.sh": "0312638dd9ad4d4c02916d361e85ccbe9bcab7fbcc36339100140b01476ecf76",
"test_envelope.py": "865ca94aff9e2b4ce321b966681360c370014711e6c1bc913a81115b76cfa7c0",
"test_stage_image.py": "5edff56f585267f38bb6cbfe2daf5ca3d5936ddf3ccbedcf2f8a61de160eb5ef"
}
}

View file

@ -0,0 +1,25 @@
{
"proposal_id": "SYNC-SCANNER-JSON-DIGEST-20261101",
"revision": 5,
"original_revision": 1,
"reused_r4_packet_sha256": "64c73100f2347470fda1b8b3194dbf6a8009291d9d80223af463b3c5ecab52fd",
"target": "same railiance01 forgejo/forge and existing scanner fragments; same six source candidates",
"expected_result": "Cached artifact verification consumes/counts the sent binary, then unchanged guarded revision4 activation and six-source followthrough complete",
"verification": "PLAN.md;4 pinned-image staging cases;2 envelope tests;actual read-only input-consumption comparisons passed both transports;unchanged revision4 guard/probe tests",
"rollback": "unchanged revision4 guarded rollback via delegated controller; keep repaired coverage, all artifacts and failed receipts",
"cost_estimate_eur": {
"additional_infrastructure": 0,
"attended_minutes": [
20,
35
],
"founder_time": "unpriced"
},
"risk_estimate_eur": {
"complete_credible_loss_bound": null,
"illustrative_recovery": 100,
"basis": "one hour at illustrative EUR100/hour; full recovery/security loss unpriced, explicit supervision required"
},
"authority": "pending exact revision5 review; not natively executed",
"scanner_and_source_candidates": "unchanged; candidateD7 and same six postimages"
}

View file

@ -0,0 +1,50 @@
"""R5 envelope: consume/count cached-stage stdin; preserve approved R4 behavior."""
from pathlib import Path
import argparse,hashlib,importlib.util,json
HERE=Path(__file__).resolve().parent
BASE=HERE.parent/'native-r4'
OUTPUT=Path('/tmp/forge-scanner-rollout-r5-receipt.json')
R4='64c73100f2347470fda1b8b3194dbf6a8009291d9d80223af463b3c5ecab52fd'
def sha(raw):return hashlib.sha256(raw).hexdigest()
def base_module():
s=importlib.util.spec_from_file_location('approved_r4_library',BASE/'rollout.py');b=importlib.util.module_from_spec(s);s.loader.exec_module(b)
if b.verify_packet()!=R4:raise RuntimeError('reviewed-r4-packet-drift')
return b
def verify_packet():
raw=(HERE/'packet.json').read_bytes();p=json.loads(raw)
for name,digest in p['files'].items():
if Path(name).name!=name or sha((HERE/name).read_bytes())!=digest:raise RuntimeError('r5-packet-drift')
base_module();return sha(raw)
def make_runner(base,module):
class Rollout(base.make_runner(module)):
def shell(self,pod,script,args=(),data=None,timeout=90):
staging=script==(base.ORIGINAL/'stage.sh').read_text()
if staging:script=(HERE/'stage.sh').read_text()
self._cached_stage_call=staging
try:return super().shell(pod,script,args,data,timeout)
finally:self._cached_stage_call=False
def event(self,name,**fields):
if name=='command-failed' and getattr(self,'_cached_stage_call',False):fields['operation']='artifact-staging'
return super().event(name,**fields)
return Rollout
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--approved-digest',required=True)
p.add_argument('--binary',type=Path,default=Path('/tmp/sync-scanner-candidate/receive-secret-scan-candidate'));a=p.parse_args()
digest=verify_packet()
if a.approved_digest!=digest:raise RuntimeError('exact-founder-acknowledgement-required')
b=base_module();m=b.support();m.RECEIPT=OUTPUT;c=json.loads((b.ORIGINAL/'inputs.json').read_bytes())
runner=make_runner(b,m)(c);runner.receipt.update(revision=5,packet_sha256=digest,reused_r4_packet_sha256=R4,mode='apply')
with OUTPUT.open('x') as f:f.write(json.dumps(runner.receipt)+'\n')
runner.apply(a.binary)
if __name__=='__main__':
try:main()
except Exception as e:
print(json.dumps({'result':'refused-or-unverified','reason':str(e) if isinstance(e,RuntimeError) else type(e).__name__}))
raise SystemExit(1)

View file

@ -0,0 +1,87 @@
"""Apply exactly reviewed paired scanner and Apps source after native verification.
No Git operations or native actions. Requires founder acknowledgement of packet.
All six original hashes are checked before writing; differing owner work is refused.
"""
from pathlib import Path
import argparse
import hashlib
import importlib.util
import json
import os
import re
import tempfile
HERE = Path(__file__).resolve().parent
SOURCE = HERE.parent / 'native-r2'
def sha(raw): return hashlib.sha256(raw).hexdigest()
def patched(original, diff):
before = original.decode().splitlines(keepends=True)
result = []; cursor = 0; lines = diff.splitlines(keepends=True); i = 2
while i < len(lines):
match = re.fullmatch(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[^\n]*\n?', lines[i])
if not match: raise ValueError('malformed-hunk')
start = int(match[1]) - 1
if start < cursor: raise ValueError('overlapping-hunk')
result.extend(before[cursor:start]); cursor = start; i += 1
while i < len(lines) and not lines[i].startswith('@@ '):
line = lines[i]; tag, content = line[0], line[1:]
if tag in (' ', '-'):
if cursor >= len(before) or before[cursor] != content: raise ValueError('hunk-preimage-drift')
cursor += 1
if tag in (' ', '+'): result.append(content)
elif tag != '-': raise ValueError('unsupported-patch-line')
i += 1
result.extend(before[cursor:])
return ''.join(result).encode()
def candidates():
changes = json.loads((SOURCE / 'scanner-source-changes.json').read_bytes())
changes += json.loads((SOURCE / 'apps-source-changes.json').read_bytes())
result = []
for change in changes:
target = Path(change['target']); raw = target.read_bytes()
if sha(raw) != change['original_sha256']: raise RuntimeError('owner-source-drift:' + str(target))
candidate = ((SOURCE / change['candidate_file']).read_bytes() if 'candidate_file' in change else patched(raw, change['patch']))
if sha(candidate) != change['candidate_sha256']: raise RuntimeError('candidate-hash-mismatch')
result.append((target, raw, candidate))
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--apply', action='store_true')
parser.add_argument('--approved-digest')
args = parser.parse_args()
spec = importlib.util.spec_from_file_location('rollout', HERE / 'rollout.py')
module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
digest = module.verify_packet()
prepared = candidates()
if args.apply:
if args.approved_digest != digest: raise RuntimeError('exact-founder-acknowledgement-required')
receipt = json.loads(Path('/tmp/forge-scanner-rollout-r5-receipt.json').read_bytes())
if receipt.get('packet_sha256') != digest or receipt['events'][-1]['event'] != 'activation-verified':
raise RuntimeError('native-activation-not-verified')
for target, original, candidate in prepared:
if target.read_bytes() != original: raise RuntimeError('concurrent-owner-source-drift')
mode = target.stat().st_mode & 0o777
fd, name = tempfile.mkstemp(prefix='.scanner-reviewed-', dir=target.parent)
try:
with os.fdopen(fd, 'wb') as stream: stream.write(candidate); stream.flush(); os.fsync(stream.fileno())
os.chmod(name, mode)
os.replace(name, target)
finally:
if os.path.exists(name): os.unlink(name)
print(json.dumps({'result': 'applied' if args.apply else 'preimages-and-candidates-verified', 'files': len(prepared), 'packet_sha256': digest}))
if __name__ == '__main__':
try: main()
except Exception as error:
print(json.dumps({'result': 'refused-or-unverified', 'reason': str(error) if isinstance(error, RuntimeError) else type(error).__name__}))
raise SystemExit(1)

View file

@ -0,0 +1,25 @@
{
"result": "passed",
"image": "code.forgejo.org/forgejo/forgejo@sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba",
"binary_bytes": 3437906,
"cases": [
{
"case": "fresh",
"passed": true
},
{
"case": "cached-full-input",
"passed": true
},
{
"case": "cached-empty-rejected",
"passed": true
},
{
"case": "cached-short-rejected",
"passed": true
}
],
"cached_files_unchanged": true,
"native_mutation": false
}

View file

@ -0,0 +1,61 @@
#!/bin/sh
# Exact public executable staging. No repository/configuration changes.
set +vx
set -eu
PATH=/usr/bin:/bin
export PATH
new=$1 old=$2 adapter_sum=$3 scanner_sum=$4 marker=$5
test "$(id -u)" = 1000
for parent in /data /data/security /data/security/receive-secret-scan "$old"; do
test -d "$parent" && test ! -L "$parent"
done
verify() {
file=$1 sum=$2
test -f "$file" && test ! -L "$file"
test "$(stat -c %u "$file")" = 1000
test "$(sha256sum "$file" | cut -d ' ' -f 1)" = "$sum"
}
verify "$old/gitleaks" "$scanner_sum"
if test -e "$new" || test -L "$new"; then
test -d "$new" && test ! -L "$new"
test "$(cat "$new/.migration-owner")" = "$marker"
verify "$new/receive-secret-scan" "$adapter_sum"
verify "$new/gitleaks" "$scanner_sum"
test "$(stat -c %a "$new/receive-secret-scan")" = 755
test "$(stat -c %a "$new/gitleaks")" = 755
# Finish the exec input stream even when no binary rewrite is needed.
received=$(wc -c)
test "$received" -eq "$(stat -c %s "$new/receive-secret-scan")"
printf '{"stage_verified":true,"already_staged":true}\n'
exit 0
fi
temporary="$new.staging-$$"
test ! -e "$temporary" && test ! -L "$temporary"
umask 077
mkdir "$temporary"
printf '%s' "$marker" > "$temporary/.migration-owner"
cleanup() {
code=$?
trap - EXIT HUP INT TERM
if test -d "$temporary" && test ! -L "$temporary" &&
test "$(cat "$temporary/.migration-owner")" = "$marker"; then
rm -f -- "$temporary/receive-secret-scan" "$temporary/gitleaks" "$temporary/.migration-owner"
rmdir "$temporary" || code=33
fi
exit "$code"
}
trap cleanup EXIT
trap 'exit 34' HUP INT TERM
cat > "$temporary/receive-secret-scan"
cp "$old/gitleaks" "$temporary/gitleaks"
verify "$temporary/receive-secret-scan" "$adapter_sum"
verify "$temporary/gitleaks" "$scanner_sum"
chmod 755 "$temporary/receive-secret-scan" "$temporary/gitleaks" "$temporary"
test ! -e "$new" && test ! -L "$new"
mv -T -- "$temporary" "$new"
test "$(cat "$new/.migration-owner")" = "$marker"
verify "$new/receive-secret-scan" "$adapter_sum"
verify "$new/gitleaks" "$scanner_sum"
test "$(stat -c %a "$new/receive-secret-scan")" = 755
test "$(stat -c %a "$new/gitleaks")" = 755
printf '{"stage_verified":true,"already_staged":false}\n'

View file

@ -0,0 +1,26 @@
from pathlib import Path
from types import SimpleNamespace
import importlib.util,unittest
HERE=Path(__file__).resolve().parent
s=importlib.util.spec_from_file_location('r5',HERE/'rollout.py');r=importlib.util.module_from_spec(s);s.loader.exec_module(r)
ORIGINAL=HERE.parent/'native-r2'
class Tests(unittest.TestCase):
def runner(self):
class Parent:
def __init__(self):self.calls=[];self.events=[]
def shell(self,pod,script,args,data,timeout):
self.calls.append((pod,script,args,data,timeout));return b'public-result'
def event(self,name,**fields):self.events.append((name,fields))
base=SimpleNamespace(ORIGINAL=ORIGINAL,make_runner=lambda m:Parent)
return r.make_runner(base,None)()
def test_only_stage_script_changes_and_call_contract_is_preserved(self):
x=self.runner();out=x.shell('pod',(ORIGINAL/'stage.sh').read_text(),('a','b'),b'public-input',7)
self.assertEqual(out,b'public-result');self.assertEqual(x.calls[0],('pod',(HERE/'stage.sh').read_text(),('a','b'),b'public-input',7))
self.assertFalse(x._cached_stage_call)
x.shell('pod','other reviewed script',('check',),None,9)
self.assertEqual(x.calls[1],('pod','other reviewed script',('check',),None,9))
def test_cached_change_is_only_consumption_and_count_check(self):
before=(ORIGINAL/'stage.sh').read_text();after=(HERE/'stage.sh').read_text()
addition=' # Finish the exec input stream even when no binary rewrite is needed.\n received=$(wc -c)\n test "$received" -eq "$(stat -c %s "$new/receive-secret-scan")"\n'
self.assertEqual(after.replace(addition,''),before)
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,39 @@
"""Fresh/cache staging and strict input count in the pinned Forgejo image."""
from pathlib import Path
import hashlib,json,subprocess
HERE=Path(__file__).resolve().parent
IMAGE='code.forgejo.org/forgejo/forgejo@sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba'
ADAPTER=Path('/tmp/sync-scanner-candidate/receive-secret-scan-candidate')
SCANNER=Path('/home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks')
def main():
binary=ADAPTER.read_bytes();adapter=hashlib.sha256(binary).hexdigest();scanner=hashlib.sha256(SCANNER.read_bytes()).hexdigest()
argv=['docker','run','-d','--rm','--network=none','--user=1000:1000','--read-only','--cap-drop=ALL','--security-opt=no-new-privileges:true',
'--tmpfs','/data:rw,size=128m,uid=1000,gid=1000','--tmpfs','/tmp:rw,size=16m,uid=1000,gid=1000',
'--mount','type=bind,src='+str(HERE)+',dst=/scripts,readonly','--mount','type=bind,src='+str(SCANNER)+',dst=/scanner,readonly',
'--entrypoint=/bin/sh',IMAGE,'-c','tail -f /dev/null']
q=subprocess.run(argv,capture_output=True,timeout=30)
if q.returncode:raise RuntimeError('fixture-start-failed')
cid=q.stdout.decode().strip();results=[]
def cmd(args,data=None,ok=True):
q=subprocess.run(['docker','exec','-i',cid,*args],input=data,capture_output=True,timeout=60)
if (q.returncode==0)!=ok:raise RuntimeError('fixture-operation-status-unexpected')
return q.stdout
old='/data/security/receive-secret-scan/old';new='/data/security/receive-secret-scan/new';marker='stage-image-fixture'
try:
cmd(['sh','-c','mkdir -p '+old+'; cp /scanner '+old+'/gitleaks; chmod 755 '+old+'/gitleaks'])
args=['sh','/scripts/stage.sh',new,old,adapter,scanner,marker]
for case,data,ok in [('fresh',binary,True),('cached-full-input',binary,True),('cached-empty-rejected',b'',False),('cached-short-rejected',b'incomplete',False)]:
out=cmd(args,data,ok)
if ok:
result=json.loads(out);assert result['stage_verified'] and result['already_staged']==(case!='fresh')
results.append({'case':case,'passed':True})
sums=cmd(['sh','-c','sha256sum '+new+'/receive-secret-scan '+new+'/gitleaks']).decode().splitlines()
assert [x.split()[0] for x in sums]==[adapter,scanner]
assert cmd(['sh','-c','find /data -name "*.staging-*" -print'])==b''
result={'result':'passed','image':IMAGE,'binary_bytes':len(binary),'cases':results,'cached_files_unchanged':True,'native_mutation':False}
(HERE/'stage-image-tests.json').write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(result))
finally:subprocess.run(['docker','stop','--time=0',cid],capture_output=True,timeout=30)
if __name__=='__main__':main()