Skip to content

server: OMB_HEADLESS_FULL_ACCESS, Full access over the API as an operator opt-in (#3) - #10

Merged
immakermatty merged 3 commits into
mainfrom
lazurio/headless-full-access
Oct 1, 2026
Merged

immakermatty merged 3 commits into
mainfrom
lazurio/headless-full-access

Conversation

@immakermatty

@immakermatty immakermatty commented Oct 1, 2026 •

Copy link
Copy Markdown

Status: ready for review

Why

The Organization Admin decided on 2026-10-01 (plan DEV-6632, issue #3) that Steward bot teams on a Lazurio Environment run without a sandbox: the Environment, one Machine with one operator, is the boundary. This supersedes the earlier decision that they run on Auto. Both engines must work: Codex CLI and Claude CLI, as the Environment's own signed-in CLIs.

Upstream lets Full (and Custom) be chosen only in the packaged desktop app, over a private process channel. Over HTTP the server answers 403 This approval-level change can only be made from the packaged desktop app, because a headless server trusts every loopback request as its owner and any local process could otherwise raise a bot to Full. On a headless Environment there is no desktop app, so a Steward team could never reach Full.

What changes

  • OMB_HEADLESS_FULL_ACCESS, an operator-only opt-in read once at server start (new server/headless-full-access.ts, styled like OMB_DEFAULT_BOT_CWD and OMB_GITHUB_INTAKE):
    • unset, empty, 0, false: off, byte-identical upstream behaviour (403);
    • 1 or true: on, and the server logs one line at start;
    • any other value stops the server at start with the reason;
    • ignored in the desktop app (OMB_DESKTOP_PARENT=1), which keeps its own confirmation, like upstream's OMB_SHARED_WORKSPACE_FULL_ACCESS.
  • With it on, the bot PATCH /api/bots/:id lets the owner on loopback (the same trust the API already requires; not a paired session, not OMB_LOOPBACK_TRUST=service) enter Full from Ask, Edits or Auto. That is the only transition unlocked. The server logs one line per bot raised to Full.
  • server/index.ts changes in three places only: the import and start log line, one extra condition on the existing 403, one log line after a successful raise. scripts/control-omb.ts passes the variable to the verification fixture.
  • Docs: LAZURIO.md (overlay list; the Steward team paragraph now says Full without a sandbox, set engine first, then Full, the operator starts the service with the opt-in, Machines never sets it), docs/self-hosting.md (new "Full access without the desktop app" section with the variable, a curl example and exactly what is unlocked), docs/lazurio-github-intake.md (Approval levels bullet), docs/operations/lazurio-fork-release.md (Machines unit contract, overlay table row, "Deliberately not in the overlay" now records the Auto to Full decision).

What Full maps to (reused, not a parallel path)

The PATCH writes the same durable approvalMode: "full" the desktop commit leaves behind; every turn then goes through upstream's approvalModeForTurn, so nothing new runs at turn time.

  • Codex: server/drivers/codex.ts namedApprovalParams("full") (lines 287-300): thread approvalPolicy: "never", sandbox: "danger-full-access"; turn sandboxPolicy: { type: "dangerFullAccess" }. autoAcceptPermissions = approvalMode === "full" (line 1437) answers residual permission requests.
  • Claude: server/drivers/claude.ts lines 1224-1228 map Full to --permission-mode bypassPermissions, and lines 1413-1415 leave out --permission-prompt-tool for it, so there is no approval broker. ask_user questions still reach a person.
  • Delegation: upstream semantics kept. delegationInheritsFullAccess (server/auto-approve.ts 52-60, used in server/index.ts delegatedFullAccess): a Chief of Staff with Full makes the threads it delegates Full, if the recipient's engine implements Full. The Steward leader is a Chief, so its delegated work runs Full even if a worker were still on Ask.
  • Which conversations: the bot default is what changes. A bot's first conversation inherits it; every GitHub intake delivery opens a new conversation (createTask, which copies the bot default), so it starts Full. A conversation that already has its own level keeps it, as upstream.

What does not change

  • Without the variable: nothing (the e2e test asserts the exact upstream 403).
  • Custom: entering it, leaving it, still desktop-only, with or without the opt-in.
  • A single thread's level (PATCH /api/bots/:id/tasks/:threadId), POST /api/bots/:id/tasks with full, paired sessions, service-trust loopback: unchanged.
  • The tool catalog sent to the model: no tool description or MCP tool changed; server/drivers/agents-catalog-wire.test.ts passes unchanged.
  • The app UI: its level selector still offers Full only in the desktop app, so on an Environment the operator sets Full through the local API. A UI for it is left to the UI milestone.
  • Removing the opt-in later does not lower bots already on Full (same as upstream's shared-workspace policy); the operator sets them back to Ask, which never needs the opt-in.
  • Upstream docs/approval-levels.md is left unchanged to keep the upstream diff minimal; the opt-in is documented in docs/self-hosting.md, already on the allowlist.

Security note

  • The trust boundary is upstream's own: loopback is the owner. With the opt-in on, any process of the Environment's user, including a bot's own shell, can raise any bot to Full. That is accepted by the decision: the Environment (one Machine, one operator) is the boundary, and Full bots already have an unsandboxed shell as that user.
  • The opt-in is operator-only: an environment variable read once at start, not an HTTP setting, not in config.json, not settable by a bot through the API. Machines never sets it; the operator adds it to the unit of their own Environment.
  • The gateway in front of the app must keep requiring sign-in, and the port must never be exposed (already required by the release runbook).
  • GitHub rights and branch rules stay the limit for everything the bots do on GitHub.

Verification

All run locally on this head:

  • Overlay guard (exact upstream base v0.1.91, no merge commits, only allowlisted upstream files changed): passes. Touched upstream files: server/index.ts, scripts/control-omb.ts, docs/self-hosting.md, all already in allowed_upstream_changes (a comment now records this overlay against them).
  • node --test scripts/lazurio-release-contract.node-test.mjs: 6/6 pass.
  • pnpm typecheck: pass. pnpm lint: pass. pnpm i18n:check: pass.
  • New tests: server/headless-full-access.test.ts (parser and permit matrix) and server/headless-full-access.e2e.test.ts (real isolated server with fake Codex and Claude engines): off gives the exact upstream 403; on, a Codex bot and a Claude bot PATCH to Full with 200, Custom still 403 on Codex; the next Codex turn's thread/start/turn/start carry never + danger-full-access/dangerFullAccess, the next Claude turn runs --permission-mode bypassPermissions without --permission-prompt-tool; back to Ask works; an unknown value stops the server at start with the reason; start and raise log lines present.
  • Related suites pass: auto-approve, full-access-workflows.e2e, default-bot-cwd(.e2e), github-intake(.e2e), lazurio-steward-team, hosted-access, enterprise, drivers/agents-catalog-wire, drivers/codex, drivers/claude (14 files, 446 passed, 1 skipped), plus server/index.test.ts (260 passed). scripts/control-omb.ts has no unit test of its own; the e2e test exercises its fixture pass-through.
  • Run before the implementation existed, the opt-in e2e tests failed (403 instead of 200, no start line, the server started on an unknown value); the off test asserts upstream's behaviour and passes either way.

Fork CI on this head: Archive linux-x64 passes. "Overlay, typecheck and lint" fails at its first step, "Upstream workflows stay disabled", because GitHub registered the upstream ios-thread-ui.yml as an active workflow on 2026-10-01; this PR does not cause it, and the vitest shards are skipped because they depend on that job. Fix it in repository settings, then re-run: #11.

Follow-ups

  • Machines: nothing to change; the unit must keep not setting the variable. The operator adds it on their Environment.
  • UI milestone: let the app's level selector offer Full when the server runs with the opt-in (for example through a health capability), instead of curl.
  • Live canary on a real Environment with the signed-in codex and claude CLIs; the tests use repository fakes. Claude Code is known to refuse skipping permissions when it runs as root, so the service should keep running as the operator's user, as the runbook's user unit does; not verified live here.
  • Possibly propose the opt-in upstream; it has no Lazurio-specific part.

🤖 Generated with Claude Code

immakermatty and others added 3 commits October 1, 2026 23:16
The Organization Admin decided that Steward bot teams on a Lazurio
Environment run without a sandbox: the Environment, one Machine with one
operator, is the boundary. These tests pin the contract of
OMB_HEADLESS_FULL_ACCESS before it exists. Without it, a bot PATCH to Full
is refused with upstream's exact 403. With it, the loopback owner sets a
Codex bot and a Claude bot to Full through that PATCH, the next turn runs
Codex with never approvals in the danger-full-access sandbox and Claude
with bypassPermissions and no approval broker, Custom stays refused, and an
unknown value stops the server at start. The verification fixture passes
the variable to its server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ator opt-in (#3)

Upstream reserves Full and Custom for the packaged desktop app, because a
headless server trusts every loopback request as its owner and any local
process could otherwise raise a bot to Full. On a Lazurio Environment the
Machine and its one operator are the boundary, and Steward bot teams run
without a sandbox there (Organization Admin decision, plan DEV-6632).

With OMB_HEADLESS_FULL_ACCESS=1 at server start, the owner on loopback may
enter Full from Ask, Edits or Auto through the ordinary bot PATCH. Custom,
leaving Custom, thread-level changes, paired sessions and service-trust
loopback stay refused as upstream; so does everything in the desktop app,
which keeps its own confirmation. Full then is upstream's Full: the drivers
map it to Codex never approvals in the danger-full-access sandbox and to
Claude bypassPermissions, and a Chief of Staff with Full passes it on to
the work it delegates. The server logs one line at start when the opt-in
is on and one line per bot raised to Full; an unknown value stops it at
start. Unset, nothing changes. The logic lives in the new
server/headless-full-access.ts; server/index.ts only calls it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Organization Admin decided on 2026-10-01 that Steward bot teams on a
Lazurio Environment run without a sandbox, superseding the earlier decision
for Auto: the Environment, one Machine with one operator, is the boundary.
LAZURIO.md lists OMB_HEADLESS_FULL_ACCESS among the overlay and says what
the operator does after the import: set each bot's engine, Codex or Claude,
then Full, with the service started with the opt-in, which Machines never
sets. docs/self-hosting.md documents the variable, how to set a bot to Full
through the local API and exactly what is and is not unlocked. The intake's
security notes and the release runbook (Machines contract, overlay table,
what is deliberately not in the overlay) say the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head Steward QA: APPROVED at ed2c92e (base ca0e54f). I traced the sole server guard change: with the flag off the upstream 403 remains; with it on, only an owner-trust loopback bot PATCH entering Full from Ask/Edits/Auto passes. Custom, leaving Custom, paired sessions, service-trust loopback, thread-only routes, and model-facing tool files stay unchanged. Existing provider handling supplies Codex never + danger-full-access and Claude bypassPermissions. The seven modified upstream files are all on allowed_upstream_changes; Lazurio docs supersede the former Auto decision and explain the operator-only opt-in. Local isolated fixture: 53 targeted tests passed (including driver-wire and both engines), 334 related tests passed with one skipped, release contract 6/6, typecheck, lint and i18n passed. Current-head fork CI: overlay, archive and four Vitest shards succeeded; no unresolved threads or other pending reviewer. Non-blocking documentation follow-up: the upstream-generic docs/approval-levels.md still says Full can only be enabled in the packaged desktop app; the linked self-hosting page documents this explicitly scoped headless exception. No merge performed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants