f/hold docs

Core principles

This is fhold's living product and security contract. Change it alongside any implementation that changes a boundary. fhold is a single-install personal OpenCode agent: native provider sign-in, real readiness, persistent knowledge, ordinary-language recurring work and standards-based client access.

Product and ownership

The wordmark is f/hold; plain text, commands and persisted names use lowercase fhold. Owned source is MIT-licensed. Product variables use FH_* and the source workspaces share the current product identity. Third-party native tool names, configuration and licenses are not renamed.

Assistant is the only default container and includes OpenCode, AKM and supercronic. Guardian and the single Discord/Slack Portal image are optional. CLI is the primary installer/orchestrator; Admin is a local settings utility with no server, chat, updater, tray or background control plane. The optional Claude Desktop MCPB bridge is local stdio-to-Guardian.

OpenCode owns provider discovery, models, authentication and native approvals. AKM owns knowledge and task formats. No parallel provider registry, proxy, credential format, scheduler service or plugin platform is added. Image-baked Codex/Claude workers remain experimental and vendor-native. Both supervisors default on. Native sign-in/consent is never inferred and explicit off settings remain off. No software is installed at container startup. Built-in skills are ordinary image-baked managed assets, shared through the native fhold plugins and the root-owned read-only /fhold-bundle AKM source. Operator-owned AKM catalogs may omit that source without blocking startup; the built-in skills remain available through native harness integrations. AKM and fhold plugins are preinstalled in all three native harnesses. Their existing handlers are registered as native managed hooks at image build time; no personal approval records are fabricated. Agents run as fhold, with home /home/fhold; the native OpenCode HTTP username is user. Claude and Codex sign-in guidance preserve native login processes across tool calls without implementing token exchange or accepting user consent. The image-baked fhold-admin skill provides redacted current-container diagnostics and routes to versioned Claude/Codex setup scripts. It has no host/cloud management authority. Claude's existing worker relays explicit native trust/consent through a PTY; Codex's pinned foreground app-server uses its private native Unix control socket for standard pairing, without a runtime-installed/self-updating daemon. Temporary control sockets are not recovery data. Running, account sign-in and pairing remain separate from a verified native-client tool request. Assistant includes pinned, upstream npm/npx compatible with its pinned Node runtime, installed through npm's standard global upgrade at image build time so ordinary stdio MCP clients can launch their declared commands. Bundled dependency advisories remain subject to the image scan gate; do not patch npm's internal dependency tree. No desktop keyring service or vendor-specific client dependency is added to work around a headless client bug. Image-baked native CLIs have standard /usr/local/bin launchers, including for OpenCode login shells that replace the image's PATH; no user exports are needed.

Use standard container practices and upstream-supported tool installation and configuration. Do not patch vendor tools or accumulate per-tool wrappers, startup workarounds or settings that users must recreate. Tests exercise shipped behavior through normal user interfaces; they must not repair PATH, permissions or native settings to hide a product defect. Fix the owning layer and keep the normal path small and maintainable.

Optional failures are logged, isolated and reported as degraded state while the agent continues. Scheduling, knowledge hooks, keep-alive, native remote workers and diagnostics do not become whole-container restart gates. Core readiness is the authenticated native API with its required recovery restoration and owner. Only an unusable core or unsafe continuation stops startup/writers, including missing authentication, incomplete restore or actual loss of recovery ownership.

Host-specific deployment/qualification tooling and third-party addon installers are not part of the image, CLI/Admin or product test suite. fhold retains generic runtime contracts, directory/Blob transports, product tests and reusable image smokes. External tooling consumes the shipped image and documented interfaces without importing internal source modules.

The managed/seed-once asset allowlists in packages/lib/src/control-plane/seed-manifest.ts drive CLI, Admin and the standalone Assistant image. Updates replace only release-owned managed files, seed missing operator files and never synchronize or delete whole trees. New named homes live under ~/fhold/instances/<name>; Admin's unnamed default is ~/fhold/instances/default. CLI home selection is an explicit --name/-n directory name under that root or an absolute path, then optional FH_HOME, then cwd. Admin reopens the last-used compatible instance once per app launch, using its existing recent-folder list. First launch or an unavailable/ incompatible previous home shows Welcome without seeding or adopting a folder. Manual switching keeps Welcome open across renderer reloads. ~/fhold may also hold backups/docs; it is not itself an instance or a new configuration layer. Home selection is canonical; default changes never relocate existing homes or rename their persisted project identity. Each home has stable independent project intent and saved concrete ports. Admin separates saving configuration from applying it. Container-disrupting actions require explicit confirmation, with postponement as the default. A private, per-home applied-runtime digest records known startup inputs, not credentials or agent data. Read-only snapshots report pending changes across Admin restarts and instance switches. Only successful healthy activation clears the pending state; updates also verify their selected image identities first. The existing lifecycle lock and Compose path own application. No background service, automatic restart or second configuration store is introduced. Fresh setup prefers 3810/3830 and chooses an available pair if either is in use; manual ports live under Advanced. Existing instances never change ports on refresh or update. Availability checks are preflight, not a reservation until Compose starts. Welcome starts with setup/open choices, or a compact named recent list, without a redundant welcome heading. New setup is an Install → Connect → Ready wizard; naming is requested once and optional folder/port settings share collapsed Advanced. The setup screen omits a redundant creation heading. Import has a separate Welcome entry point and a Choose folders → Review & import workflow, not an accordion or mode inside installation. Its independent folder draft does not reuse or move installation controls. Back appears only on review, preserves both folder choices and invalidates the preview; Cancel exits the flow. First steps have one Cancel action, not duplicate Back/Cancel navigation. After installation, Finish later leaves the agent running. Native import protection remains the same. Disclosure controls share the ordinary filled control styling and explicit chevrons. The management sidebar combines the slash and named picker on one row without a second wordmark. Read-only Docker/Compose checks run independently of instance selection so they do not block naming or create a home. Successful checks add no status clutter; missing prerequisites show actionable guidance and an explicit retry. Each stage has explicit navigation out of setup; leaving an installed agent does not stop it. Opening an existing instance never silently starts a fresh installation. Compatible-home labels are read from current intent, not stored in a second registry. Preparing an empty/new folder does not install anything until explicit confirmation. A chosen DNS-safe name reuses deployment.projectName for Compose/container naming and the Assistant hostname; the CLI uses that same name to select its default folder, not a separate install-name option or second name registry. Fresh setup refuses names already present in Docker. Compose commands check project working-directory ownership before acting, so a selected folder cannot take over another instance. Opening/switching homes never resizes the window or starts/stops a stack.

Admin Apps is organized by the app the user wants to connect, not by protocol, container or harness: OpenCode first, then Claude, Codex, Discord, Slack and MCP. Claude offers Desktop chat and Claude remote connection, not a choice between similarly named developer products. Each bot owns its token setup, allowlists, inline permissions and individual user mappings. Apps owns app permissions; there is no separate access-manager navigation destination. OpenCode/MCP network controls stay under that app's Advanced settings. App-scoped saves use the existing intent/baseline/restart path and preserve other apps' drafts; bot enablement retains its shared MCP dependency. There is no new connection registry, protocol or generic integration engine. Configured, running, native startup intent and verified client readiness remain distinct. Native workers remain experimental and require native consent.

Guarded apps present inline Chat only, Read files and Full access choices; granting Full access requires explicit confirmation. Normal bot setup hides fhold keys entirely and preserves its assigned identity without a credential picker. Claude Desktop/MCP explicitly create a connection before showing the key-copy step needed by the external app. A collapsed Advanced access section at the bottom of Apps offers compact permission editing with real Manage buttons, not key cards, accounts or a policy matrix. Existing registry IDs, values, defaults and policy contracts do not change; no new policy or app-assignment store is added. Native OpenCode/Claude remote connection/Codex access is independent of Guardian permissions. Copy fetches connection keys only on explicit request; there is no key-reveal UI and values never persist in renderer preferences. Permission dialogs have no copy, replace or remove actions. System's collapsed Connection keys section owns explicit, confirmed key rotation; it preserves identity, permissions and conversations and explains that external apps must update their copied key. Native assignment/final-key restrictions remain unchanged. Changing a reused identity's permissions affects all uses and requires explicit confirmation. Ordinary edits preserve its ID/value and conversation ownership. Per-app saved-access selectors are not part of ordinary setup. Creating a new external connection is explicit; editing an existing one belongs to Advanced access. Never silently fork, transfer or revoke identity. Known bot assignments are shown honestly; external key use cannot be detected or inferred from a credential name. App-scoped saves preserve other drafts, use the reviewed configuration baseline and retain the existing deferred restart path. Opening setup/status creates no keys, assigns no access and performs no automatic permission escalation.

Management notifications and the persistent pending-restart alert live at the bottom of the sidebar, including at narrow widths. Setup retains visible inline feedback rather than placing errors in its hidden sidebar. Restart activation still requires explicit confirmation; moving alerts never restarts containers or resizes the window.

Agent settings starts with the resolved native default model, not a directory of apparently connected accounts. Saved sign-ins and endpoint definitions are a secondary, reviewable inventory; they never prove a working connection. Add AI service guides native sign-in or an OpenAI-compatible endpoint, model selection, an explicit short response test and an explicit Use this model action. Listing, saving credentials and discovering model IDs make no inference request. Model discovery runs from the Assistant's network, not the Admin host. Keys remain in native auth, never inline in generated provider configuration. Native OAuth attempts remain provider/method/instance bound until completed or cancelled. Tests stay on the page and never change the default model; only explicit Use changes it. The model picker uses native text-input/text-output/tool-capable, non-deprecated models. A model list alone is not verification of tool execution. Targeted PATCH /global/config calls let OpenCode persist and reload its own preferences, preserving unrelated settings and JSONC comments. Only its preferred user settings file is writable; native trusted clients can edit that file too. Config directories, plugins and operator policy remain read-only. Native reloads can interrupt active OpenCode work, but do not restart its process or container. Confirm the effective native result after reload; never roll back a native file from a client-side snapshot. Disable endpoint uses native disabled_providers, preserving its definition and credentials; saving it again explicitly re-enables only that endpoint. Sign-in removal is exact and separate from endpoint disabling or vendor-side revocation. Never silently discard restored credentials, including IDs missing from the catalog. No model-name heuristics, duplicate provider registry or raw JSON UI is added. Memory and scheduling preferences remain advanced CLI/config intent, not setup questions. Fresh installs enable AKM/automatic memory and default to the host OS timezone, preserving existing explicit choices.

state/stack.json requires product: "fhold" and owns deployment intent. Derived env/keyrings do not own settings. Inspection is read-only; mutations use a shared per-home lock and current intent. Foreign/nonempty unrelated homes are refused, not adopted. fhold has no foreign-home importer or aliases.

Security

Persistence and recovery

system/ is release-owned; config/, knowledge/ and workspace/ are operator-owned. state/ contains control-plane intent/derived credentials; data/ contains durable native runtime state. Portable backup is not full runtime recovery: preserve detected runtime data, links and external files separately, and report omissions prominently.

Own-backup restore requires a supported bounded fhold manifest, validates hashes and preview digest, refuses conflicts/path escapes, preserves source/target and requires explicit sensitive-data opt-in. Per-file atomic writes are not transaction-wide rollback. Private plans/receipts make partial failure honest. Native history recovery uses offline non-root workers, WAL-inclusive consistent snapshots, explicit workspace mappings, stripped old authority, collision checks, existing-target preservation and private retry journals outside knowledge. Unfinished tool calls block by default. Explicit reviewed archival may convert them to native terminal interrupted errors without replaying them; raw exports remain unchanged and private receipts identify every conversion.

An explicit whole-instance export/import scope preserves the entire stopped home, including native sessions/databases (and WAL/SHM), account credentials, permissions, plugins, active tasks and deployment identity. Both operations verify stopped Docker writers under the ordinary lifecycle lock; other writers must also be stopped. The same manifest envelope records all files, directories and unfollowed links, with hashes and explicit process-coordination omissions. Full import only accepts an empty destination, validates the complete inventory and preserves the original home/export. It reconciles generated host paths/owner metadata without seeding, changing native authority or upgrading images. Containers remain stopped; partial imports block startup and retain private evidence. CLI requires explicit stopped confirmation; Admin additionally binds apply to preview and explains sensitive data, active schedules, downtime, external sources and single-instance identity. External drives/checkpoint namespaces are not rolled back. This is not an active clone, cross-version converter or a substitute for ephemeral checkpoint ownership.

Opt-in Assistant-only ephemeral recovery is a separate, sensitive same-instance format, not portable import. One image-baked Bun worker restores before default seeding or any AKM/native writer, takes bounded SQLite-native snapshots on its own timer, and publishes complete immutable generations under ownership and a conditional descriptor. Live SQLite stays on local storage. Local or mounted network directories hold artifacts, subject to coherent atomic filesystem semantics; object-storage transports share the same recovery core. No deployment, wake, scale, cloud-management tooling, extra service or mandatory sidecar is added.

One optional externally supplied FH_RECOVERY_INCLUDE_FILE extends the native catalog with literal absolute file/directory paths and registered SQLite files. The versioned selection is recorded with each checkpoint and bound by hashes. Restore applies the current selection without writing newly excluded or unselected paths; the next checkpoint records the edited coverage. No new namespace is required for coverage edits. Lists have no fixed entry cap, but all existing size/count/deadline, link, private-state and destination boundaries still apply. CLI/Admin expose the same generic settings through StackConfig and an operator policy file, not another backup engine, format, service, plugin registry or cloud management layer. Saving is deferred. Initialization/offline restore require confirmation and stopped local writers; status/coverage inspection are read-only. Host tools and the image share the selection normalizer. Blob credentials are private files granted only to Assistant, never environment values or portable content. Compose adds exact directory/policy/private-state mounts and derives adequate termination grace without widening native or ingress authority. Versioned recovery policy may exclude directory roots, require independent mount roots and opt into recognized network-mount discovery. Ordinary local volumes stay covered. Excluded roots are never traversed/written; missing required mounts block startup. Registered SQLite and WAL/SHM paths cannot be excluded or placed on recognized network filesystems. SQLite discovered in selected trees uses native snapshots without requiring a second path list. Recorded policy/effective exclusions remain part of historical checkpoints; current policy governs restore. Topology changes during an operation abort publication and retain partial-restore journals. Private inspection is read-only; confirmed offline restore starts no application writers. Native process coordination remains excluded even under an explicitly selected parent; plugin caches may be included without restoring temporary wrappers or live locks. This does not relax the prohibition on arbitrary links.

Explicit initialization is one-time; an established missing/corrupt/incompatible head cannot become a blank agent. Valid newer surviving local state is preserved. An accepted-generation surviving receipt permits restoring only missing selected checkpoint members after ephemeral storage loss, preserving newer surviving local files and databases. Unreceipted partial state and orphan WAL/SHM files remain blocked. Incomplete restore journals block native startup. Known initialized core and explicitly selected databases cannot silently disappear during active capture; versioned native Codex filenames may change during native migrations. Package versions and ownership epochs are provenance, not restore-compatibility gates. Supported checkpoint format, identity, hashes, database integrity and current ownership remain enforced. Native tools own their schema upgrades. Warm startup does not apply backup inventory limits to an already receipted home; only missing checkpoint members are validated and restored, without overwriting surviving data. Routine capture does not stop the apps, and separately captured databases and files are not one application-wide transaction. Readiness requires completed restoration and ownership. A delayed/failed checkpoint warns about durability and retries without stopping a working agent. Actual ownership loss still stops writers. Private diagnostic writes and the optional probe can fail without invalidating ownership; status writes retry. The engine owns the accepted publication clock; health does not maintain a separate timestamp that can diverge after publication. SIGTERM stops writers and descendants before the bounded final checkpoint; forced termination can lose unpublished changes. Writer shutdown and recovery have separate bounded waits so an in-flight capture cannot consume the stopped-writer final checkpoint's budget. External host grace must cover both phases and be tested in that environment.

Directory ownership has no automatic timeout takeover. External tooling must confirm the previous writer is stopped before explicitly clearing a stale owner. Publication fencing does not make arbitrary external tool actions exactly-once. Private recovery includes supported native account/trust/approval files without auto-trusting anything; actual token refresh and vendor reconnect remain separate qualification gates. Same-instance recovery with explicit scheduling enablement preserves reviewed future-only intent; scheduling off leaves stored tasks inactive. See Assistant recovery for supported destinations, configuration, limits and verification evidence.

Ordinary updates preflight identity/config/assets before managed writes and retain private control-plane checkpoints/failure evidence. They preserve knowledge, history, keys, approvals and scheduling intent. Offline file refresh is not a running-container upgrade; do not downgrade images automatically after native data changes. Future schema conversions belong beside a real changed schema, not an empty timestamp-release migration registry.

Current release boundary

GitHub at https://github.com/fwdslsh/fhold is the canonical source and contribution host. GitHub builds Linux standalone CLI and Admin AppImage downloads and publishes Assistant/Guardian/Portal images to public Docker Hub. Every candidate needs its own artifact and runtime gates; earlier artifacts do not qualify changed source. The 0.1 release line uses real UTC X.Y.yyMMddHHmm timestamps and explicit alpha/beta/rc channels. From 0.2, the patch format is yyMMdd<build>: UTC year/month/day plus an unpadded daily build number starting at 1, with the same prerelease suffixes. See the release runbook for examples and date/build ordering. Historical 0.1 versions remain valid and immutable. This changes release naming, not recovery or runtime schemas. The CLI remains a standalone executable, not an npm bootstrap package. Fresh installs use pinned fwdslsh/fhold-{assistant,guardian,portal} images. Explicit local builds use the fhold namespace without registry pulls. Native Linux x64 and ARM64 runners test runtime images and CLI; ARM64 Admin startup and Windows/macOS packaging remain unqualified. No cross-host publishing bridge is needed. An existing version/receipt permits identical-byte retries only; changed source or content requires a new version, including unpublished candidates.