443 lines
45 KiB
Markdown
443 lines
45 KiB
Markdown
# Termind architecture
|
||
|
||
Current ownership and safety contracts; implementation details belong in code/tests, completed-work evidence in
|
||
[REFACTOR.md](REFACTOR.md). Read before changing dependencies, lifetimes, persistence, execution or secrets.
|
||
|
||
## Modules and persistence
|
||
|
||
One Xcode project builds TFM (macOS), TFI (iOS/iPadOS), separate test targets, the macOS-only `termind-mcp` bridge and
|
||
the standalone macOS `termind-port-forwarding-benchmark` tool. The benchmark depends on shared frameworks through a
|
||
narrow SPI; apps and test bundles never host or depend on it. AgentKit is a separately versioned remote package; its
|
||
sibling working tree is not the app dependency. Follow `AGENTS.md` for package releases and pinned builds.
|
||
|
||
| Owner | Responsibilities / dependencies |
|
||
| --- | --- |
|
||
| TermindDomain | Portable values/rules; Foundation, CryptoKit, AgentKit values; no UI/concrete I/O. |
|
||
| TermindApplication | Ports, feature models, connection/import policy, remote-file-system and sync/outbox use cases; Domain, AgentKit. |
|
||
| TermindInfrastructure | In-memory repositories, metrics parsing, StoreKit, Vault/Settings/Metrics CloudKit; Application, Domain, Persistence. |
|
||
| TermindRemoteInfrastructure | SSH/SFTP, Keychain, keys, sampling, forwarding, transfers, scoped imports; Infrastructure, Application, Domain. C libraries stay outside portable framework tests. |
|
||
| TermindPresentation | Reusable SwiftUI policy/content, Ghostty themes, `TerminalPreferences` bridge; Application and Domain only. |
|
||
| TermindPersistence | All SwiftData models, frozen V1–V22 schemas, migrations and container construction; no app dependency. |
|
||
| TFM / TFI | Native UI/adapters/lifetimes; shared modules never import apps. |
|
||
|
||
- Use direct owner imports/public APIs, not re-exports, forwarding aliases, `Shared` types or a generic Core module.
|
||
Shared resources enter both app bundles; icons, metadata and signing remain platform-owned.
|
||
- Moves preserve entity identity, fields, defaults and historical semantics. Original-module V1/V14 fixtures guard
|
||
migration. `makeContainer` fixes the complete schema/plan; one container per process serves all scenes. Never
|
||
recreate a failed store silently. macOS retains its `Termind` location; mobile uses Application Support/in-memory
|
||
tests. Both apps share the CloudKit container and bundle-derived Keychain access group.
|
||
- Architecture tests fail closed on absent/empty production roots. Source checks guard dependencies, native-list
|
||
allowlists, AgentKit/SwiftData ownership only; UI/copy/wiring need behavioral or visual coverage.
|
||
|
||
## State, navigation and editing
|
||
|
||
### Saved values
|
||
|
||
- Single-line presentation normalizes newlines only on edits; paste adapters act before native controls flatten
|
||
breaks and rename editors preserve marked text. Reads, multiline content, secrets and validation stay unchanged;
|
||
no bulk cleanup/migration.
|
||
- Domain optional-name defaults serve hints and saves: Host address, Identity username, key algorithm, Host plus SSH
|
||
argument, provider protocol or model ID. Missing sources yield empty hints. Nonempty names remain stored; clearing
|
||
requests a fresh default. Legacy blanks materialize on the next local save in its existing transaction.
|
||
- Desktop autosave uses page-owned buffers; reopening shows saved values. Mobile provider drafts merge raw names
|
||
before defaulting at Save: explicit clearing conflicts with a concurrent rename, unlike legacy blank-to-default.
|
||
|
||
### Windows and sessions
|
||
|
||
- App-wide snapshots/preferences and `VaultStore` transactions feed narrow feature intents; sessions receive only
|
||
`RemoteSessionHostStore`. Each scene owns a `WindowModel`, shell/navigation/search/context panels. Process routing
|
||
is UUID-scoped with weak native windows. Paywalls stay pinned to one window; speech belongs to one composer.
|
||
- New desktop windows start at Welcome; Dock reopen preserves destination and Metrics entry selects Metrics. Dock
|
||
New Window uses the registered factory and activates hidden/windowless apps. Restoration never reconnects live
|
||
Workspaces. New windows snapshot ordinary content size once, after full-size content styling; fullscreen/auxiliary
|
||
windows cannot replace device-local last size. System-restored frames/positions win; tests use in-memory sizing.
|
||
- Quick Open claims focus only after mounting in its active window; close retires requests. It releases toolbar Find
|
||
and suspends terminal autofocus. Batch Startup opens selected SSH/SFTP Hosts as panes in one new Workspace.
|
||
- Command-W closes the focused pane, removing empty Workspaces but retaining the window; otherwise dismisses natively.
|
||
All window-close paths confirm if that window owns any Workspaces, even hidden/disconnected. Shift-Command-W requests
|
||
window dismissal. Clearing document-edited decoration must not change confirmation/delegate behavior.
|
||
- Moving a Workspace between registered windows preserves surfaces/transports/splits/state, without close history;
|
||
out-of-window drags create no window. Native close disconnects only owned sessions and cancels pending moves;
|
||
independent transfers/services survive. Mounting never repeats authentication; explicit retry/reopen may.
|
||
- Desktop Vault/Assistant, Metrics, dynamic Workspaces and Settings routes are window-local. Settings data is app-wide;
|
||
closing it restores the replaced destination. Width changes may adapt panels, never routes/editor identity/session
|
||
composition/monitor ownership; covered panels retain last size, native table identity, sorting and scroll.
|
||
- `AssistantLibraryModel` owns standalone conversations, not panes. Workspaces own SSH/SFTP/serial panes and session
|
||
conversations; sessions own transports/terminal/browser state. Ordering/pinning commits metadata before publication
|
||
and preserves instances during runs/approvals/compaction. Archive/delete keep run guards; archived chats cannot move.
|
||
- Connection/authentication/forwarding state is memory-only, excluded from domain records, sync and UserDefaults.
|
||
Serial is local/temporary, never persisted or an Agent host. Vault-synced Workspace templates contain named
|
||
SSH/SFTP/empty split trees/fractions; only TFM materializes them, preserving empty panes for missing Hosts.
|
||
|
||
### Trust, imports and local preferences
|
||
|
||
- Known Hosts is independent of Host records: editing/deleting a Host never changes trust. Plain and OpenSSH `|1|`
|
||
hashed endpoints share matching; hashed records retain wire hostname and ignore the separate port.
|
||
- SSH import previews are window-owned indexed public metadata. Host selection never selects credentials; imported
|
||
identities require selected keys. The actor retains scoped descriptors/digests, not private-key text/passphrases;
|
||
reread selected keys at unlock/commit and fail closed if changed. Picking/drop share validation; config parsing
|
||
executes nothing. Create credentials before one Vault save, clean up failure, never overwrite records/clear mismatch.
|
||
- macOS appearance/language are local; System appearance clears `NSApplication.appearance`. Login launch reads
|
||
`SMAppService`, not mirrored preferences; quiet launch hides the app without suppressing its window. Mobile uses
|
||
Apple's per-app language settings without an injected locale override.
|
||
- Hosts group, Snippets package and both Ungrouped expansion states are device-local and survive relaunch without
|
||
entering Vault persistence or sync. TFM's compact Snippets sidebar keeps a separate device-local expansion state.
|
||
Legacy synced expansion values seed each installation once; new groups and packages start expanded.
|
||
- Debug visibility/logging are device-local/default-off for saving. Inject SwiftLog `Logger`; no process-global
|
||
bootstrap. Infrastructure owns one locked writer/file per launch: disable closes but preserves, re-enable resumes,
|
||
deletion recreates only when enabled. No retention policy or in-app viewer. Debug stderr uses the same sanitization.
|
||
Log stable English codes/non-sensitive metadata, never credentials, prompts, identities/paths, remote output, process
|
||
names or raw errors. Metrics always logs received byte size; capped incomplete/oversized diagnostics redact addresses.
|
||
|
||
## Remote sessions, terminals and files
|
||
|
||
### Connection and authentication
|
||
|
||
- SSH/SFTP/mobile Workspaces/Metrics share preparation/recovery policy. Host-key approval occurs inside the actual
|
||
socket/libssh2 handshake-to-auth attempt, never a discovery connection. Generations reject retired callbacks,
|
||
credential results and timers.
|
||
- Every resolved Password, software Private Key, Key Passphrase and proxy Password is one-shot per physical connection
|
||
attempt. Runtime copies share one explicitly owned, non-copy-on-write allocation; source `Data`, proxy frames and the
|
||
allocation are overwritten before release. The transport clears it after the authentication call and every success,
|
||
failure and cancellation boundary clears it again. UI submissions move plaintext into that one-shot owner before
|
||
leaving retry state; retry/session/Agent/Metrics/transfer state retains only reentry flags and non-secret Identity/Key
|
||
provenance. Saved material is reread from Keychain for another physical connection; temporary Passwords and every
|
||
Passphrase must be entered again. UI authentication fields clear synchronously on submission. Availability checks query
|
||
Keychain metadata without reading secret data; the legacy software-Key JSON envelope is parsed strictly and fails closed.
|
||
- Desktop SSH/SFTP submission uses `RemoteSessionHostStore`: explicit password wins; custom input never borrows an
|
||
unselected Identity password. Only explicit input changes selected credentials; custom input reuses the Host Identity.
|
||
Identity/secret writes precede separate Host association. Keep temporary credentials, validation/passphrase ordering,
|
||
transient Hosts and reconnect state session-owned.
|
||
- Keyboard-interactive uses the same opener for PTY/exec/SFTP/forwarding/key installation/jumps. C callbacks wait only
|
||
on the transport queue for cancellable async responders showing literal server prompts/echo flags. Request IDs isolate
|
||
rounds; answers are ephemeral, never autofilled/cached/logged/in Agent history. Empty answers are valid; zero prompts
|
||
need no form. Waiting excludes network timeout; cancellation wakes the callback, cancels responder and interrupts open.
|
||
- User sessions own challenge queues; Agents use interaction queues. Desktop external auth separates app/MCP origins;
|
||
stopping MCP preserves app requests. Mobile has no app auth sheet. Forwarding/Metrics own Journey challenges, never
|
||
external-window fallback.
|
||
- Terminal drops/additional transfer and forwarding connections forbid password/passphrase/MFA/trust forms and system
|
||
user-presence authorization (Touch ID or device password) throughout the route. Reusable snapshots freeze target,
|
||
Jump Host and proxy network data but retain authentication only as Identity IDs, plus a Key ID only when no Identity
|
||
exists. Each silent open resolves the Identity's current username/password/Key from Vault and Keychain. Protected Keys
|
||
are skipped before their secret is read; password and other noninteractive methods remain eligible. Passphrases and
|
||
temporary passwords never enter reusable snapshots. Username-only/SSH `none` remain eligible. Cancellation interrupts
|
||
opening. A terminal drop tries an independent SFTP preflight when durable references are available; any non-cancellation
|
||
failure falls back to SFTP Channels on the already authenticated PTY connection without presenting a form.
|
||
- Username-only Identities try `none`, then advertised methods. Device-local `lastUsedAt` tracks successful saved-secret
|
||
reads, not authentication. Batch route reads; missing/availability/edit reads do not count. First access per Identity/
|
||
process writes directly without Vault journal/refresh; sync strips this field and preserves local values on apply.
|
||
- Exporting several authorized Keys resolves credentials immediately before work, authenticates once per Host and opens
|
||
one exec Channel per Key on that connection. Local validation completes before the remote account is changed.
|
||
- Welcome parses bounded SSH grammar without shell/OpenSSH config execution. Exact aliases supply metadata; overrides
|
||
create session route snapshots, never replaced with saved endpoints. Routes cap at eight Hosts and reject cycles.
|
||
Connect saves nothing. Explicit Save copies changed records/prerequisites in one Vault transaction, rolling back new
|
||
passwords on failure; originals remain unchanged. Host-key decisions stay in the live handshake.
|
||
|
||
### Port Forwarding
|
||
|
||
- Independent Vault library/rules, outside Host drafts, with rollback-safe save/delete and saved names unaffected by Host
|
||
rename. V20 freezes earlier entities and adds palette color. UI selection/editor transitions do not own connections or
|
||
challenges. Start/retry is single-rule only; no bulk start. Only explicit actions create running intent.
|
||
- macOS Hosts may allocate one to 64 physical forwarding SSH connections; the default is one. Local and SOCKS rules
|
||
share the Host pool and dispatch each accepted TCP flow to the least-loaded healthy worker. Remote listeners use one
|
||
worker. iOS preserves the synced Host value but always runs one connection. Pool size is snapshotted at first Start.
|
||
- The first connection owns Journey and interactive authentication. Auxiliary connections use refreshed durable trust and
|
||
the authenticated in-memory configuration with forms forbidden; once-only trust/MFA answers never authorize them. At
|
||
most four opens run concurrently. Transient worker loss retries after 1/2/5/10/30 seconds; interaction-required auxiliary
|
||
failures leave a degraded pool. Old TCP streams never migrate or replay.
|
||
- Stop one rule without disturbing peers. Last stop cancels opens/prompts/retries and releases credential snapshots.
|
||
Expiration/termination stop resources; Host/route edits and rule replacement stop affected runs. Generations reject stale
|
||
opens/listeners; replacement awaits cleanup. A pool-level listener owns each local bind and pauses when no worker is
|
||
healthy. Disconnect completes after source-cancellation descriptor closure. Local/SOCKS use SO_REUSEADDR for TIME_WAIT,
|
||
never SO_REUSEPORT/live shared binds.
|
||
- One serial owner per libssh2 session across PTY, exec, SFTP, jump bridges and forwarding. Readiness, deduplicated FIFO and
|
||
bounded fair passes replace bulk polling; Metrics and user SSH use `SSHSingleChannelAdapter` without a topology scheduler.
|
||
Preserve immutable write buffers across EAGAIN, drain half-close, bound queues/DNS/connect deadlines and retain
|
||
session-wide pending operations. Slice deadlines include callbacks/inner loops; protocol progress wakes peers.
|
||
- Every authenticated SSH connection has one FIFO Max Sessions budget shared by its PTY, exec and SFTP Channels.
|
||
The synced Connections preference defaults to ten and may change from one to 100; lowering it never closes active Channels,
|
||
and new work waits until capacity is available. Direct/forwarded TCP Channels, remote listeners and jump-host bridges are
|
||
forwarding rather than sessions and never consume this budget.
|
||
- Public send/receive adapters accept complete encrypted packets and apply bounded backpressure outside library calls,
|
||
avoiding pinned libssh2's lost forwarded-tcpip confirmation on EAGAIN. Teardown discards writes/returns EOF; descriptors
|
||
close after cancellation handlers. See [performance](PORT_FORWARDING_PERFORMANCE.md) for budgets, rationale and measurement
|
||
limits; buffers are per flow/session, never a global memory cap.
|
||
|
||
### Terminal behavior
|
||
|
||
- SSH worker owns PTY dimensions; retain latest valid authentication-time size/pixels. EAGAIN window-change packets keep
|
||
original dimensions and finish before the newest request without blocking other I/O. Disconnect clears resize state;
|
||
retired auth cannot install a PTY/clear replacement dimensions. Ghostty computes grid. Resize failure is diagnostic only.
|
||
- Application owns `TerminalCommandDraftController`, `CommandGenerationCoordinator` and `TerminalFindController`; apps
|
||
provide context/validity/insertion. Generated commands are inserted, never submitted.
|
||
- Font family/size/follow-theme-size are device-local, outside `TerminalPreferences`, Settings journal and CloudKit. Seed
|
||
missing local size from pre-upgrade local data before remote materialization; existing mirrors win. Ignore legacy remote
|
||
font fields and omit outgoing fields. Themes/custom text (including authored fonts) still sync. Missing fonts fall back
|
||
without deleting choice. Apply local overrides after theme/custom commands, resetting family lists; update surfaces in place.
|
||
- Desktop copy-completed-selection/right-click-paste default off and sync in the terminal record; mobile preserves without
|
||
exposing/applying. Keep native surfaces. Copy only local completion; paste once on release through Ghostty clipboard/input
|
||
capture. Remote mouse/Option-right-click stay Ghostty-owned; mouse reports never broadcast. Empty paste arms no capture;
|
||
bracketed-paste classification spans chunks through terminator/ticket expiry.
|
||
- Host Shell Integration is Vault-synced Default/Enable/Disable (V22, older Hosts frozen). Each SSH attempt resolves destination
|
||
override/global once before auth for bootstrap/History/generator/system info/directory actions. Live edits never inject or
|
||
reconnect; new reconnects resolve again. Mobile integration/exit uses one ordered MainActor consumer retired on close/
|
||
reconnect/expiration. Prompt markers are idempotent; Bash DEBUG excludes `__termind_*` helpers from History/busy state.
|
||
- History starts after authenticated start, retains optional remote status/duration (never receive-time inference), normalizes
|
||
backslash continuation only and caps at 1,000 rows/Host. Both UIs show first line plus ellipsis.
|
||
|
||
### SFTP and external edits
|
||
|
||
- Domain owns `RemoteFile`, Application `RemoteFileSystem`/`MobileSFTPChannelModel`, Remote Infrastructure libssh2 protocols,
|
||
Presentation symbols/kinds. Device-local History changes only after fresh successful listings/downloads, never stale/failed/
|
||
cancelled/type-changed work.
|
||
- `SFTPClient` is the connect/disconnect facade and owns its authenticated `SSHConnection`; `SFTPChannel` is the sole
|
||
file-operation implementation. Standalone SFTP, Agent primary SFTP and terminal fallback therefore
|
||
share Channel open/close/EAGAIN, atomic-write, resume, edit, streaming and copy behavior; feature owners retain their distinct
|
||
authentication, pooling and retirement lifetimes.
|
||
- Generation-scoped `SFTPTransferContext` outlives panes. SFTP data work resolves one topology per admitted file: Channels Only
|
||
opens sibling SFTP Channels, Connections Preferred silently probes the full route and falls back to siblings, and Smart uses
|
||
Channels through 64 MB/64,000,000 bytes (including unknown sizes) before preferring connections. A failed silent probe degrades later work
|
||
without replaying active operations; cancellation caches no failure. Browser control Channels stay independent. Terminal
|
||
preflight donates a successful independent connection; only failed preflight uses the terminal's multiplexed PTY/SFTP
|
||
libssh2 owner and fixes that generation to Channels. Closing the PTY does not retire transfers holding that transport.
|
||
Durable trust refresh never promotes once-only trust.
|
||
- One process scheduler covers Workspace/terminal/edit transfers and Agent data operations. It defaults to 30 active files/ten
|
||
per Host.ID (ranges 1–100 and 1–min(50, global)), rotating Hosts then Workspace/Agent-run/batch sources while retaining FIFO
|
||
within a source. A Channel and an independent connection consume the same logical-file capacity. Cross-Host copy consumes one
|
||
global slot and one slot on each endpoint Host. Browser and Agent metadata/control operations do not consume data slots.
|
||
`SFTPSettingsModel` scheduling outlives Settings; new limits/topology affect queued work only. Pane close releases only its
|
||
ownership; queued work retains the primary connection, paused work configuration/checkpoints but no connection.
|
||
- Checkpoints, references, acknowledged offsets and digests are memory-only. Pause/loss retains partial destinations; explicit
|
||
resume verifies source/prefix. No ordinary staging/bookmarks/restart recovery or silent replay. Cancel/Host delete removes
|
||
partials and reports failed cleanup. Mobile expiration pauses upload/download, cancels copies; desktop quit allows three seconds.
|
||
Same-connection copies alternate bounded reads/writes, validate size/metadata/EOF and atomically commit temporary files.
|
||
- Shared planners retain directory-only batch destinations/mutation counts. In-app SFTP-to-SFTP drag/copy consumes the private
|
||
remote payload before asking for its Finder file promise, streams directly between clients without local staging, and exposes
|
||
exactly one `.copy` queue row per regular file. Apps own UI, cancellation/errors/refresh and deletion.
|
||
`SFTPRemoteConflictResolver` validates recursive child paths, subtree mapping/Keep Both: upload blocks strict descendants; copy
|
||
also blocks conflicting root. Desktop context menus snapshot reconciled selection; appearance never mutates it.
|
||
- Terminal drop preparation is cancellable/generation-scoped; late results cannot enqueue. Nonmodal notices cannot replace same-batch
|
||
failures. Native browser alerts/conflict choices stay app-owned.
|
||
- One process-local Application external-edit model outlives browsers/Workspaces. Remote Infrastructure owns managed copies, streamed
|
||
SHA-256, atomic-replacement observation and immutable upload snapshots. Upload is explicit; paused edit uploads retain staging until
|
||
terminal completion. Dirty-only cards do not own edit lifetime; clean records remain reusable.
|
||
- Discard stops tracking but preserves local copy; next Open downloads anew. Running/preparing uploads cannot discard; paused uploads
|
||
cancel/clean first. Desktop Keep Local Copy uses save panel/coordinated copy, not ShareLink, and checks edits before quit. Mobile
|
||
promises no quit warning/restart recovery; paused uploads need explicit resume.
|
||
- Validate original kind/size/mtime/mode before write and rename, preserving permissions. Overwrite uses POSIX rename, falls back only
|
||
if unsupported, never unlinks original on failure; non-overwrite never uses POSIX overwrite. This is best-effort, not atomic CAS.
|
||
Missing/changed metadata needs confirmation; nonregular destinations fail closed. Acknowledge only uploaded snapshot digest so later
|
||
saves remain dirty. Host removal/route edits invalidate records but preserve copies. All regular files use SHA-256.
|
||
- Native Quick Look/Open With remain app-owned and reuse managed copies; macOS invocation choices never change defaults. Mobile rejects
|
||
retired/background late presentations but preserves active handoffs across deactivation. Same-format imports target valid nonbusy edit
|
||
records, retain remote baseline and mark dirty; ordinary upload/conflict policy commits.
|
||
|
||
### Agent/MCP byte channels
|
||
|
||
- Agent SSH and the primary Agent SFTP subsystem share one pooled Host transport and one fair libssh2 owner; concurrent exec and
|
||
SFTP work use separate Channels on it regardless of which tool connects first. Agent SFTP data work shares the process SFTP
|
||
scheduler and resolves Smart/Channel-only/connection-preferred topology by the synced preference; metadata/control calls use
|
||
Channels without consuming data capacity. Connection-preferred work may add silent SFTP transports, which retain the same
|
||
180-second idle pool across conversations/MCP and fall back to Channels on the shared primary.
|
||
`sftp_copy` belongs to Agent SFTP: regular trees to existing directories, browser conflict/merge policy and preserved sources.
|
||
Cross-Host copies take two ordered endpoint leases; same-Host copies use two leases for Connections Preferred and one SFTP
|
||
Channel for Channel-only/Smart-unknown work. Active exec/SFTP leases block idle sweeping; background commands
|
||
retain only their Channel, not the whole transport. Started operations never replay. No model bytes/staging/scratch.
|
||
- macOS MCP exposes the same definition; Bearer/tool switches/Full access remain boundaries, auth uses external interaction. Stop awaits
|
||
copies. Audit metadata/counts/endpoints/policy, never bodies. Stdio only forwards protocol; mobile has no local MCP listener.
|
||
- Separate loopback HTTP upload/download capabilities are ten-minute, session-bound 256-bit secrets for Host/path/intent, in headers only.
|
||
Authenticate/atomically claim before body/network work. Ordinary MCP retains Bearer/4 MiB cap. Dedicated HTTP-neutral streams/leases use
|
||
backpressure, no staging/scratch. No directory overwrite; failed cleanup reports temporary path.
|
||
- Upload commits only after exact length/incremental SHA-256. In-memory resume uses acknowledged remote offset, matching `Upload-Offset`
|
||
and suffix Content-Length; reject parallel claims, verify prefix, never replay committed bytes. Lost responses cannot undo commit.
|
||
- Download ticket creation does no remote I/O. GET regular files only, no HEAD/bodies/query credentials; one `Range: bytes=N-` uses client
|
||
saved length. Invalid range returns 416 without consuming ticket; resume rereads/verifies prefix/metadata before headers. Dedicated
|
||
worker owns handles/queue; FSTAT fixes length and EOF revalidates bytes/size/mtime, not an atomic remote snapshot.
|
||
- Download pulls at most 64 KiB after each send. Success requires source validation, sent completion, checked close and lease release.
|
||
Pre-header errors use safe JSON; post-header errors close without appended JSON. Clients must poll status and verify local size/hash;
|
||
HTTP 200/Content-Length alone proves neither success nor persistence.
|
||
- Shared metadata/expiry policy, separate directional state/claims/errors/ordering. Four active slots, each direction at most 64 unfinished
|
||
and 256 terminal records for thirty minutes. Live work blocks idle expiry; completion remains queryable. Disconnect/recoverable SSH
|
||
failure pauses; explicit revocation cancels. Resume needs original active session/unexpired capability; exit loses both. Generation IDs
|
||
reject old callbacks. Host/session retirement, stop/token rotation/direction disable revoke; release capacity after cleanup. Audits never
|
||
include bytes/tokens. Upload expiry may clean partials; download allows valid Range after success.
|
||
|
||
## Callback isolation
|
||
|
||
Speech and service state is MainActor-isolated, external callbacks are not. Create explicitly Sendable callbacks in nonisolated factories;
|
||
actor-inherited closures can trap before dispatch. Recheck recording tokens after authorization. iOS realtime audio taps use nonisolated
|
||
factories/Sendable buffer handlers. Timers/workers enter explicit short MainActor methods; remote I/O remains async/detached.
|
||
|
||
## Agent runtime and security
|
||
|
||
### Providers
|
||
|
||
- Empty credential reference means anonymous; nonempty missing/empty/unreadable secret is an error. `modelSecret(for:)` applies consistently
|
||
to discovery/probes/Chat/Guardian/title/compact/generator/Memory. Anonymous requests neither read secrets nor inherit unrelated errors;
|
||
providers omit empty auth headers. Vertex requires its own service account/project/location.
|
||
- Name/URL hints are not prefilled input. Application materializes blanks at save: Automatic uses full OpenAI Responses URL; explicit
|
||
protocols use their base. Nonempty input survives type changes; legacy empty runtime rows never infer public endpoints. Desktop uses
|
||
buffers, mobile normalizes after conflict merging; viewing/editing never contacts providers.
|
||
- Vertex is an explicit generateContent option, independent of retained ordinary URL/key; leaving type disables without deletion, never
|
||
fallback on missing credentials. Models are manual. AgentKit owns addresses/OAuth. Bound JSON import validates Google token URLs;
|
||
plaintext stays transient/in `AISecretStore`. Two-reference edits compare originals and roll back if metadata fails; delete removes both.
|
||
V19's separate backing freezes older providers; sync preserves omitted old-client fields, honors explicit clears/deletes, never cloud-syncs
|
||
private keys or reconstructs V12-discarded data.
|
||
- User-selected HTTP/HTTPS/self-hosted endpoints require ATS arbitrary loads; HTTPS still uses normal trust. No local-network exception may
|
||
override it. Local-network descriptions are localized; App Review notes explain user-owned HTTP servers.
|
||
|
||
### Execution and authorization
|
||
|
||
- AgentKit owns loop/gate/providers/MCP/host-neutral tools; Termind owns SSH/SFTP/pools/Vault/persistence/policy/UI. Built-ins precede additive
|
||
`TermindAgentTools`, cannot be shadowed. `AgentToolServices`/`AgentSSHInteraction` keep auth out of durable events. `ChatSlashCommandRegistry`
|
||
owns command semantics; composers inject actions. Termind prompt/plan policy stays cache-stable, presentation raw values stay persisted.
|
||
- `AgentRuntime` actor and context pipeline modify requests, not stored history. Schema validation precedes preflight/cards/hooks/authorization/
|
||
execution; event channel serializes durable/UI events. Tool-incapable models get none; unknown purposes default to chat. Planning/acting
|
||
have no count ceiling; finish/stop/hooks/errors terminate. Guardian is bounded and fails closed.
|
||
- Compact/title/generator buffer streaming (never force `stream: false`), require complete validated results/terminal SSE marker, cap raw
|
||
responses at 16 MiB and discard partials on cancellation/malformed/premature EOF. Ordinary turns retry transient transport/service failures
|
||
five times at 1/2/4/8/16 seconds using the same logical request, discarding partial UI. Tools dispatch only after complete responses, never
|
||
replay on retry. Final failures are error rows, not assistant text. Caller-URL fetch/scratch_fetch are Ask; enabled search is pre-approved.
|
||
- Guardian is isolated/tool-free `.reviewing`, with per-conversation serialized in-memory full-to-delta review. Only direct user text/local
|
||
question answers authorize. Stable policy/schema/cache prefixes; native JSON schema where supported, strict local validation otherwise.
|
||
Unavailable/timeout/malformed/recursive/inconsistent review fails closed before execution. Explicit model wins; automatic selection prefers
|
||
codex-auto-review, then version-ranked supported families, then Chat (see resolver/tests for exact ranking).
|
||
- Immutable hidden `AgentTurnContextSnapshot` captures facts/skills/plan/surface availability at submission/steer/edit. Replay uses original
|
||
baseline/deltas; absent legacy context is unknown, not current UI. Leave session/mode clears; retry retains, fork copies, compact rebases.
|
||
V16 adds independent replay backing, removed on deletion/truncation while older messages remain frozen.
|
||
- Project tool output once before `toolFinished`: bounded preview and scratch reference, retaining crash/replay metadata and paging offsets.
|
||
Later completion never shortens earlier results; `sftp_read`/`scratch_read` report complete lines/truthful offsets.
|
||
- Terminal suggestions use latest delivered user snapshot, not focus. Surface availability is partial and does not restrict independent SSH/
|
||
SFTP. Standalone Assistant can discover Hosts without sidebar attachment; ask only for ambiguity. App auth handles SSH prerequisites;
|
||
`request_user_secret` is for command secrets. Suggestions create user-operated cards, never execute. Deterministic provider schemas/cache
|
||
keys; cached tokens never double-count usage.
|
||
- Concurrent batches require every call parallel; preflight may tighten (`ssh_exec` proven-safe only, destructive MCP sequential). Answers stay
|
||
model-ordered, including refusals/cancellation. Questions/approvals queue singly; hooks cannot remove structural authorization. Command
|
||
classification unwraps bounded known syntax; unknown/complex/unsupported input fails closed as state-changing.
|
||
- Tool policy precedes permission mode: **Deny** always refuses; **Approve** needs no further approval; **Ask** goes to user or Guardian in
|
||
Approve for me. Full access pre-approves Ask but never bypasses trust/secrets/schema/path/fetch/sandbox/capability boundaries. New MCP tools
|
||
default Approve only when read-only and not destructive; rediscovery preserves explicit policy in both planning/acting.
|
||
|
||
### Skills, memory and durable data
|
||
|
||
- Skills are global app-managed packages/journal. Immutable application Creator/Installer merge only into runtime/catalog/reads, never Settings/
|
||
persistence/sync. Reserved names cannot be shadowed; collisions remain visible/paused. New libraries are empty; old starters remain ordinary
|
||
user records with identity/content/resources/deletions preserved.
|
||
- Acting-only Ask `skill_install` pins public GitHub directories to a commit, bounds/verifies bytes and rejects redirects/symlinks/submodules/
|
||
incomplete packages. Reviewed memory staging binds source/files/name/invocation/library revision; commit package+intent atomically, no scripts/
|
||
overwrite, available next run. Reads are bounded/paged; only SKILL.md instructs, references/frontmatter grant no permission. Mutations are
|
||
revision-bound and running snapshots fixed. V18 backing preserves supporting files from legacy payloads; `load_skill` is replay-only.
|
||
- Hermes Memory is device-local/default-off. AgentKit owns tools/screening/learning/revocable snapshots; Application lifecycle/UI; Persistence
|
||
alone GRDB. Separate database stores memory (2,200 Unicode scalars), user (1,375), frozen conversation snapshots/checkpoints/FTS5 history;
|
||
SwiftData remains conversation source. Atomic tool writes/base-state-checked reviews; forks reference snapshots, planning searches only,
|
||
skills retain approval, memory/credentials never sync.
|
||
- Disable revokes snapshots/cancels work but preserves storage. User-only Destroy persists disablement/unfinished marker, revokes leases, joins
|
||
work, closes/removes owned DB directory; failure stays disabled/retries launch, stale writes rejected by generation. Other records/exports
|
||
survive. Disabled startup never opens/creates DB; explicit re-enable can backfill and relearn from old chats.
|
||
- Tools never receive interactive PTYs: exec uses per-command Channels and files use exclusive leases from the Agent SFTP pool.
|
||
Secret handles are single-use/run-tool-Host-purpose-bound;
|
||
plaintext never reaches model/review/events/logs/SwiftData. Original arguments are projected only for pending local approval, never retained.
|
||
Fetch requires public HTTP(S), DNS revalidation per redirect/restricted-origin reauthorization and sensitive-header redaction. MCP metadata
|
||
only tightens safety.
|
||
- Per-conversation bounded `AgentScratchWorkspace` proves containment, rejects escape/`..`/symlinks and serializes path mutations. Pull/push
|
||
bytes stay outside transcripts; previews are bounded, push approval binds size/digest/redacted head and rejects changed content. Persist after
|
||
redaction/truncation. Launch marks unfinished runs interrupted, never replays; restore completed results and explicit unknown-outcome repairs,
|
||
never assumes unanswered calls were undispatched.
|
||
|
||
## Assistant presentation
|
||
|
||
- Presentation owns summaries/status/previews/time/Markdown/disclosures; hosts inject Remote Infrastructure presenters without importing transport
|
||
registry into Presentation. Protocol/persisted schemas remain unchanged. ListViewKit stays app-owned/allowlisted for large dynamic lists;
|
||
bounded navigation/archive uses SwiftUI List. Native row/cache/edit/viewport lifetimes stay platform-owned.
|
||
- Mobile presentation revisions reconfigure only changed rows/heights, retaining hosting content. Measurement does not start timers; only active
|
||
displayed content does. Deadlines never restart on reuse; completion defaults wait until the entire row exits unless reader chose disclosure.
|
||
Desktop width changes retain lists/parsed Markdown and never follow tail. Full expansion is window-owned; Copy uses full sanitized projection.
|
||
- Approval callbacks idempotently stage the same card before response; queued events cannot erase denial irrespective of arrival order. Historical
|
||
repair recognizes local presenter identity, never MCP/name-only matches. Bounded previews/scrollable arguments keep actions accessible without
|
||
reading live skill/memory stores or changing authorization/results/persistence.
|
||
|
||
## Commerce, review invitations and metrics
|
||
|
||
- `ProAccessController` is app-wide. StoreKit metadata/verified transactions alone determine offers/access; permanent wins, trial/subscribed/grace
|
||
grant access, metadata failures do not remove it. Foreground/minutely expiration checks. Presentation derives values, native paywalls navigate.
|
||
Workspace composition/broadcast/batch open/multi-host snippets/local templates are free; sync/other Pro features use current entitlement.
|
||
Product IDs, localized copy and release checks: [APP_STORE_COMMERCE.md](APP_STORE_COMMERCE.md).
|
||
- App-wide/device-local `AppReviewController` records attempts before StoreKit, never claims display. Automatic criteria: seven days, three active
|
||
dates, five foreground Workspace minutes, three quiet seconds entering Vault/Settings; input/focus/navigation/sleep/modal cancels. Once/version,
|
||
120-day cooldown including manual review. Tests/screenshots disable live requests; native observation remains app-owned.
|
||
- `ServerMetricsModel` shares one serial sampler/saved Host across windows/menu/mobile. Publish rows before secrets/network; detached workers do
|
||
short MainActor steps, recheck cancellation after credential reads, clear secrets on pause/removal/quit. Active monitor owns interaction.
|
||
Screenshot fixtures inject frozen values into production WindowGroup, never real discovery/samplers/config.
|
||
- Bound procfs/optional hardened nvidia-smi XML; partial failures preserve base data, invalid deltas unknown. Rates use remote uptime, process
|
||
identity PID+start, RSS remote page size; never collect arguments/environment. Five-minute samples memory-only; order/pause/device choices persist.
|
||
Process/filesystem detail sampling only while needed. Prefer physical network interfaces unless explicitly selected.
|
||
- Read-only/parallel/planning-safe `get_metrics` returns existing snapshots only: validated Host IDs, timestamps/staleness, null unknown/nonfinite,
|
||
max 20 processes. It cannot start sampling/details/auth. Monitoring does not depend on AgentKit.
|
||
- Metrics sync excludes samples/secrets/prompts/UI preferences. Each monitor owns Journey/MFA across jumps; cards signal but never navigate. Leaving
|
||
detail retains challenge; cancel pauses/returns that window to list. Retirement cancels queued responders, generation/request IDs isolate cleanup;
|
||
temporary trust clears on pause/removal/config change. Free cap atomically three cards including paused, never delete on downgrade; Pro unlimited.
|
||
- Desktop status item/popover receives model explicitly; last window close retains app/monitors, Quit cancels. Sleep/inactive login independently
|
||
suspend; resume only awake/active with Data Protection Keychain available. Native lifecycle stays in `TFM/Platform`.
|
||
|
||
## Settings replication
|
||
|
||
- Master iCloud and Vault/Metrics/Settings selections currently default **on** (`SettingsKey`); upgrades preserve explicit choices. Pro gates separate
|
||
`TermindVaultV1`, `TermindMetricsV1`, compatibility-named `TermindAIConfigurationV1` coordinators. Each owns a `CloudSyncLifecycle`: serialized
|
||
generations, consent, fetch-before-send, retry/reset/status. Zones/subscriptions/accounts/journals/engines/merge remain independent. Persistence
|
||
alone owns journal models. Authorize secret upload only after successful current-engine fetch; pause/retirement revokes. Stale work cannot publish
|
||
or clear replacements; injected transport enables deterministic tests.
|
||
- Frozen V13 Settings; V14 terminal, V15 Metrics journal/account/quarantine, V21 SFTP. Commit data+intent atomically, then mirror defaults; remote
|
||
apply refreshes without echo. Separate records cover providers/models/MCP/skills/roles/search/terminal/custom text/SFTP limits. First SFTP row
|
||
imports prior local values once, missing 15/5; future preparation uses persisted truth. `v1/sftp/default` uses `SFTPPreferencesItem` in same zone/
|
||
V1 envelope so old clients ignore it before decode. Missing strategy decodes as Smart; new/missing records default to 30/10 while
|
||
existing saved limits remain unchanged. Invalid limits/type mismatches quarantine. Running Agent snapshots remain fixed.
|
||
- Live role records sync explicit choices, not resolved fallbacks: nil generator = Same as Chat, nil Guardian = Automatic. Read legacy
|
||
`securityReview`, migrate to canonical `guardian` without reusing system fields. Conversations/approval mode/last-used model/listener config/
|
||
unrelated defaults stay local. Listener token is this-device-only Data Protection Keychain.
|
||
- `AISecretStore` migration needs verified account/cloud copy/durable local marker; reads never cache, deletion markers prevent resurrection.
|
||
Custom MCP headers/terminal text are ordinary synced settings, explicit credentials never CloudKit. Tombstones/unresolved references survive
|
||
partial fetch; cloud beats baseline. Duplicate skill names/MCP namespaces retain all data but only lowest UUID executes. Metrics hides records
|
||
awaiting their Vault Host. Signed-device/deployment checks: [SETTINGS_SYNC_RELEASE.md](SETTINGS_SYNC_RELEASE.md).
|
||
|
||
## Mobile boundaries
|
||
|
||
### Adaptive/native UI
|
||
|
||
- One sidebar-adaptable TabView per window, five stable stacks: Vault/Metrics/Workspaces/Assistant/Settings. `MobileFeatureRegistry` injects one
|
||
ordered ID/factory set and rejects duplicates. Adaptation never replaces the tree or loses routes/search/scroll/sheets/drafts/connections/runs.
|
||
Metrics is independent; Vault shortcuts select it. Layout follows actual placement/container width, never device idiom.
|
||
- Toolbar policy derives once from tab placement/system vertical edge. Peer switchers are distinct from app tabs; search remains native/minimized
|
||
without focus claims on pose/sidebar change. Save/duplicate guards/dirty dismissal/errors and stable sheets survive layout changes.
|
||
- With iOS 27.1 SDK/runtime, read division/occlusion ReservedRegions after safe-area/keyboard layout, including margins/RTL. Terminal uses largest
|
||
unobstructed rectangle (top/leading ties), resizes existing Ghostty PTY and rejects invalid sizes. Assistant keeps transcript and interaction
|
||
surfaces mounted across regions; Metrics retains panel sizes/table/sort/scroll, using fold regions or selected page. Pose never changes ownership.
|
||
- Application owns Host/Snippet operation state/sections; Presentation labels/history/drag type; apps native selection/menus/clipboard. Metrics enters
|
||
via `MobileMetricsServices`, both apps' host/auth/background commands via Remote Infrastructure `AgentHostServices`.
|
||
|
||
### Workspaces and activity
|
||
|
||
- One SSH session/SFTP Channel per mobile Workspace, no Welcome/pane composition even with Pro; Assistant independent. Peer switch retains connection,
|
||
Recent opens on demand, cross-open retains live paths. Explicit close never retries; failures stay in Journey. Background expiration closes
|
||
transports but retains intent; foreground resumes existing Workspaces.
|
||
- `mobile.workspaces.v1` persists ordered local descriptors only (ID/Host/kind/title/duplicate index), no commands/secrets/paths/content/transfers.
|
||
Cold launch after Hosts loads Recent only, no sessions/history/network. MainActor open-by-ID reuses or registers/connects once across scenes;
|
||
failure needs explicit retry. Save retains unopened entries/order. Read legacy arrays/v1, assign missing lowest-free indices and write envelope;
|
||
only successful loads permit save. Corrupt/unknown bytes remain/write-blocked. Prune missing Hosts; close/exit/delete removes live+Recent entries;
|
||
Close All preserves connection history. Titles reserve indices without renumbering. Host-delete outbox cancels related runtime/local history.
|
||
- One serialized Live Activity for current Workspaces only. Secret/command-free validated links select ID/list without restoring unrelated sessions;
|
||
cold launch ends leftover activities but links may open saved entries. Explicit start intent waits for foreground; last current close clears it
|
||
even with Recent. Foreground alone never recreates dismissed activity. Snapshot/queue is separate from observed stores and cannot keep SSH/Agent
|
||
alive. Stale links explain unavailable; restoration cannot distinguish termination from force-quit.
|
||
|
||
### Scene interactions
|
||
|
||
- App-wide runtime/conversation store prevents duplicate runs; each scene owns navigation/commands/library selection/New Chat draft. Focused values
|
||
route keyboards; per-window delegates consume quick actions once. Switching neither reconnects nor claims terminal focus; Find owns F3/Shift-F3
|
||
and Escape only while active, otherwise Escape passes to Ghostty.
|
||
- Settings editors use explicit drafts; ephemeral probes cancel on config/editor changes and cannot revive discarded data. Secrets remain transient
|
||
until atomic save/rollback. Mobile never starts local MCP. Archive persists before navigation/read-only until restored. Injected speech appends
|
||
without send and cancels on interruption/conversation change/background. Finite background expiration cancels runs/drains events/closes Agent
|
||
transports, never replays or auto-approves on return.
|
||
- Window drop interaction covers visible Workspaces: terminal routes files to uploads, text/links retain Ghostty behavior. Leaving restores original
|
||
interaction/targets. File-provider data is copied inside callbacks into staging retained by queued transfers after navigation.
|