feat: pluggable broker transport + experimental Windows AppContainer backend - #52
Merged
Conversation
Add Windows support to the isolated code runner, behind a pluggable broker transport so POSIX is unchanged. Transport groundwork (verified on macOS/Linux): - Pluggable broker transport with a MessageChannel abstraction: unix domain socket on POSIX (unchanged), Windows named pipe otherwise. Broker, guest bootstrap, backends, and runner now speak endpoint+transport instead of a hard-coded socket path. Windows (experimental, verified via CI, not locally): - _winpipe: host named-pipe server whose security descriptor grants ALL APPLICATION PACKAGES + a low-integrity label, so a low-IL AppContainer can reach it over local IPC without any network hole (pywin32). - _winproc: AppContainer launch via STARTUPINFOEX security-capabilities + CreateProcess, inside a Job Object capping memory/processes and killing the tree on close (ctypes). - WindowsBackend: ties them together (AppContainer + Job Object + low integrity); import-safe on POSIX, is_available() true only on Windows. Packaging/CI: - isolation-windows extra (pywin32); Windows-only mypy overrides. - New .github/workflows/isolation.yml matrix (ubuntu/macos/windows) that installs pywin32 + bubblewrap and runs the isolation tests with the Windows path opted in via CTP_TEST_ISOLATION_WINDOWS=1. The experimental Windows broker/backend tests are gated on that opt-in so they don't block the mandatory suite. - Docs: Windows backend section + backend table/security-model updates. Signed-off-by: chris hay <chris.hay@uk.ibm.com>
…reter, pipe SD fallback CI iteration for the Windows isolation job: - Docker integration is Linux-only (Windows daemon can't run the linux image). - WindowsBackend grants ALL APPLICATION PACKAGES read+execute on the interpreter dirs so the AppContainer python can read pyvenv.cfg + stdlib. - Named-pipe server falls back to the default security descriptor if the custom AppContainer SD can't be built, so the pipe still comes up for the local guest. Signed-off-by: chris hay <chris.hay@uk.ibm.com>
…efine them) PIPE_ACCESS_DUPLEX / FILE_FLAG_FIRST_PIPE_INSTANCE aren't reliably exposed by win32con; an AttributeError was killing the pipe-server thread, so the guest never found the pipe (FileNotFoundError). Signed-off-by: chris hay <chris.hay@uk.ibm.com>
… locate pipe hang Signed-off-by: chris hay <chris.hay@uk.ibm.com>
…late pipe hang Signed-off-by: chris hay <chris.hay@uk.ibm.com>
…r drops frames) The host received list_tools and sent the reply, but the guest's asyncio Proactor named-pipe reader stopped delivering frames after the hello ack, so the guest hung. Switch the guest pipe transport to the same blocking win32 ReadFile/WriteFile (in the executor) the host uses, via a channel abstraction (_StreamChannel for unix, _PipeChannel for the Windows pipe). Signed-off-by: chris hay <chris.hay@uk.ibm.com>
…deadlock Both the host pipe server and the guest opened the named pipe as synchronous (non-overlapped) handles. On Windows a pending blocking ReadFile serializes any concurrent WriteFile on the same handle, so once the guest's read loop was parked waiting for a reply, its next request write deadlocked behind that read — the handshake's first round-trip squeaked through on a startup race, but list_tools (the second) hung with the host never receiving it. Open both ends with FILE_FLAG_OVERLAPPED and drive ReadFile/WriteFile/ ConnectNamedPipe via OVERLAPPED + GetOverlappedResult, each with its own event, so read and write no longer serialize at the handle. Serialize writes with a lock on both ends so concurrent tool-call frames don't interleave. Signed-off-by: chris hay <chris.hay@uk.ibm.com>
…can exit On the exception path the guest never closed the pipe channel. The reader parks in a blocking overlapped ReadFile inside a default-executor thread; with the handle still open that read never returns, so asyncio.run hangs waiting for the executor to drain and the guest is killed by the wall-clock timeout — reported as 'timeout' instead of 'guest_exception'. Close the channel (and cancel the reader) in a finally: CloseHandle aborts the pending overlapped read (ERROR_OPERATION_ABORTED), which the reader now maps to a clean ConnectionError, so the guest exits with its real non-zero code. Signed-off-by: chris hay <chris.hay@uk.ibm.com>
guest_bootstrap.py runs in a separate guest process/container so parent-process coverage never sees it on any platform, and _winpipe/_winproc/backends.windows are Windows-only. All are exercised by the dedicated isolation CI (isolation.yml: Docker on Linux, AppContainer + named pipe on Windows), not the mandatory Linux unit-coverage run, where they only dragged the total under the 70% gate. Signed-off-by: chris hay <chris.hay@uk.ibm.com>
…pluggable broker transport Signed-off-by: chris hay <chris.hay@uk.ibm.com>
… bounded broker Security fixes to the tool broker (the guest runs untrusted code and holds the broker token, so it can craft raw frames): - Namespace authority: a host-pinned namespace is enforced — a mismatching guest-supplied namespace is rejected, and tool resolution uses the dotted namespace.name form so the strategy resolves strictly in that namespace with no cross-namespace fuzzy fallback. Closes a namespace-restriction bypass. - Namespace-qualified allowlists: allowed_tools entries may be bare or namespace-qualified so an allowed name can't select a same-named tool elsewhere. - Canonical execution: brokered calls run through ToolExecutor instead of a direct tool.execute(), so sandboxed calls get the same wrappers/guards/observability; callers may inject a fully-wrapped executor. - Bounded lifecycle: in-flight host calls are tracked and cancelled on aclose(), and calls are refused once the guest has reported its result. Also: migrate IsolationLimits/IsolatedResult to Pydantic (frozen + extra=forbid for limits, field-level validation); extract the broker RPC protocol vocabulary (methods, frame/payload keys, transports, default namespace, debug env) into shared _wire constants used by host and guest instead of scattered string literals; add adversarial protocol-boundary tests (namespace substitution, qualified allowlist, call-after-result, concurrency, malformed frames) taking broker.py to 100% line coverage. Signed-off-by: chris hay <chris.hay@uk.ibm.com>
- Seatbelt is described as 'network and write confinement with partial read protection' (not full isolation) in the backend summary table, matching the detailed footnote, and notes sandbox-exec is deprecated by Apple. - Roadmap: add the shipped isolated-execution/security work (v0.23-v0.25), update the 'Shipped' label and last-updated date, drop the stale v0.23 tag from the Next section. Signed-off-by: chris hay <chris.hay@uk.ibm.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.
Adds Windows support to
IsolatedCodeRunner, behind a pluggable broker transport so POSIX is unchanged.Transport groundwork (verified on macOS/Linux)
MessageChannelabstraction: unix domain socket on POSIX (unchanged), Windows named pipe otherwise. Broker, guest bootstrap, backends, and runner now speakendpoint+transportinstead of a hard-coded socket path. Existing backends (Local/Seatbelt/Docker/bubblewrap) are unaffected; Docker keeps its v0.24.0 fixes (pre-pull,--pull never,--user).Windows (experimental — verified via CI, not locally)
_winpipe: host named-pipe server whose security descriptor grantsALL APPLICATION PACKAGES+ a low-integrity label, so a low-IL AppContainer can reach it over local IPC without a network hole (pywin32)._winproc: AppContainer launch viaSTARTUPINFOEXsecurity-capabilities +CreateProcess, inside a Job Object capping memory/processes and killing the tree on close (ctypes).WindowsBackend: ties them together; import-safe on POSIX,is_available()true only on Windows.CI
isolation.ymlnow runs Docker integration on Linux and the experimental Windows pipe/AppContainer path on windows-latest (CTP_TEST_ISOLATION_WINDOWS=1+ pywin32). The mandatory matrix skips the Windows broker tests (opt-in only), so this PR'stest.ymlstays green while the Windows internals are iterated viaisolation.yml.isolation-windowsextra (pywin32); Windows-only mypy overrides.Heads-up: the Windows named-pipe/AppContainer internals have never run on real hardware — like the Docker backend, expect the
isolation.ymlwindows job to need a few iterations before it's green. The mandatory checks (test.yml) should pass immediately.Stacked on v0.24.0. WASM backend follows on a separate branch.
🤖 Generated with Claude Code