f/hold docs

Environment, mounts, and networks

This document describes the active runtime. The executable source is packages/skeleton/system/stack/stack.compose.yml.

Host layout

New named homes live under ~/fhold/instances/<name>; Admin's unnamed default is ~/fhold/instances/default. CLI --name/-n selects a directory name under that root or an absolute path. If absent, explicit FH_HOME takes precedence over cwd. The CLI resolves that choice once for every command and its child processes; users do not need to export a variable. Admin uses its default/recent home or a selected custom folder. Default changes never move an existing home; select its original path explicitly to continue managing it. ~/fhold can contain sibling backups/, docs/ or other local directories; only the selected instance directory is managed or backed up.

Manual full-instance export/import includes all six trees below (and extra home-owned files), while containers are stopped. It preserves native sessions, credentials and saved identity. Portable content deliberately omits runtime authority. Neither captures sources outside this folder or rolls back external checkpoint storage; see import/export.

The following paths are relative to that instance's home, not ~/fhold:

Host path Owner Purpose
system/ fhold release Exact managed OpenCode and Compose files
config/ Operator Seed-once OpenCode, Codex, Claude, AKM, and Compose settings
config/recovery/include.json Operator Same-instance recovery selection/mount policy, not a portable backup member
knowledge/ Operator and AKM Knowledge, task sources, scoped user environment, provider auth
workspace/ Operator Trusted local agent workspace
state/stack.json Control plane Versioned stack intent
state/stack.env Control plane Non-secret values derived from StackConfig intent
state/installation.json Control plane Generated release/managed-image provenance, never an identity fallback
state/applied-runtime.json Control plane Private startup-input digest and pending-activation flag; no secret values; shared by CLI/Admin restart status
state/credentials/ Control plane/operator Named Guardian key directories plus derived key-free registry
state/portal-credentials/ Control plane Derived, adapter-scoped runtime keyrings
config/guardian/oauth.json Operator OAuth resource-server settings; disabled by default
config/guardian/oauth-identities.json Operator Exact OAuth issuer/subject to credential maps
state/secrets/ Control plane/operator File-backed runtime credentials
data/ Containers Assistant home, AKM state, portal SQLite files, audit logs
data/recovery/ Recovery worker Private destination/identity-scoped receipts and staging, never portable content

Updates replace only the allowlisted managed files in seed.ts. They seed operator files only when absent and never synchronize or delete whole directories.

Native policy files under config/opencode, config/codex and config/claude are mounted read-only at the harnesses' system paths. See managed harness configuration for exact mounts, task permissions and standalone deployment inputs.

Host-side Compose variables

The control plane writes or preserves these non-secret values in state/stack.env:

Variable Meaning
FH_HOME Absolute stack home
FH_PROJECT_NAME Persisted instance name, or stable per-canonical-home default, from deployment intent
FH_INSTANCE_HOSTNAME Derived Assistant OS hostname from that same project name; not another user setting
FH_OPENCODE_PREFERENCES_FILE Derived native write target: existing opencode.jsonc, otherwise opencode.json, otherwise config.json; fresh homes seed opencode.json. Not a user setting.
FH_UID, FH_GID Non-root container identity
FH_IMAGE_NAMESPACE Image namespace; default fwdslsh (public Docker Hub); fhold selects local builds
FH_STACK_CONFIG_VERSION Derived intent schema version
FH_ENABLED_ADDONS Derived profiles: gateway,discord,slack
FH_ASSISTANT_BIND_ADDRESS Derived native OpenCode host bind
FH_ASSISTANT_PORT Derived native OpenCode host port
FH_TIMEZONE, FH_AUTOMATIC_MEMORY Derived schedule timezone and automatic memory intent
FH_CODEX_REMOTE, FH_CLAUDE_REMOTE Independent native supervisor startup switches, both default 1; optional 0 keeps a worker off without removing account state. Native sign-in/consent is still required.
FH_CODEX_SANDBOX Native Codex isolation: workspace-write default, read-only, or explicit danger-full-access container isolation; task approvals follow native policy
FH_GUARDIAN_BIND_ADDRESS Derived Guardian host bind
FH_GUARDIAN_PORT Derived Guardian host port
DISCORD_ALLOWED_GUILDS, DISCORD_ALLOWED_ROLES Derived Discord scope
DISCORD_ALLOWED_USERS, DISCORD_BLOCKED_USERS Derived Discord user scope
SLACK_ALLOWED_CHANNELS Derived Slack channel scope
SLACK_ALLOWED_USERS, SLACK_BLOCKED_USERS Derived Slack user scope
FH_SETUP_COMPLETE Install completion marker
FH_RECOVERY_URL, FH_INSTANCE_ID Derived recovery enable/destination and stable identity; file destinations use /recovery inside the container
FH_RECOVERY_DIRECTORY Exact operator backup bind root; off/Blob use an unused private placeholder under state/
FH_RECOVERY_INTERVAL_SECONDS, FH_RECOVERY_MAX_UNSAVED_SECONDS, FH_RECOVERY_OPERATION_TIMEOUT_SECONDS Capture cadence, overdue-backup warning threshold and operation deadline from recovery intent
FH_RECOVERY_INCLUDE_FILE, FH_RECOVERY_STATE_DIR Fixed policy and namespace-scoped private-state container locations
FH_RECOVERY_CREDENTIAL_FILE, FH_RECOVERY_CLIENT_ID Private file path or optional managed identity UUID, never a credential value
FH_ASSISTANT_STOP_GRACE Derived complete writer/final-recovery shutdown budget

Project, namespace and image pins belong to deployment in StackConfig. FH_ASSISTANT_VERSION, FH_GUARDIAN_VERSION and FH_PORTAL_VERSION are derived Compose inputs. Managed image defaults advance together on update; explicit pins survive. state/installation.json records verified build-release provenance, not product identity; StackConfig's product: "fhold" remains required.

GUARDIAN_ALLOWED_ORIGINS, GUARDIAN_MODERATION_TIMEOUT_MS, and GUARDIAN_ASSISTANT_TIMEOUT_MS are advanced Guardian settings. They may be passed to the host command or preserved in stack.env; neither may contain a credential. The escalation threshold is a managed security value and cannot be raised through the user overlay.

Only fhold, Guardian, Discord, and Slack interpolation keys from stack.env are copied into the Docker client process. Process-control values such as DOCKER_HOST, PATH, and COMPOSE_FILE are never trusted from that file. The same sanitized environment is used for preflight and activation.

File secrets

Host file under state/secrets/ Consumers
fhold_opencode_password Assistant, Guardian
fhold_guardian_handle_key Guardian handle encryption and ownership proofs
discord_bot_token Discord adapter
slack_bot_token Slack adapter
slack_app_token Slack adapter
fhold_recovery_connection_string Assistant recovery only; private standard Blob credential file

Named Guardian keys live at state/credentials/<username>/key; generated keys contain 32 random bytes encoded as base64url. Guardian mounts the complete credential store read-only. Each portal mounts only its generated keyring, containing the fallback and credentials referenced by that portal's config/portal/<adapter>/credentials.json user map. Bot-token files are created empty and must be filled through fhold portal token or Admin before their portal is enabled. Other secrets are mounted through Compose secrets; no secret value belongs in an environment variable.

Provider credentials are the deliberate exception to the state/secrets location. OpenCode owns knowledge/secrets/auth.json; Assistant reads it through its normal knowledge tree and OpenCode auth path, while Guardian receives only a read-only file mount for moderation.

Assistant

Assistant joins only agent_net and publishes the native OpenCode server as ${FH_ASSISTANT_BIND_ADDRESS:-127.0.0.1}:${FH_ASSISTANT_PORT:-3810}:4096. The values are derived from StackConfig and may not be changed by the custom Compose overlay. Any non-loopback bind is an explicit operator choice and bypasses Guardian.

Host source Container target Mode
data/assistant /home/fhold read/write
config/assistant /home/fhold/.config/opencode read-only
config/assistant/<preferred native file> /home/fhold/.config/opencode/<same filename> read/write nested file mount for OpenCode's native settings API
knowledge/secrets/auth.json OpenCode auth path read/write
system/assistant /etc/opencode read-only
config/opencode/opencode.json /etc/opencode/opencode.json read-only, nested operator-policy mount
config/codex/config.toml /etc/codex/config.toml read-only
config/codex/requirements.toml /etc/codex/requirements.toml read-only
config/claude/managed-settings.json /etc/claude-code/managed-settings.json read-only
config/akm /etc/akm read-write; AKM's native scheduler activation only, no delegated ingress credentials
knowledge /stash read/write
data/akm/cache /opt/akm/cache read/write
data/akm/data /opt/akm/data read/write
workspace /work read/write

Assistant's native API credential is the OpenCode server password. It receives no Guardian, portal, bot, Docker, or host-admin credential. Optional recovery uses a separate storage-only connection-string file; it is not an ingress credential and its value never appears in environment settings.

Admin changes native provider/model preferences through OpenCode's authenticated global configuration API. OpenCode updates its preferred file in place and reloads its own cache; no process/container restart is needed. The remaining user config directory and managed/operator policy stay read-only. Trusted native clients can also edit the writable preferences file. Replacing its inode with an atomic host editor, or changing the preferred filename, requires fhold restart to refresh the file bind. Admin tracks that mount change, not in-place preference content, as pending restart. Native reloads can interrupt active OpenCode work.

The OS account and home are fhold and /home/fhold; native OpenCode Basic authentication uses username user. /fhold-bundle is an image-baked, root-owned read-only AKM skills source, shared with the native fhold plugins. Custom AKM configuration is preserved; it does not prevent the built-in skills from being available through the native harness integrations.

FH_KEEPALIVE_URL optionally enables conditional HTTP heartbeats using the existing scheduler, including when user schedules are disabled. Authentication is opt-in with FH_KEEPALIVE_AUTH=opencode or a mounted FH_KEEPALIVE_AUTHORIZATION_FILE. URLs and credentials are never logged. See harness plugins and keep-alive for behavior, native approval, custom deployment configuration and the best-effort boundary.

Invalid optional settings log their names and degrade only the affected feature. Malformed scheduler intent disables scheduling; numeric recovery/shutdown tuning falls back to its documented defaults. Core health remains successful when the authenticated agent is usable. Docker's health output carries optional-feature warnings into CLI status and Admin; unavailable diagnostics never restart the container. Authentication, safe restore and actual exclusive ownership remain required.

Managed recovery additionally binds config/recovery/include.json read-only at /run/fhold-recovery/include.json, data/recovery read/write at /run/fhold-recovery-state, and the exact selected private backup directory at /recovery. The destination must be outside the instance home and cannot be its parent; links are refused before saving. Off/Blob mode uses an empty private bind placeholder. Existing operator directories are not chmodded. These mounts and storage-only secret are audited; overlays cannot substitute them or lower the enabled recovery shutdown budget. No new service is introduced.

Standalone recovery optionally uses externally supplied FH_RECOVERY_INCLUDE_FILE for additional paths/SQLite and versioned mount policy. Explicit exclusions, required external mounts and opt-in network discovery do not change normal Compose volume coverage. SQLite stays local; fhold neither provisions nor restores independent mounts. See Assistant recovery for the schema, private inspection and stopped-writer policy transition.

Optional native Codex/Claude Code remote workers inherit the same nonroot container boundary, with no additional mounts or ports. Their own native sign-in state persists under data/assistant; it is not a delegated Guardian credential or portable OpenCode provider auth. Child environments omit OpenCode server credentials and ingress settings. The mounted filesystem remains trusted native agent access, not a Guardian policy boundary. See native remote access.

Guardian

Guardian joins agent_net and ingress_net. It publishes ${FH_GUARDIAN_BIND_ADDRESS:-127.0.0.1}:${FH_GUARDIAN_PORT:-3830}:8080 only when an ingress profile is enabled.

Host source Container target Mode
data/logs /opt/fhold/logs read/write
system/guardian /opt/fhold/moderator-config read-only
config/guardian Guardian OpenCode user config read-only
knowledge/secrets/auth.json Guardian OpenCode auth path read-only
workspace /work read-only
state/credentials /run/fhold-credentials read-only

Guardian receives the named credential store, handle-signing key, and upstream OpenCode password. It has no persistent application database or writable copy of provider credentials. Each registry record selects a fixed managed Assistant agent profile. Guardian reads files through its own read-only workspace mount so MCP workspace access can reject canonical paths and file descriptors that escape /work; it never delegates that authorization decision to OpenCode's native file API.

The read-only config/guardian mount also carries oauth.json and oauth-identities.json. Both are mode 0600 operator files. They contain public identity-provider metadata and identity-to-credential names, never an OAuth token or client secret. Guardian reads identity mappings per request; resource-server configuration changes require a restart.

Portal adapters

Discord and Slack join only ingress_net, publish no host ports, and call http://guardian:8080/mcp. Each gets only its generated named-credential keyring, platform credential files, and one adapter-specific data/portal/<adapter> volume containing SQLite continuity state.

Container hardening

All four managed services:

Before start or restart, the control plane resolves the complete managed file plus user overlay and rejects boundary expansion: replaced core images or commands, changed mounts, secrets, networks, health commands, logging, runtime users or published ports beyond the StackConfig choices, plaintext secret-like environment values, custom managed-secret grants or access to agent_net, privileged containers, added capabilities, host namespaces/devices, and container-runtime mounts.