Skip to content

feat(sandbox): inject an always-on sandbox briefing into the harness#25

Merged
nhuelstng merged 9 commits into
TNG:mainfrom
NoRiceToday:feat/omac-sandbox-briefing
Jun 29, 2026
Merged

feat(sandbox): inject an always-on sandbox briefing into the harness#25
nhuelstng merged 9 commits into
TNG:mainfrom
NoRiceToday:feat/omac-sandbox-briefing

Conversation

@NoRiceToday

Copy link
Copy Markdown
Contributor

Problem

An agent launched inside the omac sandbox has no idea it's sandboxed. When it
hits a denied filesystem path or a blocked network host, it misreads the failure
as a bug in its own code, retries it pointlessly, or tries to "self-authorize"
by flipping its harness's own permission/sandbox settings — which does nothing,
because omac enforces the sandbox at the kernel level (Seatbelt / bubblewrap +
Landlock) before the harness ever starts. Nothing told the agent, up front,
that it runs under omac, that a blocked operation is policy rather than a bug,
and that changing the rules is the user's job, not the agent's.

This PR gives every sandboxed launch an always-on briefing, injected into the
harness's system prompt, so the agent begins already knowing the ground rules.

Context

  • The briefing text lives in exactly one place — internal/sandboxbrief/brief.md
    — and is embedded into the binary, so it ships with omac and is edited from a
    single source.
  • omac's two harnesses expose different system-prompt surfaces: Claude Code
    takes a CLI flag (--append-system-prompt); OpenCode has no such flag and
    instead receives context through its bridge plugin. The injection path forks
    accordingly.
  • Injection is deliberately conservative: it fires only when a real sandbox
    wraps the inner command and that command is the harness's own agent binary,
    so the briefing never lands on an --inner-overridden or no-sandbox-debug
    bash process.

Changes Overview

  • internal/sandboxbrief (new) — embedded brief.md plus Default() and
    Resolve(override) (any override text wins; empty/whitespace falls back to the
    embedded default).
  • internal/config — optional sandbox.briefing override field;
    Harness.SystemContextArgs adapter (Claude → --append-system-prompt,
    OpenCode → nil), mirroring the existing ResumeByIDArgs func-field pattern.
  • internal/cli/briefing.gobriefingInjection(...) gate that decides
    whether (and with what text) to inject for a given launch.
  • internal/cli/start.go + serve.go — wire the gate into both launch
    paths: Claude via the flag, OpenCode via the OMAC_SANDBOX_BRIEFING env var.
  • OpenCode plugin (.opencode/plugins/omac-multidir.ts and the embedded
    internal/plugin/assets copy) — push OMAC_SANDBOX_BRIEFING into the system
    prompt; inert outside omac, purely additive, never touches user config.
  • internal/cli/setup.goensureOpenCodePlugin auto-provisions the
    OpenCode bridge plugin globally on launch, so the relay works even if the user
    never ran omac plugin install.

Diagram

flowchart TD
  A["omac start / omac serve"] --> B["briefingInjection(noSandbox, inner, harness, override)"]
  B -->|"no sandbox, empty inner,<br/>or inner ≠ harness agent binary"| X["skip — no briefing"]
  B -->|"sandboxed launch of<br/>the harness's own binary"| C["sandboxbrief.Resolve(override)"]
  C --> D{harness}
  D -->|Claude| E["SystemContextArgs →<br/>--append-system-prompt &lt;brief&gt;"]
  D -->|OpenCode| F["env OMAC_SANDBOX_BRIEFING=&lt;brief&gt;"]
  F --> G["bridge plugin pushes it into<br/>output.system (system prompt)"]
  E --> H["agent starts already knowing<br/>it's in the omac sandbox"]
  G --> H
Loading

Deployment / Impact

  • Purely additive and environment-gated. Outside omac nothing sets
    OMAC_SANDBOX_BRIEFING, so the OpenCode plugin branch is inert; the Claude
    flag is appended only on a sandboxed agent launch. No breaking changes to the
    config schema or CLI.
  • New optional sandbox.briefing config key; absent/empty uses the embedded
    default.
  • omac start / serve now write the OpenCode bridge plugin into
    ~/.config/opencode/plugins on launch — idempotent, refuses to clobber a
    foreign same-named file, and any failure is a warning, never a launch blocker.

Testing

  • New unit tests: sandboxbrief (default non-empty, override vs. fallback),
    the briefingInjection gate (active for the agent binary; skipped for
    no-sandbox / non-harness-binary / empty-inner), SystemContextArgs (Claude
    flag builder, OpenCode nil), sandbox.briefing YAML parsing, and the
    non-OpenCode ensureOpenCodePlugin no-op.
  • go build ./..., go vet, and go test on the changed packages
    (internal/cli, internal/config, internal/sandboxbrief) all pass locally
    (Go 1.25).
  • Verified end-to-end on macOS for both harnesses: the briefing reaches the
    system prompt for Claude (via --append-system-prompt) and OpenCode (via the
    bridge plugin). Linux verification still needed — someone on Linux
    (bubblewrap + Landlock) should confirm the same end-to-end behavior.

Sajjad Ahmad added 9 commits June 29, 2026 15:53
Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
…system-prompt)

Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
…unch

Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
Address review: dedupe the briefing rationale that was repeated across
start.go/serve.go/setup.go/plugin (each site now carries only its local,
non-obvious detail and points at the canonical spot), and trim the
verbose doc comments to what the code doesn't already say.

Remove the dead `case res.Overwrote` branch in ensureOpenCodePlugin:
InstallMultiDirIn is called with force=false, which returns an error
rather than overwriting a differing file, so Overwrote is never set.

Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
Replace the misleading 'filtered to an allowlist' framing: the default
network model is interactive prompt-and-learn (a new external host pauses
and prompts the user, whose choice is remembered), not a static allowlist.

Make the enforcement concrete (kernel-level, applied before the harness
starts) so it's clear why disabling the harness's own permissions can't
lift a restriction. Add two facts that prevent lack-of-knowledge errors:
credentials stay denied even inside granted dirs, and capabilities are
discoverable via OMAC_SKILLS / OMAC_<NAME>_BASE. Drop the behavioral
'report it / suggest a profile change' line in favor of the plain fact
that changing the rules is the user's action — stating what the
environment is, not how the agent should work.

Signed-off-by: Sajjad Ahmad <sajjad.ahmad@tngtech.com>
@NoRiceToday NoRiceToday force-pushed the feat/omac-sandbox-briefing branch from 0d12f18 to 4af4a60 Compare June 29, 2026 13:54
@nhuelstng nhuelstng merged commit 05a4219 into TNG:main Jun 29, 2026
4 checks passed
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