f/hold docs

Guardian MCP API

Guardian is a curated MCP agent gateway. It turns the native OpenCode API into a small, policy-filtered set of durable agent operations; it is not a generic OpenCode proxy.

HTTP boundary

Method Path Authentication Result
GET /health none {"ok":true} when Guardian is available
GET /.well-known/oauth-protected-resource none RFC 9728 metadata when OAuth is enabled
GET /.well-known/oauth-protected-resource/mcp none Path-aware alias of the same metadata
MCP transport methods /mcp Bearer MCP Streamable HTTP
any any other path n/a 404

The same /mcp endpoint supports modern MCP negotiation and stateless 2025-era clients. There is no admin HTTP API, OpenAI/Anthropic compatibility endpoint, A2A endpoint, raw OpenCode proxy, UI pass-through, or voice API.

Authentication and policy

Authorization: Bearer <credential>
Initial username Key file Default policy
owner state/credentials/owner/key full
discord state/credentials/discord/key chat
slack state/credentials/slack/key chat

Operators may add up to 128 lowercase named credentials. Each record has a username, stable internal identity, key, and policy. Keys contain 32–512 printable non-whitespace ASCII characters. Guardian compares every configured key with fixed-length digest comparison and rejects duplicated credentials. Missing, weak, malformed, duplicated, or unknown credentials return 401.

fhold credential add automation read
fhold credential set-policy automation full
fhold credential rotate automation
fhold credential remove automation

The bearer header carries only the key; the username is the operator-facing identity resolved by Guardian. A credential can be used by any MCP client and assigned to either portal. Rotating its key preserves session ownership. Removing and recreating the same username creates a different internal identity and cannot recover the removed credential's sessions.

The configured chat, read, or full policy selects both the MCP catalog and the managed Assistant profile. A prompt or handle can never select a more privileged policy.

Capability chat read full
Guarded agent run and job polling/cancel yes yes yes
Owned session list/get yes yes yes
Session messages/diff/todos resources yes yes yes
Question response and permission rejection yes yes yes
Bounded workspace search/read no yes yes
Session fork/delete no no yes
Permission approval (once/always) no no yes
Managed Assistant profile remote remote-read remote-full

full does not bypass OpenCode permissions. It permits Guardian to relay an explicit client's decision when OpenCode returns an ask interaction.

OAuth bearer tokens

OAuth is optional and additive. Guardian remains a resource server, not an authorization server. With OAuth enabled it:

  1. advertises the configured authorization-server issuer through protected resource metadata;
  2. returns 401 with a WWW-Authenticate resource_metadata challenge;
  3. verifies JWT signature, issuer, audience, expiry, allowed algorithm, and every configured required scope against the configured HTTPS JWKS; and
  4. maps the exact (issuer, sub) pair to a named fhold credential.

An unmapped subject has no access. OAuth scopes do not select chat, read, or full; the mapped named credential does. Guardian stores no access token, refresh token, OAuth client ID, or client secret.

fhold config oauth \
  --resource https://agent.example.com/mcp \
  --issuer https://identity.example.com/ \
  --jwks-url https://identity.example.com/.well-known/jwks.json \
  --audience https://agent.example.com/mcp \
  --scopes fhold

fhold credential map oauth \
  https://identity.example.com/ user-subject owner

The resource, issuer, and JWKS URLs must be HTTPS. Supported JWT algorithms are RS256, PS256, ES256, and EdDSA; RS256 is the CLI default. See remote MCP deployment for the reverse-proxy and identity provider requirements.

Tools

All essential operations are tools so tools-only clients, including OpenCode, can complete the full workflow.

Agent and jobs

fhold.agent.run

{
  "message": "required; 1..32000 characters",
  "session": "optional opaque session handle",
  "title": "optional title; 1..160 characters",
  "waitMs": "optional; 0..30000"
}

The tool creates or resumes an owned session, screens the message, starts the policy-selected agent asynchronously, and waits for at most waitMs. It returns opaque session and job handles plus one of:

fhold.job.get accepts { "job": handle, "waitMs"?: 0..30000 } and returns the same status shape. fhold.job.cancel accepts { "job": handle } and aborts the associated active OpenCode run.

Sessions

OpenCode session IDs and Guardian ownership proofs are never returned.

Workspace

fhold.workspace.search and fhold.workspace.read are advertised only for read and full credentials.

{ "mode": "files | text | symbols", "query": "required", "limit": 50 }
{ "path": "relative/path.txt" }

Reads are text-only and capped at 256 KiB. Paths must be relative to /work. Traversal, absolute paths, VCS/credential directories, .env files (except .env.example), private keys, auth files, and secret-like path components are denied. Guardian reads its own read-only workspace mount and verifies the canonical path and opened file descriptor remain inside /work, so symlinks cannot escape the boundary. Search results pass through the same filesystem check before their content or metadata is returned.

This is a confidentiality grant as well as a no-write policy: every ordinary workspace file is visible to a read or full credential. Keep credentials outside /work.

Interactions

fhold.interaction.respond accepts an opaque interaction handle plus:

Question answers and optional permission messages are screened before they reach Assistant. Guardian verifies that the interaction is still pending and belongs to the same owned session. Non-full credentials can never approve a permission.

Catalog

fhold.catalog.get({}) returns the policy-filtered tool, resource, and prompt names. It does not return the Assistant's internal tools, configuration, providers, or credentials.

Resources

Resources are additive conveniences. Tools-only clients use fhold.session.get with include for the same session views and fhold.job.get for job state.

The URI variables are opaque Guardian handles where applicable. Reads repeat policy, ownership, expiry, and path validation; possession of an upstream ID is not authorization.

Prompts

Guardian publishes four static workflow prompts:

They produce client-visible user messages and never bypass agent.run, policy, moderation, or permission handling.

Handle and ownership model

New session, message, job, and interaction handles use AES-256-GCM with a key derived from the file-backed Guardian handle secret. They are expiring, credential-identity scoped, and conceal every upstream identifier. Session handles default to 30 days, jobs to 24 hours, and interactions to one hour.

Guardian also writes an HMAC-bound ownership record into each created OpenCode session. Listing, polling, resource reads, and mutations require that proof. This keeps Guardian stateless without trusting user-supplied session IDs or adding a database. Only current encrypted handles are accepted.

Each new job handle is also bound to the exact OpenCode user-message ID created for that run. Polling an older completed job remains deterministic, while an older handle cannot cancel a later run in the same session.

Security behavior

CORS

Set GUARDIAN_ALLOWED_ORIGINS to a comma-separated list of exact origins only when a browser MCP client is required:

https://agent.example.com,http://127.0.0.1:3000

A request without an Origin header is a non-browser MCP client. An Origin that is absent from the allowlist returns 403. Allowed browser responses expose the MCP session/protocol headers and request ID required by a Streamable HTTP client; credentials remain bearer headers rather than cookies. The public OAuth metadata routes allow cross-origin reads independently of this /mcp allowlist.