diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 5294b9b..0cef942 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,3 +1,13 @@ +# FW01 source discovery correction — 2026-10-13 + +The [source packet and receipt](evidence/runs/2026-10-13-fw01-discovery/README.md) +records actual healthy/readable/unreadable baseline evidence and the bounded +permission-error fix. New focused controls pass; source payload and version stay +unchanged. Next: independent review, serialized owner aggregate and genuine normal +PR admission. No full-scan/runtime completeness, publication or installation claim. + +--- + # FW00 referral dispositions — 2026-10-13 [Current packet](evidence/runs/2026-10-13-fw00-referrals.md) closes only B-160's diff --git a/docs/evidence/runs/2026-10-13-fw01-discovery/README.md b/docs/evidence/runs/2026-10-13-fw01-discovery/README.md new file mode 100644 index 0000000..d8bee7e --- /dev/null +++ b/docs/evidence/runs/2026-10-13-fw01-discovery/README.md @@ -0,0 +1,79 @@ +# FW01 source discovery completeness — agent-stack + +## Frozen packet and baseline + +Owner source: [63e155d](https://github.com/ssheleg/agent-stack/commit/63e155d9c1ca9df789535926acc3546f33954e3b). +Version remains 0.28.2; this packet changes owner validation and evidence only. +The [actual whole-validator baseline](baseline.json) executes `python3 -B test/validate.py` +in an immutable Git-source fixture: healthy exits 0, readable unauthorized maker exits +1, then its parent directory is made unreadable (OS PermissionError independently +confirmed) and the same validator exits 0. This proves a discovery omission, beyond +an extracted-helper observation. No complete owner suite or native runtime was run. + +The upstream family finding is +[the bounded central receipt](https://github.com/ssheleg/sshlg-skills/blob/38b69b1d3c022f45e4ac3464c5b83f4b96047852/docs/evidence/completion-1013/fw01-discovery-followup.json). +SEO's same discovery seam was corrected at +[76d25cd](https://github.com/ssheleg/seo-aeo-audit/commit/76d25cd5c283bce7c4fdd25c7c42fee7038fe1b2). +Its lexical/exemption policy is not copied into this owner. + +## Contract and acceptance + +- Keep this owner's scanner predicate, skip directories, lexical matching and exact + exemptions unchanged. Only discovery error handling is in scope. +- `os.walk` directory discovery failure must add an ordinary owner guard failure + identifying relative source path and error class. It must not become an empty, + apparently complete source set. Keep running readable checks to collect evidence. +- Existing unreadable source-file behavior already exits nonzero; no success claim + or broad parser rewrite is introduced. Excluded dependency directories remain + excluded, including when unreadable. No symlink traversal policy is changed. +- Add focused helper admission controls and actual whole-validator healthy, + readable allocation, unreadable directory, and restoration controls. Confirm the + permission plant actually fails under this user; root privilege must not produce + a fake passing permission test. Restore permissions in finally; all fixtures go + through the owner's residue ledger. +- Preserve versions and every published payload file. Verify npm pack path/digest + equality against the accepted baseline. Independent peer review and normal PR + admission follow; broader owner gate waits for the coordinator's serial slot. + +Implementation is the next step. No scanner, runtime, board closure, publication +or installation is claimed by this source-first packet. + +The first baseline harness attempt failed its own diagnostic-stream assertion: +this owner emits failure diagnostics on stderr, while make-skill uses stdout. +The actual readable plant already returned 1. The harness was corrected to inspect +both streams, then all three baseline conditions above were rerun successfully. +This was a harness failure, not a successful owner check or an implementation fix. + +## Implemented and focused verification + +Only the scanner's `os.walk` call gained an error callback (relative path and error +class); the owner's lexical rules, exemptions and skip set are unchanged. The new +`test/audit_regressions/fw01_discovery.py` passes 9 controls: helper healthy, +permission refusal, restoration, excluded dependency and absent-root admission; +actual whole-validator healthy, readable unauthorized maker, unreadable directory, +and restoration. Permission tests explicitly prove OS denial and restore modes in +finally. Both owned fixture trees were removed after success. + +The [pack comparison](payload-parity.json) checks all 44 current npm payload +files (609,718 bytes) against accepted baseline Git blobs. Only test/docs +paths change; package selection and version are unchanged. Registry metadata confirms +that version exists; this is not a fresh immutable-tarball readback. No new release +or installed update is needed for this excluded validation/evidence source change. + +## Independent review and owner aggregate + +[Root's independent source acceptance](https://github.com/ssheleg/sshlg-skills/blob/85d1758e02cbf1759be861a7af2108d267c339a3/docs/evidence/completion-1013/fw01-sibling-peer.json) binds the implementation above +and independently replays all 9 controls. It accepts directory discovery refusal +within the documented scanner scope, without a filesystem race or symlink proof. + +The exact implementation commit `bc0f66387850f30674ae14636f7e40d98e029d2b` then passed the complete owner +`npm test` command, exit 0, in 12.04 seconds. The +[structured result](owner-full.json) binds source, command and log digest; +[full output](owner-full.log.gz) preserves every group. Minimum observed free +space was 1533267968 bytes. The watchdog would stop only this owned +process group below 300 MiB; it did not fire. Owned scratch/cache trees were removed. +No provider, native-host or model behavior was exercised. + +Next: commit this documentation freeze, promote the existing draft PR once, and +require genuine eligible PR jobs before normal integration. Hosted checks and merge +remain pending; no release or installed update is needed for this excluded source. diff --git a/docs/evidence/runs/2026-10-13-fw01-discovery/baseline.json b/docs/evidence/runs/2026-10-13-fw01-discovery/baseline.json new file mode 100644 index 0000000..0cb8180 --- /dev/null +++ b/docs/evidence/runs/2026-10-13-fw01-discovery/baseline.json @@ -0,0 +1,25 @@ +{ + "source": "63e155d9c1ca9df789535926acc3546f33954e3b", + "scope": "actual whole validator, no full owner suite", + "permission_plant_confirmed": true, + "results": [ + { + "case": "healthy", + "exit": 0, + "stdout": " unlooked: front matter vs a strict reader \u2014 no sshlg-skills umbrella above this checkout\nOK: agent-stack structurally valid (17 checks = 13 named + 4 per-skill, 4 skill(s), v0.28.2)\nresidue: this run left nothing \u2014 0 temp tree(s) created, 0 removed\n", + "stderr": "" + }, + { + "case": "readable unauthorized allocation", + "exit": 1, + "stdout": " unlooked: front matter vs a strict reader \u2014 no sshlg-skills umbrella above this checkout\nresidue: this run left nothing \u2014 0 temp tree(s) created, 0 removed\n", + "stderr": "FAIL: 1 problem(s)\n - scripts/fw01_hidden/leak.py: creates a temp tree outside the residue ledger \u2014 'tempfile.mkdtemp(' at line 2. A shipped script cannot reach the suite's ledger: make the tree with a context manager that removes it on every path and give the file an exact-count entry, with its reason, in `TEMP_TREE_ALLOWED` \u2014 or move the work into the suite\n" + }, + { + "case": "unreadable source directory", + "exit": 0, + "stdout": " unlooked: front matter vs a strict reader \u2014 no sshlg-skills umbrella above this checkout\nOK: agent-stack structurally valid (17 checks = 13 named + 4 per-skill, 4 skill(s), v0.28.2)\nresidue: this run left nothing \u2014 0 temp tree(s) created, 0 removed\n", + "stderr": "" + } + ] +} diff --git a/docs/evidence/runs/2026-10-13-fw01-discovery/initial-harness-failure.json b/docs/evidence/runs/2026-10-13-fw01-discovery/initial-harness-failure.json new file mode 100644 index 0000000..f4b7060 --- /dev/null +++ b/docs/evidence/runs/2026-10-13-fw01-discovery/initial-harness-failure.json @@ -0,0 +1,8 @@ +{ + "source": "63e155d9c1ca9df789535926acc3546f33954e3b", + "harness_exit": 1, + "actual_readable_plant_exit": 1, + "wrong_assumption": "diagnostic must be in stdout", + "correction": "inspect stdout plus stderr; retain genuine nonzero condition", + "rerun_receipt": "baseline.json" +} diff --git a/docs/evidence/runs/2026-10-13-fw01-discovery/owner-full.json b/docs/evidence/runs/2026-10-13-fw01-discovery/owner-full.json new file mode 100644 index 0000000..e185c24 --- /dev/null +++ b/docs/evidence/runs/2026-10-13-fw01-discovery/owner-full.json @@ -0,0 +1,17 @@ +{ + "owner": "agent-stack-fw01-scan", + "source": "bc0f66387850f30674ae14636f7e40d98e029d2b", + "command": "npm test", + "exit_code": 0, + "seconds": 12.04, + "minimum_free_bytes": 1533267968, + "disk_stopped": false, + "log": "owner-full.log.gz", + "log_sha256": "c30bf7ff7c13b4fbb04e87ffb77b4dc3147d38ba3513ba6a9ab50615d6e84d09", + "scratch_removed": true, + "independent_review": "https://github.com/ssheleg/sshlg-skills/blob/85d1758e02cbf1759be861a7af2108d267c339a3/docs/evidence/completion-1013/fw01-sibling-peer.json", + "scratch_remaining_before_owned_cleanup": [ + "data-gym-cache", + "node-compile-cache" + ] +} diff --git a/docs/evidence/runs/2026-10-13-fw01-discovery/owner-full.log.gz b/docs/evidence/runs/2026-10-13-fw01-discovery/owner-full.log.gz new file mode 100644 index 0000000..7d144cb Binary files /dev/null and b/docs/evidence/runs/2026-10-13-fw01-discovery/owner-full.log.gz differ diff --git a/docs/evidence/runs/2026-10-13-fw01-discovery/payload-parity.json b/docs/evidence/runs/2026-10-13-fw01-discovery/payload-parity.json new file mode 100644 index 0000000..727755d --- /dev/null +++ b/docs/evidence/runs/2026-10-13-fw01-discovery/payload-parity.json @@ -0,0 +1,240 @@ +{ + "baseline": "63e155d9c1ca9df789535926acc3546f33954e3b", + "package_version": "0.28.2", + "registry_observation": { + "version": "0.28.2", + "gitHead": "2b3d769775e368d0a1e6d0f20ea095ae24fbbd54" + }, + "current_pack_files": 44, + "current_pack_bytes": 609718, + "all_current_pack_blobs_equal_baseline": true, + "changed_tracked_scope": [ + "docs/evidence/runs/2026-10-13-fw01-discovery/README.md", + "docs/evidence/runs/2026-10-13-fw01-discovery/baseline.json", + "docs/evidence/runs/2026-10-13-fw01-discovery/initial-harness-failure.json", + "test/validate.py" + ], + "files": [ + { + "path": "CHANGELOG.md", + "sha256": "0fce6df83bb875438bbcd2dd1609665ae19348284405c1b2c7b075514a375e7e", + "size": 111515 + }, + { + "path": "LICENSE", + "sha256": "826df230e15df9b208c1e08c42ef3b764b21c4abec5a91a38976748e92a82e9d", + "size": 1064 + }, + { + "path": "README.md", + "sha256": "49953599fcd54dfcba0f1254636ddd46fca0e0ac6c141091bf72552c53131f3e", + "size": 14601 + }, + { + "path": "SECURITY.md", + "sha256": "87715bb85d820beb6179cf5294fc7ed461cb234657df810031c0eb527fb00b62", + "size": 3071 + }, + { + "path": "bin/agent-stack.js", + "sha256": "5706481b1f1266174eeefebc1f2fc5ff6f30f77b395289490b182e95d5c97b00", + "size": 7391 + }, + { + "path": "package.json", + "sha256": "42cbab0ed2a00d1bc950ada5688221b8d291bf82f1a681df6e3994935066ceed", + "size": 1323 + }, + { + "path": "plugins/agent-stack/.claude-plugin/plugin.json", + "sha256": "678aa34cceef4ff51bedad7f718baa901af911fdf33afd9bf22b091ac1461a3f", + "size": 1081 + }, + { + "path": "plugins/agent-stack/skills/agent-evals/references/multi-agent.md", + "sha256": "e0c3175aa8858de1ba8117c613d2e201e32ff581453078c927a3ebddcff6c0bb", + "size": 5473 + }, + { + "path": "plugins/agent-stack/skills/agent-evals/references/otel-genai.md", + "sha256": "d2cadad0a6a0632d234a51621b136f3001f5a7422e256f84d261d7c0ff1660d5", + "size": 13131 + }, + { + "path": "plugins/agent-stack/skills/agent-evals/references/statistics.md", + "sha256": "77cd03cc4f258a097888f81b817e8c9487b4d777d9ac092f76fe48de8de692ca", + "size": 13853 + }, + { + "path": "plugins/agent-stack/skills/agent-evals/SKILL.md", + "sha256": "8f533ce3e320e0416163a39a98207cd347a0d22332c4072afa1d628f77204b8b", + "size": 20773 + }, + { + "path": "plugins/agent-stack/skills/agent-harness/references/audit.md", + "sha256": "b91b966250438edd91b87c392113404d830b06301518d9120246321c88d9a1aa", + "size": 11484 + }, + { + "path": "plugins/agent-stack/skills/agent-harness/references/context-lifecycle.md", + "sha256": "f682b0d4894f7f58baf4f39e622ed1bf8bc773d3c76c552a09aecab4c1e23fef", + "size": 9055 + }, + { + "path": "plugins/agent-stack/skills/agent-harness/references/instruction-budget.md", + "sha256": "3962dbe1643ca1f867575786597cfeea93a28b1d0cc940811020e2d4b49fc6a6", + "size": 7026 + }, + { + "path": "plugins/agent-stack/skills/agent-harness/references/layers.md", + "sha256": "e2f4fd73424b0896c493616dbb98e7bd973eaad3b8365b3c1b31964fbac46d89", + "size": 6161 + }, + { + "path": "plugins/agent-stack/skills/agent-harness/references/pi-sdk.md", + "sha256": "b7eb9375934887ed5f85e597e80c5f3d83c02c37cd585aff13960104a31ddcd5", + "size": 14100 + }, + { + "path": "plugins/agent-stack/skills/agent-harness/references/pi.md", + "sha256": "684616062c9d109ff13eed83a5b9a10544040f1ee22790ee70e8d8156b286c08", + "size": 12889 + }, + { + "path": "plugins/agent-stack/skills/agent-harness/references/system-prompt.md", + "sha256": "f8577ebfeb8ae7448b36b5cd7fcca1ef4ab7065d1aef9baf6f7946eb5ea2905a", + "size": 7712 + }, + { + "path": "plugins/agent-stack/skills/agent-harness/references/techniques.md", + "sha256": "4198c8d2d8a0d6da2f8ba7f2370310578167819abbb9bd351a722491ee8f31e8", + "size": 7968 + }, + { + "path": "plugins/agent-stack/skills/agent-harness/references/tools.md", + "sha256": "572822bc05a710019fc5454f2ddb3750537dbf3667fc73fad0c8bcdcd0f1346a", + "size": 10351 + }, + { + "path": "plugins/agent-stack/skills/agent-harness/references/workbench-contracts.md", + "sha256": "a17af9cc84dcc8661085ebb606b09ac08ebe39f6c1fc436ab547ac21747bca80", + "size": 10349 + }, + { + "path": "plugins/agent-stack/skills/agent-harness/scripts/audit_agent.py", + "sha256": "f0193565dd3851d3ea779a05f018099b9f2bca3eccacc370b9a43048baba656a", + "size": 19276 + }, + { + "path": "plugins/agent-stack/skills/agent-harness/scripts/instruction_budget.py", + "sha256": "0f88ff1764cc5a87a2b523126c871e71af750bc29128e05b97f9e6955db999ab", + "size": 5863 + }, + { + "path": "plugins/agent-stack/skills/agent-harness/SKILL.md", + "sha256": "003866190df99a02ea862860050c03c2ce8971479ced5ba75bfe42057a0ba4e0", + "size": 14571 + }, + { + "path": "plugins/agent-stack/skills/agent-interop/references/a2a.md", + "sha256": "e39c4c6d1f19b39e09720c40a09a50e8f2a59824aeb83c5336443f8b709a6c34", + "size": 11113 + }, + { + "path": "plugins/agent-stack/skills/agent-interop/references/gateway.md", + "sha256": "e74e8585eb6a1fba08af5b419a67c41f38689a29886a89dc149ef3465b9319a0", + "size": 22093 + }, + { + "path": "plugins/agent-stack/skills/agent-interop/references/mcp-scale.md", + "sha256": "c69b0bfd73e189776fcc6ecdd7d3e88964073ab19a66ffcf79aab579b341a892", + "size": 8841 + }, + { + "path": "plugins/agent-stack/skills/agent-interop/references/mcp-ship.md", + "sha256": "2918ca0beed94ca7f7e34bb30e2609495c421121d3a94a0e43e743fc76b644a2", + "size": 7099 + }, + { + "path": "plugins/agent-stack/skills/agent-interop/references/mcp.md", + "sha256": "9b427194659408ae7f521ea689b8b23cbfd216f918f313cc961dce360b7e80b4", + "size": 14716 + }, + { + "path": "plugins/agent-stack/skills/agent-interop/references/registry.md", + "sha256": "648e7649369b9aa5dac5f424c9a4373292e5d73c90d9466b58171934e5c7b51f", + "size": 8377 + }, + { + "path": "plugins/agent-stack/skills/agent-interop/SKILL.md", + "sha256": "3e70a38f204edc6822814335dc6dfc4e8c3b825117137239da46b96fdd60bfec", + "size": 15786 + }, + { + "path": "plugins/agent-stack/skills/agent-orchestrator/references/context-engineering.md", + "sha256": "57811d800b75f25a5a242505e8e6be2b33775fb156893ebec51af5ea18fcfc1e", + "size": 11541 + }, + { + "path": "plugins/agent-stack/skills/agent-orchestrator/references/governance.md", + "sha256": "bb2e3389779a56d4ace610bb14b5df05ace7fa6efc1e5810b0ae929f7050e435", + "size": 9487 + }, + { + "path": "plugins/agent-stack/skills/agent-orchestrator/references/graph-engineering.md", + "sha256": "c594d1b0a4813f0d47e60a26e2b7905a964c3017556a400fcbd534f390811e70", + "size": 28552 + }, + { + "path": "plugins/agent-stack/skills/agent-orchestrator/references/kv-cache.md", + "sha256": "b8ae7c5ba9531ebb02ded2e403a8fcb4ae2b944940076a4f15f966d05f6caeae", + "size": 9911 + }, + { + "path": "plugins/agent-stack/skills/agent-orchestrator/references/llm-proxy-billing.md", + "sha256": "84c2144d6a5a917c042c5e31ecfacfe6238c60870da567abc00bd770e3bccccd", + "size": 19674 + }, + { + "path": "plugins/agent-stack/skills/agent-orchestrator/references/memory-architecture.md", + "sha256": "860466e4e54d0116063146401c3df54a25bbf07cdfba18b63d0fb0e06a285f1a", + "size": 20225 + }, + { + "path": "plugins/agent-stack/skills/agent-orchestrator/references/memory-landscape.md", + "sha256": "b297b121ebb07c50579f929f0e5361598625b024dbf49653b82e12b1dc0b4f4d", + "size": 8296 + }, + { + "path": "plugins/agent-stack/skills/agent-orchestrator/references/memory-lifecycle.md", + "sha256": "6c7252553bc6308e6f5ec88bb802a62e4bacd98f7946ec5b2e82fef8c1aa0073", + "size": 11365 + }, + { + "path": "plugins/agent-stack/skills/agent-orchestrator/references/patterns.md", + "sha256": "a239bc4549339b049b54b154f88536bed68256ffffdcdc4a0f41cf7185eeb360", + "size": 25555 + }, + { + "path": "plugins/agent-stack/skills/agent-orchestrator/references/pipeline.md", + "sha256": "89f160068715136c6f7793b305f78371831ebef9527cdb481e15b5af9a1af40b", + "size": 4549 + }, + { + "path": "plugins/agent-stack/skills/agent-orchestrator/references/provider-lifecycle.md", + "sha256": "d9b4462baf1c805daf05612d7ee8556ac5e666f72de62609666f1ee469294de3", + "size": 8966 + }, + { + "path": "plugins/agent-stack/skills/agent-orchestrator/references/runtime.md", + "sha256": "4c49fe64e16256fd78529e4a557782c5ccd9be8af8a5703bb92a773bdea12d70", + "size": 13205 + }, + { + "path": "plugins/agent-stack/skills/agent-orchestrator/SKILL.md", + "sha256": "8a80583133707ea69c9e7c0963a68f74b138b74e5148e31ceeea12097af32aba", + "size": 20286 + } + ], + "scope": "source payload parity; registry metadata read, immutable tarball not re-downloaded" +} diff --git a/test/audit_regressions/fw01_discovery.py b/test/audit_regressions/fw01_discovery.py new file mode 100644 index 0000000..5103316 --- /dev/null +++ b/test/audit_regressions/fw01_discovery.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""FW01 discovery admission: real permissions plus public-validator controls.""" +import ast +import os +from pathlib import Path +import shutil +import subprocess +import sys + +sys.dont_write_bytecode = True +TEST = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(TEST)) +import residue + +source = ast.parse((TEST / 'validate.py').read_text()) +scanner = next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == 'temp_scan_files') +skip = next(ast.literal_eval(n.value) for n in source.body if isinstance(n, ast.Assign) + and any(isinstance(t, ast.Name) and t.id == 'TEMP_SCAN_SKIP_DIRS' for t in n.targets)) +errors = [] +guard = {'os': os, 'fail': errors.append, 'TEMP_SCAN_SKIP_DIRS': skip} +exec(compile(ast.Module(body=[scanner], type_ignores=[]), 'actual-scanner', 'exec'), guard) +scan = guard['temp_scan_files'] +COUNT = 0 + + +def check(value, label): + global COUNT + COUNT += 1 + assert value, label + + +def deny(path): + path.chmod(0) + try: + list(path.iterdir()) + except PermissionError: + return + raise AssertionError('permission plant ineffective; cannot claim this control under current privileges') + + +residue.open_case('source discovery admission') +root = Path(residue.workspace('discovery')) +hidden = root / 'scripts/hidden'; hidden.mkdir(parents=True) +source_file = hidden / 'plain.py'; source_file.write_text('answer = 1\n') +check(scan(root) == [str(source_file)] and not errors, 'healthy discovery sees source') +try: + deny(hidden) + errors.clear(); scan(root) + check(any('scripts/hidden' in e and 'PermissionError' in e for e in errors), 'unreadable directory records discovery failure') +finally: + hidden.chmod(0o700) +errors.clear(); check(scan(root) == [str(source_file)] and not errors, 'restored directory is readable') +excluded = root / 'node_modules'; excluded.mkdir(); (excluded / 'excluded.py').write_text('answer=1\n') +try: + deny(excluded) + errors.clear(); check(scan(root) == [str(source_file)] and not errors, 'declared dependency exclusion is unchanged') +finally: + excluded.chmod(0o700) +errors.clear(); scan(root / 'missing') +check(any('FileNotFoundError' in e for e in errors), 'absent discovery root refuses') +residue.close_case('source discovery admission') + +# An owned isolated source checkout, excluding Git/dependencies/cache; no full suite. +residue.open_case('whole validator discovery') +root = Path(residue.workspace('public-discovery')) / 'repo' +shutil.copytree(TEST.parent, root, ignore=shutil.ignore_patterns('.git', 'node_modules', '__pycache__'), symlinks=True) +hidden = root / 'scripts/fw01_hidden'; hidden.mkdir(parents=True) +plant = hidden / 'leak.py' + +def run(): + return subprocess.run([sys.executable, '-B', str(root / 'test/validate.py')], cwd=root, + capture_output=True, text=True, timeout=90, + env=dict(os.environ, PYTHONDONTWRITEBYTECODE='1')) + +r = run(); check(r.returncode == 0, 'healthy whole validator: ' + r.stdout + r.stderr) +plant.write_text('import tempfile\np = tempfile.' + 'mkd' + 'temp()\n') +r = run(); check(r.returncode != 0 and 'fw01_hidden/leak.py' in r.stdout + r.stderr, 'readable unauthorized maker still refused') +try: + deny(hidden) + r = run() + check(r.returncode != 0 and 'fw01_hidden: temp tree scan cannot read directory (PermissionError)' in r.stdout + r.stderr, + 'actual whole validator refuses incomplete discovery with intended diagnostic') +finally: + hidden.chmod(0o700) +plant.unlink() +r = run(); check(r.returncode == 0, 'restoration whole validator passes: ' + r.stdout + r.stderr) +residue.close_case('whole validator discovery') +residue.report() +print(f'PASS: FW01 discovery admission — {COUNT} controls') diff --git a/test/validate.py b/test/validate.py index 5702ab8..4d11110 100644 --- a/test/validate.py +++ b/test/validate.py @@ -968,7 +968,10 @@ def temp_maker_hit(text): def temp_scan_files(root): found = [] - for dirpath, dirnames, filenames in os.walk(root): + def cannot_read(error): + relative = os.path.relpath(error.filename or root, root) + fail(f"{relative}: temp tree scan cannot read directory ({error.__class__.__name__})") + for dirpath, dirnames, filenames in os.walk(root, onerror=cannot_read): dirnames[:] = sorted(d for d in dirnames if d not in TEMP_SCAN_SKIP_DIRS) found.extend(os.path.join(dirpath, f) for f in filenames if f.endswith(".py")) return sorted(found)