독립 호스트에서 안전한 작업 실행과 복구를 제공하기 위해 런타임 설정, 정책, 상태 저장소, 워크스페이스 격리 및 AgentTask 오케스트레이션을 확장한다.
24 KiB
IOP Agent CLI Runtime Contract
Contract metadata
- id:
iop.agent-cli-runtime - boundary:
inner - status: active
- shared contract dependency:
iop.agent-runtime - implemented S05 source:
packages/go/agentconfig/runtime_config.go,packages/go/agentconfig/watcher.go. - implemented S07 source:
packages/go/agentprovider/cli/status/quota.go,packages/go/agentpolicy/quota.go,packages/go/agentpolicy/failure_policy.go,packages/go/agenttask/ports.go, andpackages/go/agenttask/dispatch.go. - implemented S09 source:
packages/go/agentstate/store.go,packages/go/agenttask/ports.go,packages/go/agenttask/intent.go, andpackages/go/agenttask/reconcile.go. - implemented S18 source:
packages/go/agentworkspace/snapshot.go,packages/go/agentworkspace/overlay.go, andpackages/go/agentworkspace/confinement*.go. - remaining implementation source status: standalone host source paths for S06, S08, S11, S15, and S19 are added by their implementation tasks.
- design input:
agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md
Read when
- changing standalone
iop-agentprocess lifecycle, repo-global/user-local configuration precedence, device singleton ownership, or host-local checkpoint and recovery records; - checking standalone S07 quota/failure evidence ownership or its delegated shared-runtime continuation boundary;
- changing the host extension points for workspace isolation or change-set persistence while preserving the shared runtime ports owned by
iop.agent-runtime; - changing
LocalControlEnvelope,LocalControlRequest,LocalControlResponse,LocalControlEvent, orLocalControlError, including peer authorization, command idempotency, replay, or failure behavior; - changing Flutter or Unity client-process start, stop, focus, reconnect, crash recovery, or Unity-to-Flutter detail routing.
Scope and non-scope
This contract defines the standalone host boundary for one device-local iop-agent daemon. It owns host configuration composition, daemon and client-process ownership, the local control protocol, and host-local durable records that reference shared-runtime identities.
iop.agent-runtime is the sole authoritative contract for common provider execution, agenttask.Manager lifecycle and state transitions, agentguard admission and Permit validation, review, integration ports, and their source paths. This contract may require the host to call those shared boundaries, but it does not restate their rules or claim their implementation sources.
The Edge-Node wire and Edge configuration contracts remain owned by iop.edge-node-runtime-wire and iop.edge-config-runtime-refresh. This contract is transport-neutral at the schema level: it requires a local proto-socket boundary but does not select a concrete generated proto, socket library, or platform-specific credential API.
Evidence map
| Scenario | Required evidence | Completion evidence expectation |
|---|---|---|
| S05 | Repo-global/user-local precedence, invalid configuration, immutable repo input, and revision-change tests | config-registry evidence records both revisions and confirms the repo is not mutated. |
| S06 | Ordered selection persistence and tamper rejection tests | target-policy evidence records the selected rule, reason, and retained route history. |
| S07 | Snapshot tamper/reason/not-applicable tests, sealed safe observation projection, strict durable projection round trips, common-policy manager integration, unused-target history, and failure-budget tests | quota-failure evidence records content-bound immutable snapshots, canonical corrupt blockers, exact attempt/target transitions, sealed disk round trips, and no reused candidate. Shared semantics are authoritative in iop.agent-runtime. |
| S08 | Provider-neutral workflow evidence and same-context repair tests | workflow-evidence records review invocation and locator evidence. |
| S09 | Device singleton, workspace lease, checkpoint, restart, and archive fault tests | state-recovery proves no duplicate owner and exact retained state. |
| S11 | Same-user local-control authorization, state/event delivery, replay-gap, and denied-peer tests | local-control proves no app-token fallback, denied cross-user dispatch, and snapshot recovery. |
| S15 | Flutter/Unity singleton-process, disconnect/crash/reconnect, and Unity detail-routing tests | client-process-manager proves daemon-owned lifecycle and Flutter start/focus routing. |
| S18 | packages/go/agentworkspace/overlay_test.go and confinement_test.go cover dirty/untracked/mode/symlink fingerprinting, identical concurrent bases, same-file and disjoint writes, a real confined child that can change content and metadata only in its view/temp/cache roots, protected chmod/utime/chown/setxattr denial, canonical/sibling/snapshot/overlay-record/shared-Git denial, exact root/config/grant replay rejection, idempotency, and failure retention. |
overlay-workspace proves the executable child boundary and retained records preserve one exact immutable base and isolated writable layers. |
| S19 | Change-set persistence, ordered integration, conflict, rollback, and retention tests | change-set-integration proves retained host records identify the exact immutable change set. |
Standalone host schemas and durable records
The following are contract-first records owned by the standalone host. They define host inputs, durable backend state, and host-facing projections; they do not define shared runtime lifecycle, admission, review, or integration algorithms.
| Schema | Host-owned contract |
|---|---|
RuntimeConfig |
A versioned composition of read-only repo-global defaults and user-local configuration. It records both immutable input revisions, applies user-local values after repo-global values, replaces ordered arrays rather than appending them, and owns local roots without writing device state or credentials to the repo. |
ProjectRegistration |
A user-local, revisioned registration of one project identity and canonical workspace reference, including the applicable configuration revision and host recovery metadata. It does not mutate the repo-global configuration or create a shared runtime lease by itself. |
SelectionPolicy |
The versioned policy input supplied to the shared selector: ordered rules, local overrides, and retained route-history references. The host preserves the resolved policy revision and route evidence, while iop.agent-runtime remains the owner of selection and failover algorithms. |
PreviewRequest |
An immutable request identifying the project, workspace, configuration and policy revisions to evaluate. Preview uses the same delegated decision boundary without dispatching a provider, creating an isolation layer, changing persisted state, or otherwise causing a side effect. |
WorkspaceSnapshot |
A versioned immutable base fingerprint that records and hashes the normalized canonical root, exact configuration and grant revisions, Git revision/index identity, tracked and untracked content, dirty state, file modes, and symlink identity. A snapshot may be reused only when this complete identity matches. |
OverlayWorkspace |
A durable isolation record for one task identity that records the same canonical/configuration/grant identity, exact base snapshot, writable layer, merged read view, task-local temporary/cache roots, isolated Git metadata reference, executable confinement revision, and retention/recovery state. An idempotent replay with a different root or revision is rejected without changing the retained record. |
ChangeSet |
A frozen, content-addressed host persistence record with the exact base fingerprint and change-set revision, additions/modifications/deletions, mode or symlink operations, write set, and validation evidence. It remains immutable after review acceptance and is retained for recovery and later integration attempts. |
IntegrationRecord |
A revisioned record of one exact change set and integration attempt: dispatch and attempt ordinals, expected and observed before fingerprints, predecessor references, apply/validation outcome, rollback or blocker evidence, after fingerprint, cleanup state, and retention identity. It records delegated integration results but does not decide integration order or outcome. |
ProjectLogRecord |
An append-only host presentation and recovery projection that connects project/work-unit and attempt identities with route/quota observations, process or session references, overlay/change-set/integration locators, failures, retries, review evidence, and completion state. |
IntegrationStatus |
A current host-facing recovery projection for a task/change-set and ordinal, including queued/integrating/integrated/blocked state, conflict or blocker reference, retained overlay reference, and available recovery action. It reports shared runtime results without owning integration decisions. |
- The host owns one device-local daemon identity and the client-process records associated with that daemon. A live owner prevents a second daemon from taking over until the prior owner is conclusively released or expired.
- The host-local state file uses a versioned JSON envelope containing a monotonically increasing CAS revision, the manager snapshot, and a SHA-256 checksum over the schema/revision/state tuple. Writes use a same-directory temporary file, file sync, atomic rename, directory sync, and an advisory lock shared by all store instances. A checksum failure, malformed envelope, or unsupported schema is returned without overwriting the original evidence.
- The manager claims the durable device singleton before reconciliation and retains it via an immutable fencing token (scope/owner/token/subject handle) for the daemon owner. A background supervisor renews device, project, workspace, and integration leases by CAS at a bounded fraction of
LeaseDuration. The guarded reconciliation context is cancelled the moment any renewal cannot prove its token still matches current state. Project and workspace invocation leases plus the workspace integration lease are acquired with the same CAS state; a foreign unexpired lease prevents execution, while an expired lease is eligible for an identity-checked takeover. Every external result is followed by an atomic fence check against all live tokens before entering durable state; on loss the guarded context is cancelled, the external call is cancelled, and only exact tokens are released—never overwriting a successor lease. - Provider invocation is checkpoint-first.
Startreturns opaque process/session locators, the manager persists them beforeWait, and restart reconciliation delegates those locators toRecoveryInspector. Proven-live work is retained, an exact recovered submission advances to review, and stale, exited-without-result, partial-completion, or ambiguous observations become typed blockers with zero provider invocation. - Process, session, overlay, change-set, and completion locators carry the exact project, workspace, work-unit, attempt, kind, and revision identity. Failure budgets are persisted per stage and become a non-retryable
failure_budget_exhaustedblocker at their configured limit. - S07 host records persist only the safe shared-runtime
AttemptObservationRecord, exact attempted target identity, and manager-owned failure budget. Valid/stale quota projections retain the shared runtime's private integrity seal over every policy-visible field; strict durable decoding rejects seal drift before state use. Quota normalization,not_applicablesemantics, policy ordering, used-target exclusion, and the finalDecideContinuationresult remain owned byiop.agent-runtime; the standalone host does not duplicate or override them. - Every host record carries an explicit schema version and preserves referenced configuration, shared-runtime, workspace, isolation, base, change-set, and integration revisions exactly. Retention and cleanup must leave enough identity to recover or report a retained blocker.
- Corrupt state, an unsupported schema version, or a mismatched referenced identity is a typed host failure or blocker. The host must not silently reset a record, rebind it to current inputs, fabricate a replacement identity, or treat it as a successful recovery.
- Host isolation and change-set implementations are extension points. Their preparation, admission, review, and integration semantics remain delegated to
iop.agent-runtime; the host preserves returned immutable identities when persisting or presenting state.
Workspace overlay and executable confinement
- The default overlay backend materializes an immutable snapshot tree and isolated Git metadata, confirms that the canonical fingerprint did not drift during capture, and installs one private task view plus task-local temp and cache roots. The canonical root and device-local runtime root must not overlap.
- The overlay record revision covers project/work/attempt identity, canonical/configuration/grant/profile/base identity, exact locators, and retention policy. A separate confinement revision covers that overlay revision, the platform policy revision, canonical root, protected runtime and snapshot roots, task root, view/temp/cache roots, and profile/configuration/grant revisions.
Preparefails closed before returning an admissible descriptor when the platform cannot install executable confinement. Linux admits only a probed unprivileged user/mount namespace with a recursively read-only filesystem and explicit writable task mounts; its probe requires protected content writes andchmod,utime,chown, andsetxattrmutations to fail without changing metadata. macOS uses a verifiedsandbox-execchild policy. Other platforms are unsupported.ConfinementProof.Startaccepts only executable name, arguments, and environment. It creates anonymous stdin/stdout/stderr pipes, installs the OS policy, starts the wrapped provider child, and returns the exact child plus parent-side pipe endpoints as one proof-owned started handle. Provider launch plans cannot supply files, readers, writers, raw descriptors, or any other inheritable I/O capability.- The child may mutate only its view, temp, and cache roots. Canonical files, sibling task layers, immutable snapshots, the overlay record, and shared Git metadata remain non-writable even when addressed by absolute path or when the host opened a writable descriptor before launch. The descriptor cannot enter the child because child I/O is created exclusively by the confinement owner.
- Provider binding receives only the exact started handle returned by the proof. Before a successful ownership transfer, an incomplete handle or bind failure closes every pipe endpoint, terminates the child, and reaps it. Provider authentication and command binaries may be read outside the task roots, but the child receives no writable exception for them; temporary and cache output must be routed into task-local roots.
Runtime configuration composition and revisions
RepoGlobalRuntimeConfigis the strict, versioned repository input. It may contain the secret-free provider catalog, runtime defaults, ordered selection policy, isolation modes, and retention limits. The registry reads this source with no repository write API and never opens it for writing.UserLocalRuntimeConfigis the strict, versioned device input. It contains device-local state, overlay, log, optional temporary/cache roots, scalar and map overrides, and project registrations with project-specific overrides. Credential values and arbitrary environment values are not fields in this schema and are rejected as unknown fields.- Each source must contain exactly one YAML document at the supported schema version. Unknown fields, malformed values, invalid catalog references, non-absolute required device/workspace paths, unsupported isolation modes, duplicate selection rule identities, and negative retention limits fail the load.
- Composition is deterministic: an explicitly present user-local scalar replaces the repo-global scalar, profile-alias maps merge by key with the local value winning, and ordered selection-rule and isolation-fallback arrays replace the complete preceding array instead of appending. The same rules apply again for each project override.
- A
RuntimeSnapshotrecords SHA-256 revisions of the exact repo-global and user-local inputs plus a derived runtime revision. Its merged value is private; config and project accessors return defensive deep copies. Each effectiveProjectRegistrationcarries the applicable runtime revision, and each effectiveSelectionPolicycarries a derived policy revision. RuntimeConfigWatcherkeeps the last valid snapshot when either input is invalid and publishes only a valid changed revision. An invocation that already captured revision A remains pinned to A; a later invocation obtains revision B after B has loaded and validated successfully.
Local control protocol version and envelope
- The daemon owns exactly one local proto-socket endpoint per device-local daemon identity. It publishes client-neutral state and accepts local control only through this boundary.
LocalControlEnvelopeis the outer schema for every frame. It containsprotocol_version,kind,message_id,correlation_id, optionalevent_sequence, optionaloperation, and a typed payload.kindis exactlyrequest,response,event, orerror.LocalControlRequestcarries a request envelope, operation arguments, and an optional replay cursor. Every mutating request also carries a stable, caller-generatedcommand_id.LocalControlResponsecarries the correlated operation result, current state revision or snapshot marker when applicable, and the acceptedcommand_idfor a mutation.LocalControlEventcarries an orderedevent_sequence, event type, subject identity, state revision, and a payload that is sufficient to update a current snapshot.LocalControlErrorcarries a stable error code, safe message, retryability, correlation identifier, and recovery metadata such as the current replay floor or snapshot marker.- Protocol versions are explicit. A peer must not assume that an unknown envelope field, version, operation, or event type is safe to ignore when doing so could alter command meaning.
Operations, authorization, and idempotency
| Operation class | Operations | Required behavior |
|---|---|---|
| Read | runtime.status, project.status, overlay.status, integration.status, blocker.list, process.status |
Return a coherent host snapshot or a typed absence/error response without mutation. |
| Project mutation | project.start, project.stop, project.resume |
Require command_id; delegate shared lifecycle actions to iop.agent-runtime; persist only host-owned command presentation and recovery state. |
| Client mutation | client.start, client.stop, client.focus, client.detail |
Require command_id; execute only through daemon-owned client-process records. client.detail routes a supported Unity detail request to Flutter start/focus through the daemon. |
- The daemon authorizes a peer from local socket ownership and peer credential evidence. It accepts only a peer with the same effective OS user as the daemon; all other peers receive
permission_deniedbefore command dispatch. - A client uses no app token for this boundary, and no app-token fallback may bypass peer credential or same OS user authorization.
- Repeating a mutation with the same
command_id, operation, and immutable arguments returns the original accepted result without a second mutation. Reusing thatcommand_idwith different operation or arguments returnscommand_id_conflictand performs no mutation. - A rejected frame, failed authorization, unsupported operation, invalid state, or idempotency conflict performs no mutation and does not create a substitute command record.
Replay, delivery, and failures
- Events are ordered by a monotonically increasing
event_sequencewithin one daemon identity. Clients may reconnect with a replay cursor and must tolerate duplicate retained events by deduplicating their sequence. - The daemon replays retained events after the requested cursor when the cursor is within retention. If the cursor predates the retention floor, belongs to another daemon identity, or cannot form a contiguous replay, it returns
replay_unavailablewithsnapshot_requiredrecovery metadata instead of silently omitting state changes. - A snapshot response establishes the current state revision and replay cursor from which later events may resume. The daemon may coalesce non-essential progress events, but it must not claim a replay that hides a state transition represented by the current snapshot.
LocalControlErroruses typed codes at minimum:malformed_frame,unsupported_version,unsupported_operation,invalid_state,permission_denied,command_id_conflict,replay_unavailable, andinternal.- Error payloads exclude credentials, tokens, raw private paths, and unbounded subprocess output. Internal failures are correlated and surfaced as safe diagnostics without changing command state unless the command had already been accepted and recorded.
Client-process lifecycle
ClientProcessSpecidentifies a Flutter or Unity executable, launch and restart policy, local socket endpoint, and the supported Unity-to-Flutter detail capability. The daemon validates and owns this specification from user-local configuration.- For each client kind, the daemon is the only process owner and tracks
stopped,starting,connected, andcrashed. A duplicate start converges through the command-id rule and an existing live process identity; it does not create a second subprocess. - Crash restart and login launch follow user-local policy. A disconnect preserves daemon ownership, records the process outcome, and allows a same-user client to reconnect and replay or request a snapshot.
- Unity never starts, stops, focuses, or directly communicates with Flutter. A supported Unity
client.detailrequest is translated byiop-agentinto the corresponding Flutterclient.startorclient.focuscommand. - Stopping or exiting a client never stops the daemon or transfers runtime, project, provider, scheduling, retry, or integration ownership to a client.
Prohibitions
- Do not duplicate
agenttaskoragentguardsource-path ownership, lifecycle rules, admission rules, Permit validation, review rules, or integration-port semantics in this contract. - Do not implement a direct client-to-client control path, an app-token authorization fallback, or a cross-user local control path.
- Do not dispatch a mutating operation before peer authorization and
command_idvalidation, or make rejected frames mutate host state. - Do not silently discard a replay gap, fabricate a contiguous event sequence, or treat a stale cursor as a current snapshot.
- Do not let a client own daemon lifecycle or shared-runtime execution decisions.
- Do not store device paths, checkpoint state, client process records, or credentials in repo-global configuration or project task artifacts.
Change checklist
- Read
agent-contract/inner/agent-runtime.mdbefore changing any shared runtime dependency; update that contract rather than this one when the common owner changes. - For local-control changes, update the operation matrix, authorization, idempotency, replay, failure, and client lifecycle rules together.
- For a concrete transport implementation, add its actual host source paths and focused tests in the implementing S11 or S15 task; do not backfill speculative paths here.
- For durable-state changes, run the
agentstatechecksum/atomic-CAS suite and theagenttaskrestart, duplicate-owner, cancel, corruption, partial-completion, and failure-budget matrices under the race detector. - For S07 changes, run the status snapshot integrity matrix,
agentpolicycontinuation matrix,agenttaskmulti-failure history and malformed-evidence matrix, and the sharedagentpolicy/agenttaskrace suites. - For workspace isolation changes, verify
packages/go/agentworkspace/*_test.gotogether with the sharedagentguardandagenttasksuites. - Verify standalone contract changes with index ownership searches, S11/S15 anchor searches, the relevant future host tests when they exist, and
git diff --check.