Preserve failed scanner preflight and add value-free read-only diagnosis
This commit is contained in:
parent
26d4efd280
commit
a2e57251ac
8 changed files with 219 additions and 5 deletions
|
|
@ -15,3 +15,7 @@ The exact native preparation is now [revision 2](native-r2/PLAN.md), under the s
|
|||
Native activation awaits exact founder approval under `docs/agent-autonomy-decision.md` ("The supervisor approves that proposal or performs it through the privileged execution path."). The agent must not borrow the founder admin kubeconfig. The prepared founder launcher is `/tmp/forge-scanner-rollout.sh`. Its sealed runner refuses unknown initial identity/guard/hook/source state and any packet drift.
|
||||
|
||||
The staged scanner in repo-manager and live receiver have not been altered by this candidate. Independent clean-file commits continue under the original policy.
|
||||
|
||||
## Current execution outcome, 2026-11-02
|
||||
|
||||
The founder ran the exact revision-2 launcher. It failed during preflight with a suppressed Kubernetes command error, before staging or native mutation (`native_mutation: false`, no events). The exact failed read is not yet identified. Failure evidence is preserved in `attempt-r2-preflight/`; the reviewed packet is unchanged. A separate read-only diagnostic labels each check and outputs fixed failure categories; three tests passed, including raw-diagnostic suppression and receipt preservation. Founder invocation: `bash /tmp/forge-scanner-diagnose.sh`. Do not retry the old apply while this failure is unresolved; its existing receipt prevents an unreviewed retry.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
"""Founder-run read-only replay of sealed r2 preflight, with value-free failures.
|
||||
|
||||
Never invokes apply, staging, patch, diff, server dry-run or rollback. Never
|
||||
prints command arguments, shell diagnostics, Kubernetes objects or credentials.
|
||||
The original failed execution receipt is not modified.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ORIGINAL = HERE.parent / 'native-r2'
|
||||
EXPECTED_PACKET = 'd22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237'
|
||||
OUTPUT = Path('/tmp/forge-scanner-diagnostic-receipt.json')
|
||||
|
||||
|
||||
def failure_category(returncode, stderr):
|
||||
# Fixed labels only. Raw stderr stays in memory and is discarded.
|
||||
if returncode == 0:
|
||||
return 'passed'
|
||||
for needle, label in [(b'Forbidden', 'forbidden'), (b'Unauthorized', 'unauthorized'),
|
||||
(b'certificate has expired', 'certificate-expired'),
|
||||
(b'certificate signed by unknown authority', 'certificate-authority'),
|
||||
(b'connection refused', 'connection-refused'),
|
||||
(b'no such host', 'name-resolution'),
|
||||
(b'context deadline exceeded', 'deadline-exceeded'),
|
||||
(b'NotFound', 'not-found')]:
|
||||
if needle in stderr:
|
||||
return label
|
||||
if re.search(rb'command terminated with exit code [0-9]+', stderr):
|
||||
return 'remote-check-nonzero'
|
||||
return 'unclassified-command-failure'
|
||||
|
||||
|
||||
def step_label(args, cfg, inventory):
|
||||
if 'exec' in args:
|
||||
# Exact known shell only, including the literal check argument.
|
||||
ensure(args[-1:] in [('check',), ('sh',)], 'unexpected-exec-tail')
|
||||
if cfg['guard_command'][2] in args and args[-1] == 'check':
|
||||
return 'installed-artifact-guard'
|
||||
if inventory in args:
|
||||
return 'existing-repository-hook-inventory'
|
||||
raise RuntimeError('unexpected-exec-script')
|
||||
if args[:2] == ('get', 'nodes'):
|
||||
return 'node-identity-read'
|
||||
for resource, label in [('deployment', 'deployment-read'), ('replicasets', 'replicaset-read'),
|
||||
('pods', 'pod-list-read'), ('pod', 'initial-pod-uid-read'),
|
||||
('configmap', 'old-public-hook-read')]:
|
||||
if args[:4] == ('-n', 'forgejo', 'get', resource):
|
||||
return label
|
||||
raise RuntimeError('unexpected-read-command')
|
||||
|
||||
|
||||
def ensure(condition, reason):
|
||||
if not condition:
|
||||
raise RuntimeError(reason)
|
||||
|
||||
|
||||
def main():
|
||||
spec = importlib.util.spec_from_file_location('sealed_r2_rollout', ORIGINAL / 'rollout.py')
|
||||
module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
|
||||
ensure(module.verify_packet() == EXPECTED_PACKET, 'reviewed-packet-drift')
|
||||
cfg = json.loads((ORIGINAL / 'inputs.json').read_bytes())
|
||||
receipt = {'read_only': True, 'replayed_packet_sha256': EXPECTED_PACKET, 'steps': [],
|
||||
'native_mutation': False, 'original_failed_receipt_preserved': True}
|
||||
|
||||
def save():
|
||||
OUTPUT.write_text(json.dumps(receipt, indent=2) + '\n')
|
||||
|
||||
class Diagnostic(module.Rollout):
|
||||
def k(self, *args, data=None, timeout=60):
|
||||
ensure(data is None, 'unexpected-stdin')
|
||||
label = step_label(args, cfg, module.inventory_script())
|
||||
try:
|
||||
q = subprocess.run(['kubectl', *args], capture_output=True, timeout=min(timeout, 60))
|
||||
except subprocess.TimeoutExpired:
|
||||
receipt['steps'].append({'step': label, 'category': 'timeout'})
|
||||
save(); print(json.dumps(receipt['steps'][-1]), flush=True)
|
||||
raise RuntimeError('read-only-command-timeout')
|
||||
item = {'step': label, 'returncode': q.returncode,
|
||||
'category': failure_category(q.returncode, q.stderr)}
|
||||
if q.returncode:
|
||||
match = re.search(rb'command terminated with exit code ([0-9]+)', q.stderr)
|
||||
if match:
|
||||
item['remote_exit_code'] = int(match[1])
|
||||
receipt['steps'].append(item); save(); print(json.dumps(item), flush=True)
|
||||
ensure(q.returncode == 0, 'read-only-step-failed:' + label)
|
||||
return q.stdout
|
||||
|
||||
def event(self, name, **details):
|
||||
# No writes to the native r2 receipt; details contain public counters only.
|
||||
receipt['checks'] = {'event': name, **details}; save()
|
||||
|
||||
def dry_run(self, d):
|
||||
receipt['server_dry_run'] = 'intentionally-not-replayed'
|
||||
|
||||
def ready_pod(self, d, seconds=180):
|
||||
return super().ready_pod(d, seconds=10)
|
||||
|
||||
try:
|
||||
Diagnostic(cfg).preflight()
|
||||
receipt['result'] = 'read-only-preflight-passed'
|
||||
except Exception as exc:
|
||||
receipt['result'] = 'read-only-preflight-refused'
|
||||
receipt['reason'] = str(exc) if isinstance(exc, RuntimeError) else type(exc).__name__
|
||||
save()
|
||||
print(json.dumps({'result': receipt['result'], 'reason': receipt.get('reason'),
|
||||
'receipt': str(OUTPUT), 'native_mutation': False}), flush=True)
|
||||
return 0 if receipt['result'] == 'read-only-preflight-passed' else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(json.dumps({'result': 'diagnostic-refused', 'reason': str(error) if isinstance(error, RuntimeError) else type(error).__name__}))
|
||||
raise SystemExit(1)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"proposal_id": "SYNC-SCANNER-JSON-DIGEST-20261101",
|
||||
"executed_revision": 2,
|
||||
"executed_packet_sha256": "d22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237",
|
||||
"execution_identity": "founder; attended terminal",
|
||||
"execution_evidence": "User returned bash /tmp/forge-scanner-rollout.sh failure output in active conversation, 2026-11-02",
|
||||
"outcome": "failed",
|
||||
"reported_reason": "kubectl-command-failed; raw diagnostics withheld",
|
||||
"verified_phase": "preflight; exact failed read/remote check not yet identified",
|
||||
"native_mutation": false,
|
||||
"receipt_sha256": "35e4ba1b09065ec765762d4238235cb02286c42e19baad0f0ae505167c44a500",
|
||||
"recovery": "No native mutation, no rollback required; original receipt and sealed packet preserved. Read-only diagnostic prepared.",
|
||||
"diagnostic_sha256": "301919aa82b9e9ae5f1a2d55175d2e988ba4c4ac955598651d7f25ba71ddc21c",
|
||||
"tests": "3 passed; failure label/redaction, command restrictions and preservation of original receipt",
|
||||
"promotion_evidence": false,
|
||||
"score": "unscored; no signed approval or separate approval evidence backfilled"
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"proposal_id": "SYNC-SCANNER-JSON-DIGEST-20261101", "revision": 2, "events": [], "native_mutation": false, "packet_sha256": "d22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237", "mode": "apply"}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
"""Value-free diagnostics and refusal boundaries; no cluster access."""
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
spec = importlib.util.spec_from_file_location('read_only_diagnostic', HERE / 'diagnose.py')
|
||||
d = importlib.util.module_from_spec(spec); spec.loader.exec_module(d)
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_error_categories_never_echo_raw_diagnostics(self):
|
||||
sentinel = b'PRIVATE_SENTINEL_DO_NOT_EMIT'
|
||||
for raw, expected in [(b'Forbidden: ' + sentinel, 'forbidden'),
|
||||
(b'command terminated with exit code 1 ' + sentinel, 'remote-check-nonzero'),
|
||||
(sentinel, 'unclassified-command-failure')]:
|
||||
self.assertEqual(d.failure_category(1, raw), expected)
|
||||
|
||||
def test_only_expected_reads_and_exact_check_scripts_are_labeled(self):
|
||||
cfg = {'guard_command': ['sh', '-c', 'EXACT_PUBLIC_GUARD']}
|
||||
self.assertEqual(d.step_label(('get', 'nodes', '-o', 'json'), cfg, 'INVENTORY'), 'node-identity-read')
|
||||
self.assertEqual(d.step_label(('-n', 'forgejo', 'exec', '-i', 'pod', '--', 'sh', '-c', 'EXACT_PUBLIC_GUARD', 'sh', 'check'), cfg, 'INVENTORY'), 'installed-artifact-guard')
|
||||
self.assertEqual(d.step_label(('-n', 'forgejo', 'exec', '-i', 'pod', '--', 'sh', '-c', 'INVENTORY', 'sh'), cfg, 'INVENTORY'), 'existing-repository-hook-inventory')
|
||||
for argv in [('create', '-f', '-'), ('patch', 'deployment', 'forge'), ('get', 'secrets'),
|
||||
('exec', 'pod', '--', 'sh', '-c', 'OTHER', 'sh'),
|
||||
('exec', 'pod', '--', 'sh', '-c', 'EXACT_PUBLIC_GUARD', 'sh', 'apply')]:
|
||||
with self.subTest(argv=argv), self.assertRaises(RuntimeError): d.step_label(argv, cfg, 'INVENTORY')
|
||||
|
||||
def test_failed_read_is_labeled_and_original_native_receipt_is_preserved(self):
|
||||
native = Path('/tmp/forge-scanner-rollout-receipt.json')
|
||||
before = native.read_bytes() if native.exists() else None
|
||||
class Failed:
|
||||
stdout = b'PRIVATE_STDOUT_SENTINEL'
|
||||
stderr = b'Forbidden: PRIVATE_STDERR_SENTINEL'
|
||||
returncode = 1
|
||||
with tempfile.TemporaryDirectory() as temp, patch.object(d, 'OUTPUT', Path(temp) / 'receipt.json'), patch.object(d.subprocess, 'run', return_value=Failed()) as command:
|
||||
captured = io.StringIO()
|
||||
with contextlib.redirect_stdout(captured): result = d.main()
|
||||
text = captured.getvalue(); receipt = json.loads(d.OUTPUT.read_bytes())
|
||||
self.assertEqual(result, 1)
|
||||
self.assertEqual(command.call_count, 1)
|
||||
self.assertEqual(receipt['steps'], [{'step': 'node-identity-read', 'returncode': 1, 'category': 'forbidden'}])
|
||||
self.assertFalse(receipt['native_mutation'])
|
||||
self.assertNotIn('PRIVATE_', text)
|
||||
self.assertNotIn('PRIVATE_', d.OUTPUT.read_text())
|
||||
self.assertEqual(native.read_bytes() if native.exists() else None, before)
|
||||
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue