diff --git a/.kaizen/agents/custodian-codex/supervision.json b/.kaizen/agents/custodian-codex/supervision.json index ef3df468..ddc4a9ad 100644 --- a/.kaizen/agents/custodian-codex/supervision.json +++ b/.kaizen/agents/custodian-codex/supervision.json @@ -1283,8 +1283,8 @@ } ], "execution_ref": "docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/outcome.json", - "verified_outcome": "Founder executed exact reviewed revision2; preflight command failed before staging/native mutation. Exact failed read not yet known; value-free read-only replay prepared.", - "recovery": "No native mutation; original failure receipt and packet preserved; awaiting founder read-only diagnostic result.", + "verified_outcome": "Founder exact revision2 failed before mutation. Follow-up read-only diagnostic: identities/readiness/old hook/artifact guard passed; existing repository hook inventory failed with remote exit1. Await specific filesystem condition; original failed execution retained.", + "recovery": "No native mutation or source activation; original packet and both receipts preserved. Read-only inventory-condition diagnostic prepared; five diagnostic tests passed.", "supervisor_execution_evidence": "User returned bash /tmp/forge-scanner-rollout.sh failure output in active conversation, 2026-11-02", "attempts": [ { diff --git a/docs/changes/2026-11-01-scanner-json-digest/README.md b/docs/changes/2026-11-01-scanner-json-digest/README.md index fc46f041..50ffb432 100644 --- a/docs/changes/2026-11-01-scanner-json-digest/README.md +++ b/docs/changes/2026-11-01-scanner-json-digest/README.md @@ -19,3 +19,5 @@ The staged scanner in repo-manager and live receiver have not been altered by th ## 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. + +The first diagnostic was returned: node/deployment/pod identity, readiness, old public hook and artifact guard all passed; existing repository hook inventory failed with remote exit1. Its selected receipt is preserved in `attempt-r2-preflight/diagnostic-receipt.json`. The pending request is now `bash /tmp/forge-scanner-inventory-diagnose.sh` to identify the specific filesystem condition without reading out hook contents. Five diagnostic tests pass, including real disposable filesystem cases. diff --git a/docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/diagnostic-receipt.json b/docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/diagnostic-receipt.json new file mode 100644 index 00000000..044a8c30 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/diagnostic-receipt.json @@ -0,0 +1,56 @@ +{ + "read_only": true, + "replayed_packet_sha256": "d22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237", + "steps": [ + { + "step": "node-identity-read", + "returncode": 0, + "category": "passed" + }, + { + "step": "deployment-read", + "returncode": 0, + "category": "passed" + }, + { + "step": "replicaset-read", + "returncode": 0, + "category": "passed" + }, + { + "step": "pod-list-read", + "returncode": 0, + "category": "passed" + }, + { + "step": "deployment-read", + "returncode": 0, + "category": "passed" + }, + { + "step": "initial-pod-uid-read", + "returncode": 0, + "category": "passed" + }, + { + "step": "old-public-hook-read", + "returncode": 0, + "category": "passed" + }, + { + "step": "installed-artifact-guard", + "returncode": 0, + "category": "passed" + }, + { + "step": "existing-repository-hook-inventory", + "returncode": 1, + "category": "remote-check-nonzero", + "remote_exit_code": 1 + } + ], + "native_mutation": false, + "original_failed_receipt_preserved": true, + "result": "read-only-preflight-refused", + "reason": "read-only-step-failed:existing-repository-hook-inventory" +} diff --git a/docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/inventory_conditions.py b/docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/inventory_conditions.py new file mode 100644 index 00000000..3c3e7986 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/inventory_conditions.py @@ -0,0 +1,91 @@ +"""Founder-run read-only explanation of inventory conditions; no hook contents.""" +from pathlib import Path +import importlib.util +import json +import re +import subprocess + +HERE = Path(__file__).resolve().parent +ORIGINAL = HERE.parent / 'native-r2' +EXPECTED = 'd22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237' +CONDITIONS = {'directory-missing', 'symlink', 'hook-symlink-present', 'symlink-query-failed', + 'dispatcher-missing', 'dispatcher-not-executable', 'scanner-fragment-missing', + 'hook-metadata-command-failed', 'repository-limit', 'no-repositories', 'wrong-uid'} +SCRIPT = r'''set +vx +set -u +export PATH=/usr/bin:/bin +issue() { printf 'ISSUE\t%s\t%s\n' "$1" "$2"; } +check_directory() { + test -d "$1" || issue directory-missing "$1" + test ! -L "$1" || issue symlink "$1" +} +test "$(id -u)" = 1000 || issue wrong-uid /data +for parent in /data /data/git /data/git/gitea-repositories; do check_directory "$parent"; done +count=0 +for repo in /data/git/gitea-repositories/*/*.git; do + test -d "$repo" || continue + count=$((count+1)) + if test "$count" -gt 500; then issue repository-limit /data/git/gitea-repositories; break; fi + for parent in "${repo%/*}" "$repo" "$repo/hooks" "$repo/hooks/pre-receive.d"; do check_directory "$parent"; done + links=$(find "$repo/hooks" -type l -print -quit 2>/dev/null) + code=$? + if test "$code" != 0; then issue symlink-query-failed "$repo/hooks" + elif test -n "$links"; then issue hook-symlink-present "$repo/hooks"; fi + test -f "$repo/hooks/pre-receive" || issue dispatcher-missing "$repo/hooks/pre-receive" + test -x "$repo/hooks/pre-receive" || issue dispatcher-not-executable "$repo/hooks/pre-receive" + test -f "$repo/hooks/pre-receive.d/10-custodian-secret-scan" || issue scanner-fragment-missing "$repo/hooks/pre-receive.d/10-custodian-secret-scan" + if ! find "$repo/hooks" -type f ! -name '*.sample' -exec sh -c ' + for file do + stat -c %a "$file" >/dev/null || exit 1 + stat -c %u "$file" >/dev/null || exit 1 + stat -c %g "$file" >/dev/null || exit 1 + sha256sum "$file" >/dev/null || exit 1 + done + ' sh {} + >/dev/null 2>&1; then issue hook-metadata-command-failed "$repo/hooks"; fi +done +test "$count" -gt 0 || issue no-repositories /data/git/gitea-repositories +printf 'COUNT\t%s\n' "$count" +''' + + +def parse(raw): + issues = []; count = None + for line in raw.decode().splitlines(): + parts = line.split('\t') + if len(parts) == 2 and parts[0] == 'COUNT' and parts[1].isdigit(): + if count is not None: raise RuntimeError('duplicate-counter') + count = int(parts[1]); continue + if len(parts) != 3 or parts[0] != 'ISSUE' or parts[1] not in CONDITIONS: + raise RuntimeError('unexpected-diagnostic-output; raw withheld') + path = parts[2] + if not re.fullmatch(r'/data(?:/[a-zA-Z0-9_.-]+)*', path) or '..' in path.split('/'): + path = 'unsafe-path-withheld' + issues.append({'condition': parts[1], 'path': path}) + if count is None: raise RuntimeError('missing-counter') + return {'read_only': True, 'repositories_examined': count, 'issues': issues, 'native_mutation': False} + + +def main(): + spec = importlib.util.spec_from_file_location('reviewed_r2_inventory', ORIGINAL / 'rollout.py') + m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m) + if m.verify_packet() != EXPECTED: raise RuntimeError('reviewed-packet-drift') + c = json.loads((ORIGINAL / 'inputs.json').read_bytes()) + def k(*args): + q = subprocess.run(['kubectl', *args], capture_output=True, timeout=90) + if q.returncode: raise RuntimeError('read-only-command-failed; raw diagnostics withheld') + return q.stdout + for resource, name, expected in [('deployment', c['deployment_name'], c['deployment_uid']), ('pod', c['pod'], c['pod_uid'])]: + if k('-n', 'forgejo', 'get', resource, name, '-o', 'jsonpath={.metadata.uid}').decode() != expected: + raise RuntimeError('target-identity-drift') + result = parse(k('-n', 'forgejo', 'exec', '-i', c['pod'], '-c', c['container'], '--', 'sh', '-c', SCRIPT)) + result['replayed_packet_sha256'] = EXPECTED + out = Path('/tmp/forge-scanner-inventory-diagnostic-receipt.json') + out.write_text(json.dumps(result, indent=2) + '\n') + print(json.dumps(result, indent=2)); print('receipt:', str(out)) + + +if __name__ == '__main__': + try: main() + except Exception as exc: + print(json.dumps({'result': 'read-only-diagnostic-refused', 'reason': str(exc) if isinstance(exc, RuntimeError) else type(exc).__name__})) + raise SystemExit(1) diff --git a/docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/outcome.json b/docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/outcome.json index a262f52e..b406f186 100644 --- a/docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/outcome.json +++ b/docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/outcome.json @@ -13,5 +13,19 @@ "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" + "score": "unscored; no signed approval or separate approval evidence backfilled", + "followup_diagnostic": { + "receipt_ref": "diagnostic-receipt.json", + "passed_checks": [ + "node/deployment/pod identities", + "readiness", + "old public hook", + "installed artifact guard" + ], + "failed_check": "existing-repository-hook-inventory", + "category": "remote-check-nonzero", + "remote_exit_code": 1, + "next_check": "inventory_conditions.py; read-only filesystem condition report", + "diagnostic_tests": "5 passed in total, including actual disposable filesystem fixtures" + } } diff --git a/docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/test_inventory_conditions.py b/docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/test_inventory_conditions.py new file mode 100644 index 00000000..de7958b2 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/test_inventory_conditions.py @@ -0,0 +1,44 @@ +"""Exercise actual read-only shell against disposable filesystem fixtures.""" +import importlib.util +from pathlib import Path +import subprocess +import tempfile +import unittest + +HERE = Path(__file__).resolve().parent +spec = importlib.util.spec_from_file_location('inventory_conditions', HERE / 'inventory_conditions.py') +m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m) + + +class Tests(unittest.TestCase): + def test_clean_missing_fragment_nonexecutable_dispatcher_and_symlink(self): + with tempfile.TemporaryDirectory() as temp: + data = Path(temp) / 'data' + hooks = data / 'git/gitea-repositories/coulomb/example.git/hooks' + fragments = hooks / 'pre-receive.d'; fragments.mkdir(parents=True) + dispatcher = hooks / 'pre-receive'; dispatcher.write_text('#!/bin/sh\nexit 0\n'); dispatcher.chmod(0o755) + fragment = fragments / '10-custodian-secret-scan'; fragment.write_text('#!/bin/sh\nexit 0\n'); fragment.chmod(0o775) + script = m.SCRIPT.replace('/data', str(data)) + def issues(): + q = subprocess.run(['sh', '-c', script], capture_output=True, timeout=10) + self.assertEqual(q.returncode, 0) + result = m.parse(q.stdout) + self.assertEqual(result['repositories_examined'], 1) + return {x['condition'] for x in result['issues']} - {'wrong-uid'} + self.assertEqual(issues(), set()) + fragment.unlink(); self.assertEqual(issues(), {'scanner-fragment-missing'}) + fragment.write_text('#!/bin/sh\nexit 0\n'); fragment.chmod(0o775) + dispatcher.chmod(0o644); self.assertEqual(issues(), {'dispatcher-not-executable'}) + dispatcher.chmod(0o755) + original = hooks / 'saved.d'; fragments.rename(original); fragments.symlink_to(original, target_is_directory=True) + self.assertEqual(issues(), {'symlink', 'hook-symlink-present'}) + self.assertTrue((original / fragment.name).exists()) + + def test_parser_rejects_unframed_data_and_hides_unsafe_paths(self): + with self.assertRaisesRegex(RuntimeError, '^unexpected-diagnostic-output; raw withheld$'): + m.parse(b'PRIVATE_SENTINEL\n') + result = m.parse(b'ISSUE\tsymlink\t/elsewhere/PRIVATE_SENTINEL\nCOUNT\t1\n') + self.assertEqual(result['issues'][0]['path'], 'unsafe-path-withheld') + + +if __name__ == '__main__': unittest.main() diff --git a/docs/repository-sync-2026-11-01.md b/docs/repository-sync-2026-11-01.md index 4dcf1955..1f81f0d0 100644 --- a/docs/repository-sync-2026-11-01.md +++ b/docs/repository-sync-2026-11-01.md @@ -83,3 +83,5 @@ The founder reviews `native-r2/PLAN.md` and the sealed source diffs, then may ap ### Revision-2 native attempt failed before mutation, 2026-11-02 The founder executed `/tmp/forge-scanner-rollout.sh` and returned `kubectl-command-failed; raw diagnostics withheld`. Its local receipt binds the exact reviewed packet, mode apply, `native_mutation: false`, and no events: it stopped during the read-only preflight, before staging or server dry-run. Exact failed command remains unknown. Preserved receipt/outcome and the separate tested read-only diagnostic are in `docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/`. The diagnostic reports step labels and safe categories, rejects unexpected commands, suppresses raw stdout/stderr and preserves the native receipt; 3 tests passed. Neither the sealed revision2 packet nor six owner-source preimages changed. Source followthrough and all held publication remain pending; failed execution stays unscored in supervision evidence. + +Follow-up diagnostic returned: eight preliminary checks passed; existing repository hook inventory failed with remote exit1. The next read-only diagnostic reports the exact rejected filesystem condition and public path, while leaving hooks and both prior receipts intact. Five diagnostic tests passed, including missing fragments, executable checks and symlink fixtures. Founder request: `bash /tmp/forge-scanner-inventory-diagnose.sh`. No native or owner scanner-source change. diff --git a/workplans/CUST-WP-0083-fleet-sync-and-scanner-remediation.md b/workplans/CUST-WP-0083-fleet-sync-and-scanner-remediation.md index 29308492..a2d2e780 100644 --- a/workplans/CUST-WP-0083-fleet-sync-and-scanner-remediation.md +++ b/workplans/CUST-WP-0083-fleet-sync-and-scanner-remediation.md @@ -59,14 +59,14 @@ status: wait decision_id: "5410707c-ce73-4f80-9c03-805102c31c58" priority: high needs_human: true -intervention_note: "Revision 2 preflight failed before native mutation. Run bash /tmp/forge-scanner-diagnose.sh in the founder terminal and return its value-free read-only diagnostic output; original receipt and exact packet are preserved." +intervention_note: "Revision 2 failed before mutation: identities/readiness/template/artifact guard passed, repository hook inventory exited1. Run bash /tmp/forge-scanner-inventory-diagnose.sh in the founder terminal and return its read-only condition report." state_hub_task_id: "589e1a54-9a4e-5f90-bd29-5c61a6cf8081" ``` Human-lane checkpoint: the founder successfully returned `/tmp/forge-scanner-preflight-receipt.json` on 2026-11-02. It verifies exact node/deployment/pod identities and old artifacts/template, Recreate strategy and 68% CPU/76% memory requests. The exact revision-2 packet is prepared at `docs/changes/2026-11-01-scanner-json-digest/native-r2/PLAN.md`, SHA256 `d22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237`. Its 13 isolated filesystem/real receiver cases and five controller tests passed; all six owner preimages/candidate digests and the two-field Apps semantic change were verified. Initial missing offline dispatcher fixture failure is retained. Server dry-run/diff, live probes and activation are prepared execution prerequisites, not completed native evidence. Await exact founder approval and attended native execution; preserve all old artifacts and guarded rollback, source followthrough and normal publication. Neither local runtime scanner nor live receiver has changed. No admin credential borrowing, invented owner approval, promotion or scored approval. -Execution update, 2026-11-02: the founder ran the exact revision-2 launcher, which failed on a Kubernetes command during preflight before any staging/native mutation. The failure receipt and outcome are preserved in `docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/`. The wrapper suppressed the failed command label as well as raw diagnostics; a separate tested read-only replay now reports step labels and fixed error categories. Await `/tmp/forge-scanner-diagnose.sh` output from the founder terminal. Original packet stays sealed; no failed trial is counted as a success and no further mutation or source followthrough is authorized by this diagnosis. +Execution update, 2026-11-02: the founder ran the exact revision-2 launcher, which failed on a Kubernetes command during preflight before any staging/native mutation. The failure receipt and outcome are preserved in `docs/changes/2026-11-01-scanner-json-digest/attempt-r2-preflight/`. The wrapper suppressed the failed command label as well as raw diagnostics; a separate tested read-only replay now reports step labels and fixed error categories. The founder returned that diagnostic: all identity/readiness/template/artifact checks passed, but repository hook inventory returned remote exit1. Await `bash /tmp/forge-scanner-inventory-diagnose.sh` output for the specific failed filesystem condition. This second read-only check reports only condition labels/public paths; five diagnostic tests now pass, including actual disposable filesystem fixtures. Original packet stays sealed; no failed trial is counted as a success and no further mutation or source followthrough is authorized by this diagnosis. ## Verify complete remote publication and report exceptions