Host Bridge — Secrets and Approved Host Actions
Status: Open — design proposal
Problem
Section titled “Problem”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:
- 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.
- 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 mergeagainst 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.”
This roadmap item proposes a host bridge — 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
Section titled “Two flows”The bridge serves two distinct flows that share a transport, an approval surface, and an audit log.
Flow 1 — secret-on-demand
Section titled “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
Section titled “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) — everyhost.runcall 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.disabled—host.runis unavailable for this workspace / role. The MCP tool isn’t even registered.
disabled is the policy shipped to roles that have no business asking the host to run anything (read-only review roles, scratch experimentation roles).
Why this is one item, two flows
Section titled “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
disabledpolicy 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
Section titled “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.
Per-platform implementation:
- macOS.
LocalAuthenticationframework — TouchID where available, falls back to login password. Wrapped by a small Swift helper bundled with the daemon. - Linux.
polkitfor 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
Section titled “Threat model”This feature widens the attack surface jackin’ presents to a malicious agent. The threat model has to be explicit before the implementation lands.
What a malicious agent can do today (without this feature)
Section titled “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
Section titled “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
Section titled “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.runcall 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.runcall still goes through the operator.
Defense-in-depth
Section titled “Defense-in-depth”- Per-workspace
disableddefault 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
Section titled “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 docker/runtime/entrypoint.sh uses today for the Claude runtime’s tirith / shellfirm to every supported runtime — and never asks the operator to wire it manually.
Tool surface (initial cut):
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 whethercommand_patternwould 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
Section titled “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
Section titled “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.runinvokes 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-workspacedisabled/allowlist/blocklistpolicies bound what can ever be requested. - Secret reads from operator-chosen sources.
secret.requestresolves 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 singlesecret.use_insubstitution. - 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
disabledunregisters 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.
Phase 1 cut
Section titled “Phase 1 cut”A minimal slice that proves the shape:
- MCP server with just
host.run(the simpler of the two flows from a “no special handle plumbing” perspective).always-promptpolicy only — no allowlist / blocklist yet. - macOS approval surface via
LocalAuthentication(TouchID + password fallback). - Linux approval surface via terminal password prompt (no polkit yet).
- Audit log written to
~/.jackin/log/host-bridge.jsonlwith rotation. Bridgetab injackin consoleworkspace editor (parallel to the existing General / Mounts / Roles / Secrets / Auth tabs): enable / disable per workspace, view recent audit entries.
Not in phase 1: secret.request / secret.use_in (more complex handle lifecycle), allowlist / blocklist policies, polkit integration, host.run_capability, prompt-fatigue debounce, audit export tooling.
Open design questions
Section titled “Open design questions”Secret-handle lifecycle
Section titled “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
Section titled “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 consoleBridge tab, also reachable from a global hotkey) showing pending and recent requests so the operator can review without losing context.
Cross-agent reuse
Section titled “Cross-agent reuse”When two agents in two containers both need the same secret, does the operator have to approve twice? Caching the approved-secret-handle for a configurable window (10 minutes?) would dedupe the prompt, but introduces a state question (which agent gets to “use” the cached handle first?). Phase 1 punts on this — no caching, every request prompts — and revisits when usage data shows the prompt rate is the bottleneck.
Cross-host
Section titled “Cross-host”When jackin’ grows to remote agent-host architectures (Kubernetes phase, see the Why jackin’ page), the bridge needs to reach the operator wherever they are. Out of scope for this item; revisit when remote-agent work lands.
Why this matters
Section titled “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.
Related work
Section titled “Related work”- jackin daemon — host-side process this feature plugs into. Phase 1 daemon must ship before this adapter can.
- Live bidirectional auth sync — sibling feature plugging into the same daemon; sets the precedent for per-axis adapters.
- Agent attention prompts — sibling MCP-server feature using the same auto-registration pattern; both flow through the daemon.
- Jackin Desktop Agent Hub — native macOS surface that should eventually show host-bridge approvals and audit entries while keeping daemon policy/execution as the source of truth.
- Container credential exposure — beyond env injection — captures the broader trade-off between today’s
docker run -eenv injection and the daemon-mediatedsecret.requestflow this item proposes; together they replace env-resident tokens with on-demand handles. - Credential proxy — earlier-roadmap idea about proxy-based credential injection without storing tokens inside containers; this item is the operator-mediated counterpart for secrets the operator wants to vet per use.
docker/runtime/entrypoint.sh— current MCP-registration pattern fortirith/shellfirm. The bridge MCP server registers through the same code path.