Adopt ShellKit: route IO / FS / network / identity / exit through Shell.current#2
Merged
Adopt ShellKit: route IO / FS / network / identity / exit through Shell.current#2
Conversation
…ll.current SwiftScript now reads its host-touching surface from `ShellKit.Shell.current` instead of Foundation directly. The same script body runs equally well standalone (Shell.processDefault wraps real fd 0/1/2 + FileManager.default + ProcessInfo.processInfo) or under an embedder that has bound a custom Shell with capturing sinks, a Sandbox URL gate, a NetworkConfig allow-list, and a synthetic HostInfo. Why: SwiftBash wants to register `swift-script` as a script interpreter (issue Cocoanetics/SwiftBash#9). Doing that without this refactor leaks every `print`, file open, URL fetch, and identity read past the bash sandbox. ShellKit is the contract bash already uses for those surfaces (Cocoanetics/SwiftBash#11), so SwiftScript joins the lane. What changed: * `Interpreter.output: (String) -> Void` defaults to `Shell.current.stdout($0)`. New `Interpreter.error: (String) -> Void` defaults to `Shell.current.stderr`. Tests that override these no longer need to add a trailing newline; the closure receives verbatim bytes (incl. terminator). * `Interpreter+Calls.swift::tryPrintCall` no longer falls through to `Swift.print` when `terminator: ""` — the call now routes through `output(body + terminator)`. Earlier code leaked non-`\n`-terminated output to the host's real fd 1, bypassing the bound shell. * `readLine()` reads from `Shell.current.stdin.readLine()`, so embedded pipelines feed input via an `InputSource`. * `ScriptExit: Error` carries an `ExitStatus`; `exit(_:)` and `abort()` throw it. New `Interpreter.evalScript(_:fileName:)` catches and returns it as `ExitStatus`. The legacy `eval` path rethrows so existing callers can still observe it. * `CommandLine.arguments` is bound automatically at `eval` time from `interpreter.scriptArguments` (when set) or `Shell.current.scriptName` + `positionalParameters`. The CLI no longer registers it manually. * New `HostHooks.swift` exposes top-level `authorizePath(_:for:)`, `authorizeURL(_:method:)`, `hostUserName()`, `hostNameOverride()`, `hostProcessIdentifier()`, `hostProcessName()`, `hostEnvironment()`, `hostProcessArguments()`. These read `Shell.current.{sandbox,networkConfig,hostInfo,environment, scriptName,positionalParameters}`. * `BridgeGeneratorTool` learns three policies: - `gates(...)`: scan signatures for `String`/`URL` args on FileManager (intent picked from method name), URLSession, String/Data file-IO inits, and emit `try await authorizePath` / `authorizeURL` calls in the generated body. Denials wrap as `UserThrowSignal` so they surface through script-side `try/catch` like any other Foundation error. - `redirectedPropertyCall(...)`: redirect `ProcessInfo.{userName, fullUserName,hostName,processIdentifier,processName,environment, arguments}` and `FileManager.currentDirectoryPath` to the bound shell's `HostInfo` / `Environment` / `scriptName` instead of Foundation defaults. - `comparableUnavailableAtFloor`: maintains a small list of types whose `Comparable` conformance lands on a newer OS than the deployment floor (today: `UUID`); the comparator emit falls back to Equatable-only ordering. - Deployment floor lowered to macOS 13 to match SwiftBash. Any Foundation symbol whose `@available` requires a newer OS is skipped via the existing `isDeprecated` check. * `IdentityModule`: hand-rolled bridges for ProcessInfo's `userName` / `fullUserName` / `processIdentifier` / `arguments` / `environment` — the generator can't bridge Int32 / [String] / [String:String] but the helpers in HostHooks return the right shapes. * `Interpreter+FileIO.swift` (the hand-rolled FileManager.default / String(contentsOfFile:) / String.write(toFile:) fast-path) now calls `authorizePath` before each disk touch. Without this, a script that uses `FileManager.default.fileExists(...)` would bypass the bridge dict entirely and reach the host's real disk even with a sandbox bound. * `URLSessionModule`: hand-rolled `URLSession.bytes(from:)` calls `authorizeURL` before opening a connection. * Foundation bridge files now `import ShellKit` (added by the generator's header emit). * `Package.swift` lifts the floor from macOS 26 to macOS 13 / iOS 16; depends on `Cocoanetics/ShellKit` `main`. * `swift-script` CLI: stdio routes through `Shell.current` (so passthrough mode behaves identically to before); CommandLine. arguments bridge no longer needs manual registration; file-script path uses `evalScript(_:fileName:)` to honor `exit(N)`. * Bridges regenerated via `Tools/regen-foundation-bridge.sh` (the script's `BRIDGE_TARGET` now defaults to `arm64-apple-macos13.0`). * `ShellKitIntegrationTests` exercises stdout / stderr / stdin / CommandLine.arguments / exit / sandbox-allow / sandbox-deny / ProcessInfo identity end-to-end. Suite: 412 → 426 tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`isDeprecated` previously checked only `domain == "macOS"`, so a Foundation symbol available on macOS 13.0 but introduced in iOS 16.1 (URLRequest.requiresDNSSECValidation, etc.) sailed through the filter and broke the iOS build of every consumer. Add iOS / tvOS / watchOS deployment-floor constants (matching the SwiftBash floor: iOS 16 / tvOS 16 / watchOS 9) and a domain → floor lookup so `introduced` and `deprecated` checks apply to all four Apple platforms uniformly. The deprecation branch also got slightly more correct: previously ANY non-macOS deprecation triggered a skip; now it only skips when the deprecation version is at-or-below the platform's floor (so a symbol deprecated in iOS 17 is still bridged for an iOS-16 build, matching swiftc's own behaviour). Net regen: 2680 → 2695 Foundation bridges, 187 → 189 stdlib bridges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cc24e2acaf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ndex 1+
The previous `gates(...)` only authorized URLSession calls when the
first arg was a `URL` and skipped index-1+ args entirely. That left
two real holes the codex reviewer flagged on the SwiftBash#9 PR:
1. `URLSession.data(for:)`, `download(for:)`, `upload(for:from:)`,
`upload(for:fromFile:)`, `bytes(for:)` — every URLRequest-shaped
overload — sailed past the network policy. A script could set
custom headers / method via URLRequest and reach any URL outside
the allow-list. Now the generator emits a `URLRequest` arg-bind
followed by `authorizeURL(req.url ?? URL(fileURLWithPath: ""),
method: req.httpMethod ?? "GET")` so the URL allow-list and
method gate fire identically to the bare-URL overloads.
2. `URLSession.upload(for:fromFile:)`'s second arg is a local-file
`URL` the session reads to upload — previously unguarded. Now
gated as `.fsRead` so a sandboxed embedder denies the file
access before the upload starts.
`renderGates` learns a `.networkRequest` GateKind that knows how
to extract `.url` and `.httpMethod` from a `URLRequest` to build
the authorize call. Auto-genned bridges now look like:
"func URLSession.upload()": .method { receiver, args in
...
let arg0 = try unboxOpaque(args[0], as: URLRequest.self, ...)
do {
try await authorizeURL(
arg0.url ?? URL(fileURLWithPath: ""),
method: arg0.httpMethod ?? "GET")
} catch {
throw UserThrowSignal(value: ...)
}
let arg1 = try unboxOpaque(args[1], as: URL.self, ...)
do {
try await authorizePath(arg1, for: .read)
} catch {
throw UserThrowSignal(value: ...)
}
...
}
A request constructed from a `nil`-URL component falls back to
the `URL(fileURLWithPath: "")` sentinel — the network policy
denies it explicitly rather than silently skipping the gate.
Every other Foundation IO surface still tracked in the broader
inventory follow-up (Cocoanetics/SwiftBash#13).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
odrobnik
added a commit
to Cocoanetics/SwiftBash
that referenced
this pull request
May 8, 2026
Closes #9. A path-invoked simple command (`./hello.swift`, `/abs/path/script.foo`) that the registry doesn't resolve falls through to a shebang-dispatch path: read the file, parse the `#!`-line, look up a registered `ScriptInterpreter`, run it inside a fresh `Shell.copy()` so positional parameters / `$0` / errexit toggling stay scoped. The SwiftScript binding is a thin bridge because SwiftScript adopted ShellKit (Cocoanetics/SwiftScript#2) — it routes IO / FS / network / identity / exit through `Shell.current`, the same TaskLocal SwiftBash binds for every dispatch. Output, stdin, sandbox confinement, network allow-list, and synthetic identity all work without per-call wiring. What's in the diff: * `BashInterpreter`: - `ScriptInterpreter` protocol + `ScriptInterpreterContext` + `ClosureScriptInterpreter` — language-agnostic registry; any embedder can add Python, Lua, etc. the same way. - `Shell+ScriptInterpreters.swift`: registration helpers, `parseShebangLine(_:)` (handles plain shebangs, `#!/usr/bin/env <name>`, `#!/usr/bin/env -S <name> --flag`, walks past KEY=value), `stripShebang(_:)` that drops the shebang content but preserves the trailing newline so interpreter diagnostics line up with the original file (`swift` itself counts the shebang as line 1). - `Shell+ExternalScript.swift`: dispatcher that reads the candidate file via `Shell.fileSystem`, applies bash-style diagnostics (missing → 127, directory → 126, no execute bit → 126 / Permission denied, FS error → 126 / message), and invokes the registered interpreter inside a `Shell.copy()` subshell. - `Shell.scriptInterpreters: [String: ScriptInterpreter]` field, propagated via `copy()`. - `Shell+Run.swift::executeSimpleCommand` falls through to the dispatcher before `command not found`. - `FileSystemError.shellMessage()` helper for bash-style short diagnostics (`Permission denied`, `Is a directory`, …). * `BashSwiftScript` (new library + product): - `SwiftScriptShellInterpreter` — `ScriptInterpreter` adapter: constructs a fresh `SwiftScriptInterpreter.Interpreter` per script, calls `evalScript(_:fileName:)`, renders parse + runtime errors via the interpreter's own renderer (which routes through `Shell.current.stderr`), translates cancellation to bash's 128+SIGTERM = 143. - `Shell+SwiftScript.swift::registerSwiftScript(names:)` registers under `["swift-script", "swift"]` by default. * `swift-bash exec`: - Calls `registerSwiftScript()` automatically. - `--sandbox HOST_DIR` now ALSO binds `Shell.sandbox = ShellKit.Sandbox.rooted(at: HOST_DIR)` — the URL gate the SwiftScript bridges consult. Earlier the flag only bound the legacy `Shell.fileSystem` overlay (used by bash builtins) and SwiftScript bypassed confinement. Full unification of the two confinement mechanisms is tracked in issue #10. Documented in `Docs/SwiftScript.md`. - Detects non-bash shebangs in the source: when `parseShebangLine` returns an interpreter that's registered as a `ScriptInterpreter`, the CLI synthesises a quoted bash command line (`'<resolved-path>' '<arg1>' …`) and runs that, so the dispatcher's shebang-fallthrough fires. Stdin / `/dev/fd/N` paths still run the source as bash because the stream is already consumed. * Tests: - `Tests/BashInterpreterTests/ScriptInterpreterTests.swift` — 25 tests covering shebang parsing (env-prefix, env-S, KEY=value), strip semantics, dispatch positive / not-found / is-directory / non-executable / unreadable / unregistered / inheritance into subshells. - `Tests/BashSwiftScriptTests/BashSwiftScriptTests.swift` — 11 end-to-end tests through the bash dispatcher: hello-world, bare `swift` shebang, `CommandLine.arguments` plumbing, exit code propagation through `$?`, parse + runtime diagnostics, line-number preservation, subshell positional isolation, stdin feed via `echo input | ./script.swift`, sandbox-rooted disk denial, synthetic-identity ProcessInfo readout. * Docs: new `Docs/SwiftScript.md` (with the `--sandbox` known-limitations note and the issue #13 / #10 follow-ups). Updates to `Docs/BashInterpreter.md`, `README.md`, `AGENTS.md`. Cross-platform: macOS native (1760 tests pass), iOS via `xcodebuild -destination 'generic/platform=iOS' build`, Linux via `swift:6.2-jammy` Docker. CI handles the full matrix. Suite: 1726 → 1760 tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
odrobnik
added a commit
to Cocoanetics/SwiftBash
that referenced
this pull request
May 8, 2026
Closes #9. A path-invoked simple command (`./hello.swift`, `/abs/path/script.foo`) that the registry doesn't resolve falls through to a shebang-dispatch path: read the file, parse the `#!`-line, look up a registered `ScriptInterpreter`, run it inside a fresh `Shell.copy()` so positional parameters / `$0` / errexit toggling stay scoped. The SwiftScript binding is a thin bridge because SwiftScript adopted ShellKit (Cocoanetics/SwiftScript#2) — it routes IO / FS / network / identity / exit through `Shell.current`, the same TaskLocal SwiftBash binds for every dispatch. Output, stdin, sandbox confinement, network allow-list, and synthetic identity all work without per-call wiring. What's in the diff: * `BashInterpreter`: - `ScriptInterpreter` protocol + `ScriptInterpreterContext` + `ClosureScriptInterpreter` — language-agnostic registry; any embedder can add Python, Lua, etc. the same way. - `Shell+ScriptInterpreters.swift`: registration helpers, `parseShebangLine(_:)` (handles plain shebangs, `#!/usr/bin/env <name>`, `#!/usr/bin/env -S <name> --flag`, walks past KEY=value), `stripShebang(_:)` that drops the shebang content but preserves the trailing newline so interpreter diagnostics line up with the original file (`swift` itself counts the shebang as line 1). - `Shell+ExternalScript.swift`: dispatcher that reads the candidate file via `Shell.fileSystem`, applies bash-style diagnostics (missing → 127, directory → 126, no execute bit → 126 / Permission denied, FS error → 126 / message), and invokes the registered interpreter inside a `Shell.copy()` subshell. - `Shell.scriptInterpreters: [String: ScriptInterpreter]` field, propagated via `copy()`. - `Shell+Run.swift::executeSimpleCommand` falls through to the dispatcher before `command not found`. - `FileSystemError.shellMessage()` helper for bash-style short diagnostics (`Permission denied`, `Is a directory`, …). * `BashSwiftScript` (new library + product): - `SwiftScriptShellInterpreter` — `ScriptInterpreter` adapter: constructs a fresh `SwiftScriptInterpreter.Interpreter` per script, calls `evalScript(_:fileName:)`, renders parse + runtime errors via the interpreter's own renderer (which routes through `Shell.current.stderr`), translates cancellation to bash's 128+SIGTERM = 143. - `Shell+SwiftScript.swift::registerSwiftScript(names:)` registers under `["swift-script", "swift"]` by default. * `swift-bash exec`: - Calls `registerSwiftScript()` automatically. - `--sandbox HOST_DIR` now ALSO binds `Shell.sandbox = ShellKit.Sandbox.rooted(at: HOST_DIR)` — the URL gate the SwiftScript bridges consult. Earlier the flag only bound the legacy `Shell.fileSystem` overlay (used by bash builtins) and SwiftScript bypassed confinement. Full unification of the two confinement mechanisms is tracked in issue #10. Documented in `Docs/SwiftScript.md`. - Detects non-bash shebangs in the source: when `parseShebangLine` returns an interpreter that's registered as a `ScriptInterpreter`, the CLI synthesises a quoted bash command line (`'<resolved-path>' '<arg1>' …`) and runs that, so the dispatcher's shebang-fallthrough fires. Stdin / `/dev/fd/N` paths still run the source as bash because the stream is already consumed. * Tests: - `Tests/BashInterpreterTests/ScriptInterpreterTests.swift` — 25 tests covering shebang parsing (env-prefix, env-S, KEY=value), strip semantics, dispatch positive / not-found / is-directory / non-executable / unreadable / unregistered / inheritance into subshells. - `Tests/BashSwiftScriptTests/BashSwiftScriptTests.swift` — 11 end-to-end tests through the bash dispatcher: hello-world, bare `swift` shebang, `CommandLine.arguments` plumbing, exit code propagation through `$?`, parse + runtime diagnostics, line-number preservation, subshell positional isolation, stdin feed via `echo input | ./script.swift`, sandbox-rooted disk denial, synthetic-identity ProcessInfo readout. * Docs: new `Docs/SwiftScript.md` (with the `--sandbox` known-limitations note and the issue #13 / #10 follow-ups). Updates to `Docs/BashInterpreter.md`, `README.md`, `AGENTS.md`. Cross-platform: macOS native (1760 tests pass), iOS via `xcodebuild -destination 'generic/platform=iOS' build`, Linux via `swift:6.2-jammy` Docker. CI handles the full matrix. Suite: 1726 → 1760 tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
odrobnik
added a commit
to Cocoanetics/SwiftBash
that referenced
this pull request
May 8, 2026
Closes #9. A path-invoked simple command (`./hello.swift`, `/abs/path/script.foo`) that the registry doesn't resolve falls through to a shebang-dispatch path: read the file, parse the `#!`-line, look up a registered `ScriptInterpreter`, run it inside a fresh `Shell.copy()` so positional parameters / `$0` / errexit toggling stay scoped. The SwiftScript binding is a thin bridge because SwiftScript adopted ShellKit (Cocoanetics/SwiftScript#2) — it routes IO / FS / network / identity / exit through `Shell.current`, the same TaskLocal SwiftBash binds for every dispatch. Output, stdin, sandbox confinement, network allow-list, and synthetic identity all work without per-call wiring. What's in the diff: * `BashInterpreter`: - `ScriptInterpreter` protocol + `ScriptInterpreterContext` + `ClosureScriptInterpreter` — language-agnostic registry; any embedder can add Python, Lua, etc. the same way. - `Shell+ScriptInterpreters.swift`: registration helpers, `parseShebangLine(_:)` (handles plain shebangs, `#!/usr/bin/env <name>`, `#!/usr/bin/env -S <name> --flag`, walks past KEY=value), `stripShebang(_:)` that drops the shebang content but preserves the trailing newline so interpreter diagnostics line up with the original file (`swift` itself counts the shebang as line 1). - `Shell+ExternalScript.swift`: dispatcher that reads the candidate file via `Shell.fileSystem`, applies bash-style diagnostics (missing → 127, directory → 126, no execute bit → 126 / Permission denied, FS error → 126 / message), and invokes the registered interpreter inside a `Shell.copy()` subshell. - `Shell.scriptInterpreters: [String: ScriptInterpreter]` field, propagated via `copy()`. - `Shell+Run.swift::executeSimpleCommand` falls through to the dispatcher before `command not found`. - `FileSystemError.shellMessage()` helper for bash-style short diagnostics (`Permission denied`, `Is a directory`, …). * `BashSwiftScript` (new library + product): - `SwiftScriptShellInterpreter` — `ScriptInterpreter` adapter: constructs a fresh `SwiftScriptInterpreter.Interpreter` per script, calls `evalScript(_:fileName:)`, renders parse + runtime errors via the interpreter's own renderer (which routes through `Shell.current.stderr`), translates cancellation to bash's 128+SIGTERM = 143. - `Shell+SwiftScript.swift::registerSwiftScript(names:)` registers under `["swift-script", "swift"]` by default. * `swift-bash exec`: - Calls `registerSwiftScript()` automatically. - `--sandbox HOST_DIR` now ALSO binds `Shell.sandbox = ShellKit.Sandbox.rooted(at: HOST_DIR)` — the URL gate the SwiftScript bridges consult. Earlier the flag only bound the legacy `Shell.fileSystem` overlay (used by bash builtins) and SwiftScript bypassed confinement. Full unification of the two confinement mechanisms is tracked in issue #10. Documented in `Docs/SwiftScript.md`. - Detects non-bash shebangs in the source: when `parseShebangLine` returns an interpreter that's registered as a `ScriptInterpreter`, the CLI synthesises a quoted bash command line (`'<resolved-path>' '<arg1>' …`) and runs that, so the dispatcher's shebang-fallthrough fires. Stdin / `/dev/fd/N` paths still run the source as bash because the stream is already consumed. * Tests: - `Tests/BashInterpreterTests/ScriptInterpreterTests.swift` — 25 tests covering shebang parsing (env-prefix, env-S, KEY=value), strip semantics, dispatch positive / not-found / is-directory / non-executable / unreadable / unregistered / inheritance into subshells. - `Tests/BashSwiftScriptTests/BashSwiftScriptTests.swift` — 11 end-to-end tests through the bash dispatcher: hello-world, bare `swift` shebang, `CommandLine.arguments` plumbing, exit code propagation through `$?`, parse + runtime diagnostics, line-number preservation, subshell positional isolation, stdin feed via `echo input | ./script.swift`, sandbox-rooted disk denial, synthetic-identity ProcessInfo readout. * Docs: new `Docs/SwiftScript.md` (with the `--sandbox` known-limitations note and the issue #13 / #10 follow-ups). Updates to `Docs/BashInterpreter.md`, `README.md`, `AGENTS.md`. * CI: SwiftBash's `build-windows` job inherited the `vcpkg install zlib` step from upstream but missed the alias hop the SwiftPorts workflow uses. vcpkg's `x64-windows-static-md` triplet ships zlib as `zs.lib`, but `CZlib`'s modulemap declares `link "z"` (mirroring libarchive's `linkedLibrary("z")`), so lld-link asks for `z.lib`. Port the alias step (`Copy-Item zs.lib z.lib`) into `.github/workflows/swift.yml` so the link resolves, then drop the unnecessary `bashSwiftScriptPlatforms` gate + `#if canImport( SwiftScriptInterpreter)` guards that were a defensive guess based on the (truncated) original lld-link error. Cross-platform: macOS native (1760 tests pass), iOS via `xcodebuild -destination 'generic/platform=iOS' build`, Linux via `swift:6.2-jammy` Docker. CI handles the full matrix. Suite: 1726 → 1760 tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
odrobnik
added a commit
to Cocoanetics/SwiftBash
that referenced
this pull request
May 8, 2026
Closes #9. A path-invoked simple command (`./hello.swift`, `/abs/path/script.foo`) that the registry doesn't resolve falls through to a shebang-dispatch path: read the file, parse the `#!`-line, look up a registered `ScriptInterpreter`, run it inside a fresh `Shell.copy()` so positional parameters / `$0` / errexit toggling stay scoped. The SwiftScript binding is a thin bridge because SwiftScript adopted ShellKit (Cocoanetics/SwiftScript#2) — it routes IO / FS / network / identity / exit through `Shell.current`, the same TaskLocal SwiftBash binds for every dispatch. Output, stdin, sandbox confinement, network allow-list, and synthetic identity all work without per-call wiring. What's in the diff: * `BashInterpreter`: - `ScriptInterpreter` protocol + `ScriptInterpreterContext` + `ClosureScriptInterpreter` — language-agnostic registry; any embedder can add Python, Lua, etc. the same way. - `Shell+ScriptInterpreters.swift`: registration helpers, `parseShebangLine(_:)` (handles plain shebangs, `#!/usr/bin/env <name>`, `#!/usr/bin/env -S <name> --flag`, walks past KEY=value), `stripShebang(_:)` that drops the shebang content but preserves the trailing newline so interpreter diagnostics line up with the original file (`swift` itself counts the shebang as line 1). - `Shell+ExternalScript.swift`: dispatcher that reads the candidate file via `Shell.fileSystem`, applies bash-style diagnostics (missing → 127, directory → 126, no execute bit → 126 / Permission denied, FS error → 126 / message), and invokes the registered interpreter inside a `Shell.copy()` subshell. - `Shell.scriptInterpreters: [String: ScriptInterpreter]` field, propagated via `copy()`. - `Shell+Run.swift::executeSimpleCommand` falls through to the dispatcher before `command not found`. - `FileSystemError.shellMessage()` helper for bash-style short diagnostics (`Permission denied`, `Is a directory`, …). * `BashSwiftScript` (new library + product): - `SwiftScriptShellInterpreter` — `ScriptInterpreter` adapter: constructs a fresh `SwiftScriptInterpreter.Interpreter` per script, calls `evalScript(_:fileName:)`, renders parse + runtime errors via the interpreter's own renderer (which routes through `Shell.current.stderr`), translates cancellation to bash's 128+SIGTERM = 143. - `Shell+SwiftScript.swift::registerSwiftScript(names:)` registers under `["swift-script", "swift"]` by default. * `swift-bash exec`: - Calls `registerSwiftScript()` automatically. - `--sandbox HOST_DIR` now ALSO binds `Shell.sandbox = ShellKit.Sandbox.rooted(at: HOST_DIR)` — the URL gate the SwiftScript bridges consult. Earlier the flag only bound the legacy `Shell.fileSystem` overlay (used by bash builtins) and SwiftScript bypassed confinement. Full unification of the two confinement mechanisms is tracked in issue #10. Documented in `Docs/SwiftScript.md`. - Detects non-bash shebangs in the source: when `parseShebangLine` returns an interpreter that's registered as a `ScriptInterpreter`, the CLI synthesises a quoted bash command line (`'<resolved-path>' '<arg1>' …`) and runs that, so the dispatcher's shebang-fallthrough fires. Stdin / `/dev/fd/N` paths still run the source as bash because the stream is already consumed. * Tests: - `Tests/BashInterpreterTests/ScriptInterpreterTests.swift` — 25 tests covering shebang parsing (env-prefix, env-S, KEY=value), strip semantics, dispatch positive / not-found / is-directory / non-executable / unreadable / unregistered / inheritance into subshells. - `Tests/BashSwiftScriptTests/BashSwiftScriptTests.swift` — 11 end-to-end tests through the bash dispatcher: hello-world, bare `swift` shebang, `CommandLine.arguments` plumbing, exit code propagation through `$?`, parse + runtime diagnostics, line-number preservation, subshell positional isolation, stdin feed via `echo input | ./script.swift`, sandbox-rooted disk denial, synthetic-identity ProcessInfo readout. * Docs: new `Docs/SwiftScript.md` (with the `--sandbox` known-limitations note and the issue #13 / #10 follow-ups). Updates to `Docs/BashInterpreter.md`, `README.md`, `AGENTS.md`. * CI: SwiftBash's `build-windows` job inherited the `vcpkg install zlib` step from upstream but missed the alias hop the SwiftPorts workflow uses. vcpkg's `x64-windows-static-md` triplet ships zlib as `zs.lib`, but `CZlib`'s modulemap declares `link "z"` (mirroring libarchive's `linkedLibrary("z")`), so lld-link asks for `z.lib`. Port the alias step (`Copy-Item zs.lib z.lib`) into `.github/workflows/swift.yml` so the link resolves, then drop the unnecessary `bashSwiftScriptPlatforms` gate + `#if canImport( SwiftScriptInterpreter)` guards that were a defensive guess based on the (truncated) original lld-link error. Cross-platform: macOS native (1760 tests pass), iOS via `xcodebuild -destination 'generic/platform=iOS' build`, Linux via `swift:6.2-jammy` Docker. CI handles the full matrix. Suite: 1726 → 1760 tests. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 tasks
odrobnik
added a commit
that referenced
this pull request
May 9, 2026
* Gate every Foundation IO door behind the host sandbox Closes #3. PR #2 routed IO/FS/network/identity/exit through `ShellKit.Shell.current` but the generator's `gates(...)` policy only inserted authorize-path / authorize-URL calls for FileManager path-arg methods, URLSession URL-arg methods, and the `String`/`Data` file-IO entry points. Every other Foundation IO door — `FileHandle(forReadingAtPath:)`, `Bundle(path:)`, `InputStream(url:)`, `FileWrapper(url:options:)`, `Process()` — still reached the host's real disk / process table even under a rooted Sandbox. This change closes the gaps listed in #3's "Known gaps" table. What changed: * New bridge receivers: `FileHandle`, `Bundle`, `InputStream`, `OutputStream`, `FileWrapper`, `Process`. The first five each get path/URL gating in `gates(...)` so a script can't open a file outside the sandbox via a class init. `Process` is denied entirely under any sandbox via a new `ProcessSandboxDenied` error from `denyProcessIfSandboxed()` — `Foundation.Process` spawns a real OS subprocess that escapes every host gate, and ShellKit's closure-based `ProcessTable` doesn't take a path-to-exec, so there's nothing to redirect to. * Generator: extend per-receiver rules with a generalised parameter-label scan. New `pathStringLabelsRead/Write` and `urlLabelsRead/Write` sets describe which `String`/`URL` argument labels carry filesystem paths; `scanByLabel` walks every parameter position and gates the matching ones (not just position 0). This closes the holes around two-path FileManager methods — `setUbiquitous(_:itemAt:destinationURL:)` (URL args at indices 1+2), `createSymbolicLink(at:withDestinationURL:)` (both URL args are writes), `contentsEqual(atPath:andPath:)` (the second path slipped past the gate). The receiver rule supplies a default intent; the label scan upgrades it (e.g. `to:` -> `.fsWrite` even when the receiver default is read). * Cross-references for the new types: `URLRequest.httpBodyStream: InputStream?`, `URLResource.bundle: Bundle`. * Host: reject unknown HTTP verbs in `NetworkConfig.checkAllowed`. Previously an unrecognised method silently fell back to `.GET`, which evaluated the request against GET permissions while sending the actual verb — a policy-bypass. * Tests: deny + allow paths in `ShellKitIntegrationTests` for each new receiver. Covers `FileHandle(forReadingAtPath:)`, `(forWritingAtPath:)`, `Bundle(path:)`, `(url:)`, `InputStream(fileAtPath:)`, `(url:)`, `OutputStream(toFileAtPath:)`, `(url:append:)`, `FileWrapper(url:options:)`, and `Process()` deny. Bridges regenerated via `Tools/regen-foundation-bridge.sh`. The mostly-reorder noise in `FoundationBridges.swift` and `StdlibBridges.swift` is generator-output non-determinism (dict iteration order); the only truly-new lines in `FoundationBridges.swift` are the gate-injected bodies for the new receivers (54 lines). Sorting the generator's dict output deterministically is a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Gate FileWrapper.matchesContents(of:) URL before disk access `matchesContents(of:)` was bridged without an `authorizePath` check, letting a script call it with an out-of-root URL and still inspect host filesystem state — an FS sandbox bypass through the newly-added FileWrapper surface. The `of:` label isn't in `urlLabelsRead` (too generic to add globally — appears all over Foundation on non-URL args), so the prior `scanByLabel`-only branch missed it. Add a positional gate at index 0 of the FileWrapper receiver branch, mirroring how FileManager / URLSession treat their first arg. Test: `sandboxBlocksFileWrapperMatchesContentsOutsideRoot` constructs a FileWrapper(regularFileWithContents:) (no disk hop) under a rooted sandbox and asserts matchesContents(of:) on a path outside the root throws Sandbox.Denial. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Gate Process bridge for non-iOS platforms `Foundation.Process` is unavailable on the iOS family (iOS, tvOS, watchOS, visionOS). The scl oracle classified it cross-platform because Linux Foundation has it, so the generator emitted the Process bridge dict and comparator without any guard — the iOS build broke with "cannot find 'Process' in scope". Generator: introduce a third Platform case `nonIOSOnly` for types available on macOS / Linux / Windows / Android but not iOS-family. `nonIOSOnlyTypes = ["Process"]` overrides both the scl-oracle classification and the Apple-only fallback for class-typed comparators. The per-type bridge file gets wrapped in `#if !os(iOS) && !os(tvOS) && !os(watchOS) && !os(visionOS)` with a `[:]` stub on the excluded platforms so the manifest's reference still resolves; the runtime-body comparator uses the same guard. Tests: skip the two Process tests on the iOS family with a matching `#if`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Skip FileWrapper test on non-Darwin platforms `Foundation.FileWrapper` isn't available on swift-corelibs-foundation (Linux / Windows / Android) — the bridge file is already `#if canImport(Darwin)`-only, so on those platforms the script fails to resolve `FileWrapper` and the test sees a "cannot find 'FileWrapper' in scope" error instead of `Sandbox.Denial`. Match the bridge's platform gating: wrap the new test in `#if canImport(Darwin)`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SwiftScript now reads its host-touching surface from
ShellKit.Shell.currentinstead of Foundation directly. Same script body runs equally well standalone (Shell.processDefaultwraps real fd 0/1/2 +FileManager.default+ProcessInfo.processInfo) or under an embedder that has bound a customShellwith capturing sinks, aSandboxURL gate, aNetworkConfigallow-list, and a syntheticHostInfo.Why
SwiftBash wants to register
swift-scriptas a script-shebang interpreter (Cocoanetics/SwiftBash#9). Doing that without this refactor leaks everyprint, file open, URL fetch, and identity read past the bash sandbox. ShellKit is the contract bash already uses for those surfaces (Cocoanetics/SwiftBash#11) — SwiftScript joins the lane.What changed
Shell.current.{stdout,stderr,stdin}. Default closures read the bound shell, so passthrough mode behaves exactly as before.print(_:terminator:)with a non-\nterminator no longer bypassesoutputand leaks to the host's real fd 1.exit(_:)andabort()throwScriptExitcarrying anExitStatus. NewInterpreter.evalScript(_:fileName:)catches and returns it; legacyevallets it propagate.CommandLine.argumentsis bound automatically atevaltime frominterpreter.scriptArguments(when set) orShell.current.scriptName + positionalParameters.HostHooks.swiftexposes top-levelauthorizePath(_:for:),authorizeURL(_:method:),hostUserName(),hostNameOverride(),hostProcessIdentifier(),hostProcessName(),hostEnvironment(),hostProcessArguments()— all readingShell.current.BridgeGeneratorToollearns three policies:gates(...): scan signatures forString/URLargs on FileManager / URLSession / String+Data file-IO inits and emittry await authorizePath/authorizeURLcalls. Denials wrap asUserThrowSignalso script-sidetry/catchcan see them.redirectedPropertyCall(...): redirectProcessInfo.{userName,fullUserName,hostName,processIdentifier,processName,environment,arguments}andFileManager.currentDirectoryPathto the bound shell's identity / environment / scriptName.comparableUnavailableAtFloor: maintains a small list of types whoseComparableconformance lands on a newer OS than the deployment floor (today:UUID); the comparator emit falls back to Equatable-only ordering.isDeprecatedcheck now scans iOS / tvOS / watchOS domains alongside macOS, so symbols introduced in iOS 16.1 (e.g.URLRequest.requiresDNSSECValidation) get filtered for an iOS-16 build.Interpreter+FileIO.swift,URLSessionModule.swift) to call the same hooks before touching disk / network.IdentityModulehand-rollsProcessInfo.{userName,fullUserName,processIdentifier,arguments,environment}— types the generator can't bridge (Int32,[String],[String:String]).swift-scriptCLI routes stdio throughShell.current; CLI no longer needs to manually registerCommandLine.arguments; file scripts useevalScript(_:)soexit(N)propagates as the host's exit code.Tools/regen-foundation-bridge.sh(BRIDGE_TARGETdefaults toarm64-apple-macos13.0).Tests
Tests/SwiftScriptInterpreterTests/ShellKitIntegrationTests.swiftexercises stdout / stderr / stdin /CommandLine.arguments/exit/ sandbox-allow / sandbox-deny /ProcessInfoidentity end-to-end against aTestShellthat wiresShellKit.Shellup with capturing sinks. Suite: 412 → 426 tests.Cross-platform
xcodebuild -destination 'generic/platform=iOS' buildswift:6.2-jammyDocker (aarch64), fullswift build🤖 Generated with Claude Code