feat(sandbox): inject an always-on sandbox briefing into the harness#25
Merged
Merged
Conversation
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>
0d12f18 to
4af4a60
Compare
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.
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
internal/sandboxbrief/brief.md— and is embedded into the binary, so it ships with omac and is edited from a
single source.
takes a CLI flag (
--append-system-prompt); OpenCode has no such flag andinstead receives context through its bridge plugin. The injection path forks
accordingly.
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-debugbashprocess.Changes Overview
internal/sandboxbrief(new) — embeddedbrief.mdplusDefault()andResolve(override)(any override text wins; empty/whitespace falls back to theembedded default).
internal/config— optionalsandbox.briefingoverride field;Harness.SystemContextArgsadapter (Claude →--append-system-prompt,OpenCode → nil), mirroring the existing
ResumeByIDArgsfunc-field pattern.internal/cli/briefing.go—briefingInjection(...)gate that decideswhether (and with what text) to inject for a given launch.
internal/cli/start.go+serve.go— wire the gate into both launchpaths: Claude via the flag, OpenCode via the
OMAC_SANDBOX_BRIEFINGenv var..opencode/plugins/omac-multidir.tsand the embeddedinternal/plugin/assetscopy) — pushOMAC_SANDBOX_BRIEFINGinto the systemprompt; inert outside omac, purely additive, never touches user config.
internal/cli/setup.go—ensureOpenCodePluginauto-provisions theOpenCode 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 <brief>"] D -->|OpenCode| F["env OMAC_SANDBOX_BRIEFING=<brief>"] 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 --> HDeployment / Impact
OMAC_SANDBOX_BRIEFING, so the OpenCode plugin branch is inert; the Claudeflag is appended only on a sandboxed agent launch. No breaking changes to the
config schema or CLI.
sandbox.briefingconfig key; absent/empty uses the embeddeddefault.
omac start/servenow write the OpenCode bridge plugin into~/.config/opencode/pluginson launch — idempotent, refuses to clobber aforeign same-named file, and any failure is a warning, never a launch blocker.
Testing
sandboxbrief(default non-empty, override vs. fallback),the
briefingInjectiongate (active for the agent binary; skipped forno-sandbox / non-harness-binary / empty-inner),
SystemContextArgs(Claudeflag builder, OpenCode nil),
sandbox.briefingYAML parsing, and thenon-OpenCode
ensureOpenCodePluginno-op.go build ./...,go vet, andgo teston the changed packages(
internal/cli,internal/config,internal/sandboxbrief) all pass locally(Go 1.25).
system prompt for Claude (via
--append-system-prompt) and OpenCode (via thebridge plugin). Linux verification still needed — someone on Linux
(bubblewrap + Landlock) should confirm the same end-to-end behavior.