Record approved scanner revision 5 and verified cached-stream correction
This commit is contained in:
parent
fa81fcf88d
commit
dfd7ee5f30
25 changed files with 10369 additions and 23 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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."
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue