iop/agent-contract/inner/iop-agent-cli-runtime.md
toki 3c48879c44 feat(agent-runtime): 독립 Agent CLI 런타임 기반을 구현한다
독립 호스트에서 안전한 작업 실행과 복구를 제공하기 위해 런타임 설정, 정책, 상태 저장소, 워크스페이스 격리 및 AgentTask 오케스트레이션을 확장한다.
2026-07-29 13:12:21 +09:00

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, and packages/go/agenttask/dispatch.go.
  • implemented S09 source: packages/go/agentstate/store.go, packages/go/agenttask/ports.go, packages/go/agenttask/intent.go, and packages/go/agenttask/reconcile.go.
  • implemented S18 source: packages/go/agentworkspace/snapshot.go, packages/go/agentworkspace/overlay.go, and packages/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-agent process 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, or LocalControlError, 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. Start returns opaque process/session locators, the manager persists them before Wait, and restart reconciliation delegates those locators to RecoveryInspector. 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_exhausted blocker 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_applicable semantics, policy ordering, used-target exclusion, and the final DecideContinuation result remain owned by iop.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.
  • Prepare fails 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 and chmod, utime, chown, and setxattr mutations to fail without changing metadata. macOS uses a verified sandbox-exec child policy. Other platforms are unsupported.
  • ConfinementProof.Start accepts 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

  • RepoGlobalRuntimeConfig is 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.
  • UserLocalRuntimeConfig is 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 RuntimeSnapshot records 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 effective ProjectRegistration carries the applicable runtime revision, and each effective SelectionPolicy carries a derived policy revision.
  • RuntimeConfigWatcher keeps 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.
  • LocalControlEnvelope is the outer schema for every frame. It contains protocol_version, kind, message_id, correlation_id, optional event_sequence, optional operation, and a typed payload. kind is exactly request, response, event, or error.
  • LocalControlRequest carries a request envelope, operation arguments, and an optional replay cursor. Every mutating request also carries a stable, caller-generated command_id.
  • LocalControlResponse carries the correlated operation result, current state revision or snapshot marker when applicable, and the accepted command_id for a mutation.
  • LocalControlEvent carries an ordered event_sequence, event type, subject identity, state revision, and a payload that is sufficient to update a current snapshot.
  • LocalControlError carries 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_denied before 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 that command_id with different operation or arguments returns command_id_conflict and 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_sequence within 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_unavailable with snapshot_required recovery 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.
  • LocalControlError uses typed codes at minimum: malformed_frame, unsupported_version, unsupported_operation, invalid_state, permission_denied, command_id_conflict, replay_unavailable, and internal.
  • 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

  • ClientProcessSpec identifies 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, and crashed. 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.detail request is translated by iop-agent into the corresponding Flutter client.start or client.focus command.
  • 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 agenttask or agentguard source-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_id validation, 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.md before 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 agentstate checksum/atomic-CAS suite and the agenttask restart, duplicate-owner, cancel, corruption, partial-completion, and failure-budget matrices under the race detector.
  • For S07 changes, run the status snapshot integrity matrix, agentpolicy continuation matrix, agenttask multi-failure history and malformed-evidence matrix, and the shared agentpolicy/agenttask race suites.
  • For workspace isolation changes, verify packages/go/agentworkspace/*_test.go together with the shared agentguard and agenttask suites.
  • Verify standalone contract changes with index ownership searches, S11/S15 anchor searches, the relevant future host tests when they exist, and git diff --check.