[eric] swarm: scan workspace file bytes for secrets on export, not just payload keys

This commit is contained in:
ciregenz
2026-06-15 13:59:11 -07:00
parent 2d82b13a1a
commit c43da8950e
3 changed files with 38 additions and 1 deletions
+18
View File
@@ -79,3 +79,21 @@ def find_denied_keys(value: Any, _path: str = "") -> list[str]:
for i, v in enumerate(value):
found.extend(find_denied_keys(v, f"{_path}[{i}]"))
return found
def _looks_secret(text: str) -> bool:
return any(pat.search(text) for pat in _CONTENT_PATTERNS)
def find_secrets_in_files(files: dict[str, bytes]) -> list[str]:
"""Paths of any file whose text body holds a secret-shaped literal. Payloads
get scrubbed key-and-content, but raw workspace files (an app's source) were
only key-scanned, so a key hardcoded in a .js would slip. Binary files are
skipped (a null byte means it isn't text someone pasted a token into)."""
hits: list[str] = []
for path, data in files.items():
if b"\x00" in data[:4096]:
continue
if _looks_secret(data.decode("utf-8", errors="ignore")):
hits.append(path)
return hits
+7 -1
View File
@@ -12,7 +12,7 @@ import shutil
import tempfile
import zipfile
from .redact import find_denied_keys
from .redact import find_denied_keys, find_secrets_in_files
MANIFEST_NAME = "manifest.json"
@@ -46,6 +46,12 @@ def pack(manifest: dict, payloads: dict[str, dict], files: dict[str, bytes]) ->
raise BundleError(
f"refusing to export: secret-shaped field(s) in {bid}: {leaked[:3]}"
)
leaky_files = find_secrets_in_files(files)
if leaky_files:
raise BundleError(
f"refusing to export: a secret-shaped value is in {leaky_files[0]}; "
"remove it (use an environment variable) and try again"
)
entries: dict[str, bytes] = {}
for bid, payload in payloads.items():
entries[f"entities/{bid}/payload.json"] = json.dumps(payload, indent=2).encode("utf-8")
+13
View File
@@ -99,6 +99,19 @@ def test_pack_refuses_denied_key():
pack({"format_version": 1}, {"bid1": {"api_key": "leak"}}, {})
def test_pack_refuses_secret_in_workspace_file():
# A key hardcoded in app source (not .env) must not ride along; pack scans
# file bytes, not just payload keys.
leak = b"const KEY = 'sk-ant-api03-AAAAAAAAAAAAAAAAAAAAAAAA';\n"
with pytest.raises(BundleError):
pack({"format_version": 1}, {"bid1": {"name": "ok"}}, {"entities/bid1/files/config.js": leak})
def test_pack_allows_clean_workspace_file():
raw = pack({"format_version": 1}, {"bid1": {"name": "ok"}}, {"entities/bid1/files/app.js": b"export default 1"})
assert zipfile.is_zipfile(io.BytesIO(raw))
def test_app_export_drops_machine_env(tmp_path, monkeypatch):
# The live .env holds the source machine's absolute paths + pinned port; it
# must never ride along. .env.example (portable) does.