docs/native-session-isolation.md
Rama main · réplica de lectura del repositorio original.
# Native sessions with host authentication
**Experimental, on by default for Codex/Claude, macOS and Linux.** Node bridges the installed Codex or
Claude Code client. Authenticate/configure that client once as the OS user that
runs Node. `SESSION_ISOLATION=native` does not require another login or a new API
key. Node does not parse, copy, refresh or proxy subscription tokens.
## Boundary
The official client remains a trusted host process and owns authentication.
Its model-controlled tools are restricted separately:
- Codex uses a named filesystem permission profile, a clean tool environment,
and its native OS sandbox. Legacy thread/start and thread/resume sandbox
overrides are omitted so they cannot replace that profile.
The in-process `view_image` tool and the host-configured `notify` command are
explicitly disabled, including when the user's configuration enables them.
- Claude retains its normal authentication home and uses `--restricted`, a strict
MCP configuration and an explicit tool set. Without selected MCP tools it also
uses `--safe-mode`. With Core-selected tools, it uses explicit customization
controls instead: user/project/local settings are ignored, hooks and skills are
disabled, automatic memory is off and ambient instruction files are excluded.
Only the supplied MCP configuration is loaded. Neither path uses `--bare`, so
the official client can retain its OAuth/Keychain login. Managed policy still
applies. Node never copies login tokens into MCP configuration.
**Bash is the only default local tool.** Read/Edit/Write/Glob/Grep require an
explicit operator opt-in; they use client permission checks, not an OS sandbox.
Bash uses the built-in
OS sandbox with `failIfUnavailable=true`, no excluded commands and no
unsandboxed retry. Credentials are denied to command environments. macOS also
enables global subprocess scrubbing; Linux uses the normal Bash sandbox's
credential deny rules and PID/proc namespaces. The global scrub switch in
Claude 2.1.259 adds broad Linux writable roots and must not be enabled here.
`CLAUDE_CODE_TMPDIR` also selects the Run's private temporary directory for
Claude's command bookkeeping, rather than the shared `/tmp/claude-<uid>` path.
- Authentication directories, other sessions and Node control files are outside
tool grants. Shells receive no platform token or ambient loader configuration.
Codex tool HOME is private; Claude keeps its auth home in the trusted client
and denies it to tools. A network filter is not used as a substitute for
preventing readable credentials from returning in Run output.
- Host plugins, ambient MCP servers, hooks and cross-session/desktop/browser
tools are not part of this mode. Owner-selected MCP tools arrive through Core's
trusted snapshot and per-call authorization. Node does not implement another model loop.
The official binary, OS user/admin, client settings delivered by an administrator
and allowed code/library paths are trusted. This is not isolation from a hostile
host process, compromised official client or administrator policy that weakens
its sandbox. It is not a blanket claim that arbitrary client versions are safe.
Do not expose an operator's personal subscription as a shared public service.
Account authorization, provider terms, spending limits and access control remain
separate requirements. Use with trusted callers while this feature is experimental.
## Session lifetime
One Node serves multiple conversations. Each active Run starts its client process;
its tools execute in that session's sandbox. Idle sessions retain data without
keeping client processes alive. Core's trusted principal, Agent, conversation
key and Node's Core namespace select the storage scope. Payload fields cannot
select a native session. A cross-process lock prevents overlapping writes to the
same conversation; cancellation of A does not stop B.
A → B → A resumes A's native session. Host personal transcripts are not imported
as conversation history. Codex controls the resumed thread ID; Claude additionally
partitions its project transcripts using `CLAUDE_CODE_PROJECT_DIR_NAME`, while
keeping `CLAUDE_CONFIG_DIR` unchanged for authentication.
Native mode defaults to `HOST_AUTH_CONCURRENCY=serial`: one participating Node
client per provider and shared host HOME at a time. The lock lives at
`$HOME/.local/state/openlinker-agent-node/host-auth-<provider>.lock`, using the
trusted client's HOME, not the session tool HOME, TMPDIR or XDG_STATE_HOME.
State is owner-only (0700 directory, 0600 lock); existing directories are never
chmodded. HOME aliases are canonicalized. Unsafe ownership, writable parents or
symlinks beneath HOME fail closed, with no fallback to `/tmp`. Tools cannot read,
write or unlink this reserved state; do not place session storage there.
Multiple Node processes/Agents/session roots coordinate **only when they share
the underlying HOME state directory and working OS file locks**. Private `/tmp`
namespaces do not split this lock. Different container HOME mounts, different
HOME values or isolated state copies still do, even if paths or client credentials
match. Node cannot infer or detect another namespace's mount or shared account.
This is a host-local file lock, not distributed coordination.
The lock covers client startup, exit, retries and session persistence. Waiting is
cancellable, counts toward the Run timeout, uses an assigned capacity slot and
emits one status event. For serial mode, explicitly set
`OPENLINKER_AGENT_NODE_CAPACITY=1` so this Node does not reserve extra Runs that
could be dispatched elsewhere. Node does not silently change configured capacity.
Waiters retry every 50ms; this is not FIFO. Sustained contention may cause a waiter
to reach its timeout without running; there is no fairness guarantee.
`HOST_AUTH_CONCURRENCY=client-managed` explicitly permits concurrent clients.
Use it only for workloads whose client/authentication concurrency is independently
verified; it does not add token-refresh protection. Ordinary mode is unchanged.
This is **cooperative Node admission, not an account-wide refresh lock**. Older
Nodes, opt-out Nodes and separately launched terminal/desktop clients do not
participate. Avoid sharing a login with those clients during account-sensitive
evaluation. Normal cancellation waits for client shutdown before releasing the
lock. A hard-killed Node can leave an orphan client after its lock is released;
check and stop that Node's remaining client processes before restarting it.
Do not delete the HOME lock while any participating Node is running: the file
is retained intentionally, and the OS releases ownership. When upgrading from
the earlier `/tmp` lock candidate, stop all old participating Nodes and settle
or stop their client processes before starting the new version. The two lock
locations do not coordinate; no old lock/session data is deleted automatically.
Real OAuth rotation remains untested; see the
[open refresh-concurrency item](native-session-isolation-follow-ups.md).
## Setup
Install supported official clients (tested baseline: Codex 0.153.0 and Claude Code
2.1.259). Configure/login to the selected client normally **as the Node OS user**.
Linux needs functioning unprivileged user/network/PID namespaces; Claude also
needs `bwrap`, `socat` and `rg`. macOS uses the client's Seatbelt sandbox.
A separate npm installation of SRT is no longer required for Node's native mode.
Authentication/session directories must be outside system/code read roots.
On Linux Codex needs its per-process helper-alias directory and executable;
Node verifies that the directory contains only known aliases and an empty lock
before granting read access. It does not grant the authentication home or tmp tree.
Ubuntu 26.04's `apparmor` package enforces a `bwrap-userns-restrict` profile for
`/usr/bin/bwrap`: bwrap can create namespaces, but every process it starts runs
without capabilities, including in further user namespaces. Under this default,
Codex native isolation works and Node's startup probe passes, but Claude's
sandbox cannot start its seccomp helper, which needs a capability-bearing nested
user namespace. Every Claude Bash call then fails closed with `apply-seccomp:
write /proc/self/setgroups (nested userns is capability-restricted ...)`; file
tools stay in the workspace and nothing falls back to an unsandboxed shell.
Granting bwrap's children capability-bearing user namespaces is exactly what
this distro policy prevents, so it is a host security decision outside Node.
The native-isolation CI job runs on Ubuntu 24.04, not 26.04.
Do not load a second AppArmor profile that attaches `/usr/bin/bwrap` alongside
the distro one: the kernel then applies neither (`conflicting profile
attachments` in the audit log), bwrap fails with `loopback: Failed
RTM_NEWADDR`, and Node's startup probe rejects native isolation for both
clients.
```sh
export OPENLINKER_AGENT_NODE_ADAPTER=codex # or claude; native isolation is the default
export OPENLINKER_AGENT_NODE_CAPACITY=1 # recommended with default serial admission
# Optional: defaults to $HOME/.local/state/openlinker-agent-node-sessions
export OPENLINKER_AGENT_NODE_SESSION_ROOT=/absolute/private/node-sessions
# Keep your existing HOME and, if configured, CODEX_HOME / CLAUDE_CONFIG_DIR.
# No additional CODEX_API_KEY or ANTHROPIC_API_KEY is required for cached login.
```
`SESSION_ROOT` must be owner-only, have no unsafe writable ancestors and lie
outside every git worktree (no `.git` directory or file in it or any ancestor).
The trusted client reads its working directory's git status, recent commits and
project instructions before any tool sandbox applies, so a session under a
repository would expose that repository to the model. Do not run Node as root.
Native mode runs each conversation in a private workspace, so it rejects
`CODEX_WORKSPACE` / `CLAUDE_WORKSPACE`, `*_SESSION_REUSE=false`, `SESSION_STORE`,
delegation and `ENV_ALLOWLIST` instead of ignoring them. `*_SESSION_REUSE`
defaults to `true` in native mode. There is no automatic unsandboxed fallback:
on unsupported platforms, as root, or with those settings, startup fails until
you fix the setting or set `SESSION_ISOLATION=off` explicitly. A Codex mock
response starts no client and keeps the `off` default; HTTP, A2A and command
adapters are never isolated. Options use the `OPENLINKER_AGENT_NODE_` prefix:
| Option | Meaning |
| --- | --- |
| `SESSION_ISOLATION` | `native` (default for Codex/Claude) or `off` |
| `HOST_AUTH_CONCURRENCY` | Native only: unset/`serial` serializes Node clients per shared host HOME/provider; `client-managed` explicitly allows concurrent clients |
| `SESSION_ROOT` | Private persistent storage outside any git worktree; default `$HOME/.local/state/openlinker-agent-node-sessions` |
| `SESSION_TEMP_ROOT` | Optional private, short temporary root (resolved path ≤40 bytes) |
| `SESSION_READ_PATHS` | JSON array of additional read-only code/library paths for shell tools; may not expose auth or session/control directories |
| `SESSION_NETWORK_DOMAINS` | JSON array of exact public HTTPS hostnames for sandboxed command networking; empty means offline commands |
| `CLAUDE_ALLOWED_TOOLS` | Unset/empty defaults to Bash. Explicit JSON list may opt trusted workloads into Read, Edit, Write, Glob, Grep, Bash; see the file-tool boundary below |
| `CODEX_WEB_SEARCH` / `CLAUDE_WEB_SEARCH` | Controls the client's native search tool separately |
### Custom model gateways
Node no longer overwrites the default Codex model endpoint. Codex retains its
existing model-provider/auth configuration. Claude retains the configured
`ANTHROPIC_BASE_URL`/standard credential environment or cached login. Explicit
`CODEX_BASE_URL` and `CLAUDE_BASE_URL` overrides remain supported (HTTPS port 443,
complete API base path). An explicit Codex gateway uses `CODEX_API_KEY`, as before;
that optional path does not make a key mandatory for normal cached login.
`SESSION_NETWORK_DOMAINS` restricts **sandboxed command** networking, not the
trusted client's model API/auth traffic or its server-side WebSearch. Adding a
model gateway does not automatically grant tools access to it. TLS verification
is never disabled. Arbitrary environment forwarding remains rejected. Claude's
restricted mode ignores user/project customization settings; setups depending
on a settings-only helper or custom/cloud authentication need separate validation,
not automatic token extraction or a subscription proxy.
## Migration and verification
### File-tool opt-in boundary
Claude's application-level file tools are not equivalent to its sandboxed Bash
tool. Keep the default for account-sensitive workloads; shell commands can read,
search and edit the session's own files. `CLAUDE_WEB_SEARCH` remains separately
controlled (on by default, explicit `false` turns it off) and does not follow this default.
Do not put hard links to sensitive host files into a session workspace. A hard
link is another name for the same inode, not a path that can be checked with
`realpath`. The real-client suite separately checks links created by sandboxed
Bash and hard links preseeded by the trusted test host. The latter can be read
by explicitly enabled Read/Grep and by sandboxed Bash itself. A Bash-only tool
set does not revoke an inode already granted by the host. A successful symlink test does not prove
protection against arbitrary preexisting inode aliases. Turning tools off does
not erase data already in transcripts or Core history. Do not reuse a conversation
that may already contain credentials after narrowing its tool permissions.
The opt-in matrix covers the five file tools, file/directory symlinks, hard links,
and Linux `/proc/self` aliases. Positive controls verify real file-tool execution;
host-content checks distinguish atomic replacement of an alias from writes to
the original host inode. The default case also submits unadvertised file-tool
calls and requires rejection. Results apply only to the pinned clients and tested
cases; they are not a proof against symlink-swap races or every client file/IPC API.
### Existing SRT sessions
The old whole-client SRT design required dedicated keys and changed authentication
homes. The new host-auth scope is deliberately separate: old workspaces, native
history and potentially exposed credentials are neither copied nor deleted.
Resume starts fresh once when migrating, then persists in the new scope.
This applies to migration from the old whole-client SRT mode to host-auth mode;
the later Bash-default/view-image hardening does not change the scope again.
Retained Core history is separate from the old client's private session data.
Remove `SESSION_SANDBOX_BIN`; Node rejects this obsolete override instead of
silently ignoring it. Legacy `SESSION_STORE`, delegation sockets and arbitrary
`ENV_ALLOWLIST` are not combined with native isolation.
Startup verifies CLI capabilities and OS prerequisites. On macOS Codex also runs
a real allowed-write/denied-read sandbox probe; Linux probes user/network/PID
namespace availability. Claude itself must initialize its
sandbox before a task, with `failIfUnavailable`; a help/version probe alone is
not evidence of file enforcement. Failures never select an unsandboxed fallback.
Run `scripts/test-native-session-isolation.sh` with both official client paths.
The acceptance suite uses private synthetic auth homes and local mock model
responses to exercise actual tools, cached auth, A-B-A recovery, credential-file
reads, the separate file-tool link matrix, cross-session reads, explicitly
opted-in scoped file edits, outside writes,
environment and parent-process probes. macOS additionally checks a synthetic
item in an explicitly named temporary keychain, never the login keychain.
These tests do not spend subscription/API quota and do not establish real model,
WebSearch or account-policy acceptance. The macOS/Linux CI matrix is configured
to run this suite on PRs and version-tag pushes; configuration is not a pass result.
Current source `25c49b2` passed the GitHub macOS and Ubuntu 24.04 amd64 jobs in
[PR #36 CI](https://github.com/OpenLinker-ai/openlinker-agent-node/actions/runs/34810002330),
including actual installed-client host-auth, link and admission tests. Local
macOS arm64 and an isolated Ubuntu 24.04.5 arm64 OrbStack machine also passed.
The latter uses the OrbStack kernel without AppArmor; the hosted Ubuntu result
provides separate evidence with its OS policy and executable-specific userns
prerequisite. No result claims all target Linux hosts or live-account safety.
See the [current acceptance record](host-auth-acceptance.md); older container and
whole-client SRT results are not substituted for this candidate.
Linux can create an identically named file in an empty private tmpfs overlay.
The acceptance check verifies that host credential files/directories remain
unchanged; successful writes to disposable overlays do not imply host access.
There are still **no per-session disk, memory or process-count hard quotas**.
`SESSION_TEMP_ROOT` and timeouts do not provide them. A task can exhaust its
filesystem or account budget without reading credentials. See the
[follow-up record](native-session-isolation-follow-ups.md).
For mTLS deployments, upgrade Core with the certificate-renewal compatibility
fix before switching this Node/Host. Schema 099 alone is insufficient; the
release notes pin the minimum Core commit. Preserve the existing identity,
private key, DataDir and spool. Do not clear local state after a Core database
reset; recover the matching server identity through an administrative plan.
The minimum Core source for this renewal-compatible release is
[`a855ba45b0ec14dc45dc2b0f29899a2f915c8d5a`](https://github.com/OpenLinker-ai/openlinker-core/commit/a855ba45b0ec14dc45dc2b0f29899a2f915c8d5a)
(or a descendant containing that fix). Deploy it before upgrading an mTLS Node/Host.