Context
Cocoanetics/SwiftBash#83 moved the sandbox's virtual↔host path mapping into ShellKit (PathMapping, carried as Sandbox.pathMapping, landed in Cocoanetics/ShellKit#17). Shell.resolve(_:) now translates script-visible virtual paths (e.g. /tmp/x, /batch/x under swift-bash exec --sandbox) to the host directories that back them, and Sandbox.confined(to:) authorizes exactly that host space. SwiftPorts and SwiftBash's JS runtime are adapted (Cocoanetics/SwiftBash#88, Cocoanetics/SwiftPorts#64).
SwiftScript is not yet adapted: authorizePath (Sources/SwiftScriptInterpreter/API/HostHooks.swift) gates the literal path and the bridges then do Foundation I/O on that same literal. Under a path-mapped sandbox every virtual absolute path is therefore denied — fail-closed, safe, but FS access for SwiftScript scripts inside the SwiftBash sandbox stays limited.
The invariant that shapes the fix
Translating for the check but not the I/O (or vice versa) is an escape: authorize(translate("/tmp/x")) passes against the per-instance temp dir while FileManager would then touch the host's shared /tmp/x. The translated path must be the one the Foundation call consumes. Half-adopting is worse than not adopting — this ships whole or not at all.
Proposed change
authorizePath(_:for:) (String and URL overloads) resolves first — ShellKit.Shell.current.resolve(path) (which also anchors relative paths to the shell's virtual CWD instead of the host process CWD) — authorizes the host form, and returns it.
BridgeGeneratorTool (Sources/BridgeGeneratorTool/main.swift, gate emission around line 1117): fs-gated args bind as var, and the emission becomes arg0 = try await authorizePath(arg0, for: .read) so the Foundation call consumes the translated path. Network gates stay Void.
- Regenerate via
Tools/regen-foundation-bridge.sh — NB: regeneration on a current machine produces ~2k lines of unrelated symbol-graph drift (newer SDK than the committed extract; verified 2026-06-11). Refresh the extract in a separate commit first, or pin the regen environment, so the semantic diff stays reviewable.
- Hand-written sites consume the return too:
Interpreter+FileIO.swift, the hand-rolled String/Data bridges.
- Display stays virtual:
FileManager.currentDirectoryPath already reports the virtual CWD; FileManager.temporaryDirectory should report ShellKit.Shell.temporaryDirectory (sandbox-aware) rather than the host's shared temp root; anything echoing resolved paths folds through Shell.displayPath(for:).
- Tests: mirror SwiftBash's
ConfinedSandboxTests — a .confined sandbox over a PathMapping, script writes /tmp/x + relative paths, bytes land in the mapped host dirs, denials outside, no host path in script-visible output.
Until this lands, SwiftScript-in-sandbox keeps the post-SwiftBash#82 interim contract (virtual spellings denied, fail-closed).
🤖 Generated with Claude Code
Context
Cocoanetics/SwiftBash#83 moved the sandbox's virtual↔host path mapping into ShellKit (
PathMapping, carried asSandbox.pathMapping, landed in Cocoanetics/ShellKit#17).Shell.resolve(_:)now translates script-visible virtual paths (e.g./tmp/x,/batch/xunderswift-bash exec --sandbox) to the host directories that back them, andSandbox.confined(to:)authorizes exactly that host space. SwiftPorts and SwiftBash's JS runtime are adapted (Cocoanetics/SwiftBash#88, Cocoanetics/SwiftPorts#64).SwiftScript is not yet adapted:
authorizePath(Sources/SwiftScriptInterpreter/API/HostHooks.swift) gates the literal path and the bridges then do Foundation I/O on that same literal. Under a path-mapped sandbox every virtual absolute path is therefore denied — fail-closed, safe, but FS access for SwiftScript scripts inside the SwiftBash sandbox stays limited.The invariant that shapes the fix
Translating for the check but not the I/O (or vice versa) is an escape:
authorize(translate("/tmp/x"))passes against the per-instance temp dir whileFileManagerwould then touch the host's shared/tmp/x. The translated path must be the one the Foundation call consumes. Half-adopting is worse than not adopting — this ships whole or not at all.Proposed change
authorizePath(_:for:)(String and URL overloads) resolves first —ShellKit.Shell.current.resolve(path)(which also anchors relative paths to the shell's virtual CWD instead of the host process CWD) — authorizes the host form, and returns it.BridgeGeneratorTool(Sources/BridgeGeneratorTool/main.swift, gate emission around line 1117): fs-gated args bind asvar, and the emission becomesarg0 = try await authorizePath(arg0, for: .read)so the Foundation call consumes the translated path. Network gates stayVoid.Tools/regen-foundation-bridge.sh— NB: regeneration on a current machine produces ~2k lines of unrelated symbol-graph drift (newer SDK than the committed extract; verified 2026-06-11). Refresh the extract in a separate commit first, or pin the regen environment, so the semantic diff stays reviewable.Interpreter+FileIO.swift, the hand-rolled String/Data bridges.FileManager.currentDirectoryPathalready reports the virtual CWD;FileManager.temporaryDirectoryshould reportShellKit.Shell.temporaryDirectory(sandbox-aware) rather than the host's shared temp root; anything echoing resolved paths folds throughShell.displayPath(for:).ConfinedSandboxTests— a.confinedsandbox over aPathMapping, script writes/tmp/x+ relative paths, bytes land in the mapped host dirs, denials outside, no host path in script-visible output.Until this lands, SwiftScript-in-sandbox keeps the post-SwiftBash#82 interim contract (virtual spellings denied, fail-closed).
🤖 Generated with Claude Code