Skip to content

Harness ring — agent runtime config

What it does: Writes the settings-file-style config that the agent's runtime obeys (PreToolUse hooks, allow/deny lists).

Some agent runtimes natively support a settings file that constrains what the agent can do — Claude Code's settings.json is the canonical example. Operators editing that file at deploy time is brittle; under TapPass's "live policy" model, the file must be derived from policy and updated on sync.

This layer is the bridge: it knows how to write a runtime-appropriate config blob into the agent's filesystem from the keyring's layer-2 slice. For runtimes that don't have a native settings file (a custom LangChain agent, say), this layer writes a tappass-agent-recognized config that the SDK reads.

The harness layer is the highest leverage layer when it works (the runtime self-enforces) and cheap to bypass if you can edit it (the kernel layer is the backstop). See concepts/governed-agents-architecture.md §9.2 for the bypass matrix.

Inputs: the layer-2 slice of a SandboxConfig, plus the agent runtime identifier (provided by tappass-host start --agent <package>):

layer_2_harness:
runtime: claude-code # or "langchain-react", "tappass-agent-default", …
permissions:
allow:
- "Read(/var/run/tappass/sandboxes/*/agent-data/**)"
- "Bash(git *)"
deny:
- "Bash(rm -rf *)"
hooks:
PreToolUse:
- command: "tappass-agent verify-toolcall"
blocking: true

Outputs: a written file in the agent's sandbox FS (e.g. /var/run/tappass/<sandbox_id>/agent-config/settings.json) and an inotify-equivalent signal so the running agent can reload.

Error cases:

  • Unknown runtime identifier → emit harness_layer_unsupported_runtime: <id>, write a generic tappass-agent config as a fallback, continue.
  • Atomic rename fails → retry once, then fail sandbox apply.

Non-goals:

Where it lives: tappass-host/src/layers/harness.py with a per-runtime adapter pattern. Adapters: claude_code.py, langchain_react.py, tappass_agent_default.py, …

Adapter contract:

class HarnessAdapter:
runtime_id: str
def materialize(self, layer_2_config: dict) -> bytes:
"""Render the runtime-specific config bytes."""
def target_path(self, sandbox_id: str) -> Path:
"""Where in the sandbox FS this config lives."""
def reload_signal(self, sandbox_id: str) -> None:
"""How to tell the running agent to reload (inotify, signal, restart)."""

At sandbox start: materialize the config; atomic-write to the target path; (no signal — the agent reads on first start).

On sync push: materialize new config; atomic-rename; signal the agent.

  • All acceptance_criteria pass.
  • Adapter for at least one canonical runtime (Claude Code) implemented and tested.
  • Generic fallback adapter for unrecognized runtimes implemented.
  • Integration test: settings file written; agent runtime observes the file and respects the rules.
  • Sync push test: settings file updated atomically; running agent reloads without restart.
  • Documentation in spec.

With host-runtime-cli: invoked second in the layer apply sequence (after kernel, before codemode/MCP/gateway).

With agent-client-sdk: the SDK reads the settings file and exposes the rules to agent code (so a LangChain ReAct agent that wraps tappass-agent automatically respects them).

Open questions:

  • (Q) Which agent runtimes should we ship adapters for in v1? Lean: Claude Code, LangChain ReAct (via tappass-agent), and one OSS agent framework (CrewAI or LlamaIndex). Owner: this component.
  • (Q) What if the agent runtime doesn't support live reload? Lean: the host runtime CLI restarts the sandbox on layer-2 changes if reload_signal isn't supported by the adapter. Owner: host-runtime-cli.
  • Adapters for every conceivable agent runtime — ship adapters as customer demand surfaces.
  • Enforcing rules at runtime if the runtime ignores its own settings file (the kernel layer is the backstop).