PlatformSecurityCredential exposure

Host Bridge Design — Flows, Threat Model, and Open Questions

Defines host-bridge request flows, trust boundaries, authorization controls, and unresolved security questions.

Research state: Incomplete

Summary

The host bridge needs typed requests, explicit authorization, narrow trust boundaries, and deny-by-default handling.

This rationale supports the Host Bridge roadmap item; enforcement details remain unresolved.

Research question

jackin isolates agent containers from the host on purpose — that's the whole security model. But hard isolation creates two operational pain points the operator runs into often:

  1. An agent needs a secret for one command, mid-session. The credential wasn't on the operator's pre-launch env-var allowlist (it's a one-off — a database password the operator has in 1Password, an SSH passphrase for a single push, an API key for a tool the agent is about to invoke). Today the operator's choices are bad: stop the agent, edit the workspace env vars, restart the container, lose the agent's working state — or paste the secret into the agent's prompt and let the agent see the raw value forever.
  2. An agent needs to do something only the host can do. Examples: read a TouchID-protected Keychain entry, sign a release with the host's GPG key (which jackin deliberately doesn't forward), invoke a CLI that's licensed only on the host, run gh pr merge against a repo whose write token lives in the host's Keychain. Today the operator has to leave the agent's flow, run the command themselves, paste the output back. Every interruption costs flow.

Both look like the same shape from the operator's seat: "the agent needs a single piece of host help, and I want to authorize it without tearing down the session."

The proposed host bridge is a small request/approval channel between agents inside containers and the operator on the host, hosted by the jackin daemon. Agents request a secret value or a host command; the operator gets a host-side prompt (touch confirmation, biometric, password); on approval the daemon does the work; the agent receives back exactly what it asked for and nothing more.

The daemon owns the policy, audit log, and execution path. The jackin desktop Agent Hub is the preferred native macOS surface for viewing pending requests and audit state once it exists; agents still talk to the daemon, not directly to the app.

Two flows

The bridge serves two distinct flows that share a transport, an approval surface, and an audit log.

Flow 1 — secret-on-demand

The agent inside the container realizes it needs a secret it doesn't have. It calls a tool on a jackin-provided MCP server:

secret.request(name: "POSTGRES_PROD_PASSWORD", scope: "single-command", reason: "running pg_dump")

The daemon receives the request and dispatches it to the operator: a host-side prompt naming the workspace, role, agent, the secret name, the operator-supplied reason, and the requested scope. The operator confirms with whatever auth the host supports — TouchID on macOS, polkit / pkcheck on Linux, password fallback. On approval, the daemon resolves the secret from the operator's chosen source (1Password, host Keychain, environment) and returns the value into the container.

Crucially, the agent does not see the resolved value as a string it can echo, log, or tool-output. The MCP server hands the secret to the agent's runtime as an opaque handle, and the runtime substitutes the handle into the next single command. The handle is single-use and expires immediately after the command exits — the value never lands in the agent's context window or chat history.

Three scope options:

  • single-command — one substitution, then the handle is invalidated.
  • session — the secret stays usable for the duration of the agent's current session, but never persists past restart.
  • persisted — the operator approves writing the secret to the workspace's env-vars layer so subsequent launches inherit it. Identical effect to manually editing the workspace env config; the bridge just spares the operator the workspace-editor round-trip.

Default is single-command. The other two require explicit operator selection on the prompt.

Flow 2 — operator-approved host command

The agent realizes it needs an action only the host can perform. It calls:

host.run(command: "gh pr merge 123 --squash", reason: "shipping the auth PR after CI")

The daemon dispatches a host-side prompt naming the workspace / role / agent / command / reason, with the same TouchID / polkit / password approval. On approval, the daemon runs the command on the host — same UID as the operator — captures stdout / stderr / exit code, and returns those back to the agent.

The agent does not get shell access to the host. Each host.run call is one-shot, one command, fully recorded.

Approval policy is per-workspace and per-role:

  • always-prompt (default) — every host.run call hits the operator approval flow.
  • allowlist — operator pre-declares a set of command patterns the daemon can run without prompting (e.g. gh pr view *, gh api user). Calls outside the allowlist still prompt.
  • blocklist — symmetric: pre-declared denied patterns are rejected without prompting; everything else prompts.
  • disabledhost.run is unavailable for this workspace / role. The MCP tool isn't even registered.

disabled is the policy for roles that have no business asking the host to run anything, such as read-only review and scratch experimentation roles.

Why this is one item, two flows

The two flows look superficially different — one returns a secret value, the other runs a command — but the daemon-side machinery is identical:

  • Same MCP server inside the container (different tool surfaces).
  • Same control channel between MCP server and daemon.
  • Same operator-approval prompt shape (workspace / role / agent / what-is-being-asked / reason / approve-or-deny).
  • Same biometric / password / touch-confirmation backend.
  • Same audit log.
  • Same disabled policy escape hatch.

Splitting the two flows into two roadmap items would multiply the security model, the operator UX, and the daemon code paths for no benefit. Keep them together.

Operator approval surface

The host-side prompt is the load-bearing part of this design. It must be:

  • Hard to spoof. A real operator-presence proof — TouchID, system polkit, sudo-style password fallback — not just a click-through dialog any agent could simulate by running a fake one inside the container. The container has no path to spawn a host-process; the prompt comes from the daemon, which has process-level integrity.
  • Stateful per request. Each request gets its own prompt with the request ID baked in. Approving prompt A does not approve a different request B that arrives a millisecond later.
  • Auditable. Every prompt + response (approved / denied / timed out) lands in the daemon's audit log with timestamp, workspace, role, agent, request payload (with secrets redacted), and the operator's response. Reviewable via jackin audit (subcommand to be designed).
  • Time-bounded. Prompts expire after a configurable window (default 30s). Expiry counts as deny. The agent receives a structured "operator did not approve in time" error.
  • Visually distinct. The prompt is a system-modal notification or native Desktop approval surface, not a TUI overlay — agent presence in jackin console must not block another agent's approval prompt from reaching the operator.

Platform approval adapters:

  • macOS. LocalAuthentication framework — TouchID where available, falls back to login password. Wrapped by a small Swift helper bundled with the daemon.
  • Linux. polkit for the password / fingerprint flow on systems with PAM-fingerprint configured. Falls back to a sudo-style terminal password prompt for headless setups.
  • Windows. Out of scope until jackin supports Windows hosts.

Threat model

This feature widens the attack surface jackin presents to a malicious agent, so its threat model must remain explicit.

What a malicious agent can do today (without this feature)

  • Run any command inside its own container (full speed by design — that's the trust boundary).
  • Read host filesystem only via mounts the operator chose.
  • Access network only via the container's network policy.
  • Cannot reach the host's process namespace, Keychain, file system outside mounts, or invoke host CLIs.

What this feature changes

  • Agents gain a request channel to the host. Without operator approval, every request fails. With approval, the agent gets exactly what was approved and nothing more.
  • An agent that gets clever about spamming requests cannot bypass approval — every request hits the operator's prompt or is rejected by policy.
  • An agent that gets clever about social engineering the operator (asking for a more dangerous action than necessary, hiding intent in a benign-looking request) is the real risk. Mitigation: the prompt shows exactly what's being requested; the audit log captures everything; allowlist policies let operators pre-decide low-risk patterns instead of approving each one individually under decision fatigue.

What stays out of scope

  • Agents cannot pre-position state on the host (no "create a file at /tmp/X for me" — that's just a host.run call with a write-flavored command, subject to the same policy).
  • Agents cannot escalate privilege (the daemon runs as the operator's UID; commands it runs inherit that UID).
  • Agents cannot interact with another agent's container directly through the bridge — every host.run call still goes through the operator.

Defense-in-depth

  • Per-workspace disabled default for high-risk roles. Roles that don't need the bridge get it disabled at the policy layer; the MCP tool isn't even registered.
  • Allowlist patterns. Operators can pre-declare safe command patterns. The default is no allowlist — every request prompts.
  • Audit log retention. Approval / deny events kept for at least 90 days, exportable via jackin audit export.
  • Prompt-fatigue debounce. When the operator denies the same request shape 3+ times in a row, the daemon offers to add an explicit deny rule rather than continuing to prompt.

How the agent expresses requests

Same shape as agent attention prompts: a jackin-bundled MCP server, auto-registered at container launch, exposes the secret.* and host.* tool families. The agent runtime's tool-use loop calls these tools when the agent decides it needs a secret or a host command; jackin bundles the MCP server, registers it in each agent's MCP config — extending the registration pattern crates/jackin-capsule/src/runtime_setup.rs uses today for the Claude runtime's tirith / shellfirm to every supported runtime — and never asks the operator to wire it manually.

Tool surface:

  • secret.request(name, scope, reason) → opaque handle.
  • secret.use_in(template_string) → runs the next command with the handle's value substituted; the value never appears in tool output the agent can read.
  • host.run(command, reason) → returns {stdout, stderr, exit_code} after operator approval.
  • host.run_capability(command_pattern) → returns whether command_pattern would be allowed by the current workspace policy; lets the agent ask "before I prompt the operator, can I even do this?" without burning a prompt cycle.

Why the daemon

Same shape as the other reactive features that depend on it. The bridge needs:

  • A long-running host process that the container's MCP server can call (per-container Unix domain socket through the bind-mount channel — see jackin daemon for the channel design).
  • A host-side process that has integrity to display approval prompts the operator trusts.
  • A central audit log that survives across CLI invocations and individual container lifetimes.
  • A policy engine (allowlist / blocklist / disabled) configured per workspace and queryable from the MCP server.

None of these can run from a CLI that exits between commands. The daemon's lifecycle, install, security posture, and control-socket transport are decided in the jackin daemon item; this feature is one adapter against that base.

Host-side effects

Per AGENTS.md "Never mutate the host machine silently": the bridge runs operator-authorized commands as the operator's UID and writes to host-side audit state. Every effect is gated on a TouchID / polkit / password approval surface that names the request before it runs.

  • Arbitrary host commands. host.run invokes a command on the host with the operator's UID, only after operator approval through the daemon's prompt surface (LocalAuthentication / polkit / password). The command, the agent's stated reason, and the approval/deny outcome land in the audit log. Per-workspace disabled / allowlist / blocklist policies bound what can ever be requested.
  • Secret reads from operator-chosen sources. secret.request resolves through whatever store the operator configured (Keychain, 1Password, env, etc.). Reads happen only on operator approval; the daemon never enumerates secrets and never caches values past the lifetime of the single secret.use_in substitution.
  • Audit log writes. The daemon appends approval / deny events to ~/.jackin/log/host-bridge.jsonl (path subject to daemon design). No other host file is mutated by this feature.
  • Opt-out path. Setting the workspace policy to disabled unregisters the MCP server inside the container; no further requests are even surfaced to the operator.
  • Out of scope: silent host writes. This feature deliberately ships no shape that can mutate the host without per-request approval. Pre-approved allowlist patterns still surface a one-line audit-log entry per invocation; no command runs invisibly.

Core capability boundary

The host-command path requires typed host.run requests, an always-prompt policy, macOS and Linux approval adapters, a rotating audit log, and a workspace surface for enabling the bridge and reviewing recent events. Secret handles, allowlist and blocklist policy, polkit, capability queries, prompt-fatigue controls, and audit export introduce separate security questions captured below and in the owning roadmap item.

Limitations and open questions

Secret-handle lifecycle

How does the MCP server hand the agent a value that the agent can use in a command but cannot read as a string? Possible mechanisms:

  • Wrapper command. The MCP server returns an opaque handle; the agent calls secret.use_in("DATABASE_URL=$HANDLE pg_dump …") and the MCP server sets up the env var on the next process the runtime spawns. Requires runtime cooperation.
  • Pipe injection. The MCP server attaches the value to a fd handed to the next command via the agent runtime's process spawn API. Strictest separation but most fragile across runtimes.
  • Heredoc redaction. Agent runtime captures stdout / stderr; the MCP server marks the handle's value as "redact in capture" so it appears in the running process but not in the conversation log. Easiest to implement but leaks if the value reaches stdout via an unrelated subprocess.

The first option is probably the right fit — explicit, runtime-mediated, hard to misuse. Pinning down the protocol is the open question.

Approval-prompt UX

A system modal the operator sees on every request will annoy them very fast. Strategies for sustainable UX:

  • Bundling. When two requests arrive within a few seconds, fold them into one prompt that lists both.
  • Allowlist learning. After the operator has approved the same exact command pattern 5 times, offer to add it to the workspace's allowlist with a "stop asking me about this" check.
  • Recent-history view. A small TUI overlay (jackin console Bridge tab, also reachable from a global hotkey) showing pending and recent requests so the operator can review without losing context.

Cross-agent reuse

When two agents in two containers both need the same secret, does the operator have to approve twice? The current contract caches no handles and prompts for every request. A shared approval window would reduce prompts but introduces ownership and single-use questions across agents.

Cross-host

Remote agent-host architectures require the bridge to reach the operator across hosts. That transport and trust boundary are outside this design; see Why jackin.

Why this matters

The pitch of jackin is "running many agents safely is a productivity win." The single biggest threat to that pitch is friction: every time the operator has to leave the agent's flow to do a host-side action, the agent stalls and the operator's flow breaks. Moving from "stop the agent, do the host thing yourself, paste back" to "agent asks, you press TouchID, agent continues" is the difference between agents being a sometime-help and agents being a workflow.

The mechanism is also the right escape valve for the "help, I forgot to set up X before launching" class of problems. Without it, every gap in pre-launch config is a session-restart penalty. With it, the operator unblocks the agent in three seconds and the work continues.

On this page