iop/agent-contract/inner/agent-runtime.md

141 lines
16 KiB
Markdown

# Agent Runtime Contract
## 계약 메타
- id: `iop.agent-runtime`
- boundary: `inner`
- status: active
- 원본 경로:
- `packages/go/agentruntime/types.go`
- `packages/go/agentruntime/failure.go`
- `packages/go/agentruntime/emitter.go`
- `packages/go/agentruntime/session.go`
- `packages/go/agentruntime/status.go`
- `packages/go/agentruntime/registry.go`
- `packages/go/agentconfig/`
- `packages/go/agentprovider/cli/`
- `packages/go/agentprovider/catalog/`
- `packages/go/agentguard/`
- `packages/go/agenttask/`
- `configs/iop-agent.providers.yaml`
- `apps/node/internal/node/runtime_bridge.go`
## 읽는 조건
- Node와 독립 host가 공통 provider run/stream/resume/cancel/status 계약을 소비할 때
- `Provider`, `ExecutionSpec`, `RuntimeEvent`, `SessionMode`, `Failure`, `Registry`를 변경할 때
- CLI provider process, logical session, emitter, terminal, status/quota 파서를 변경할 때
- agent provider catalog YAML, provider/model/profile ID, discovery/readiness와 profile factory를 변경할 때
- unattended AgentTask의 canonical workspace grant, task isolation descriptor, admission permit과 provider invocation gate를 변경할 때
- `AgentTaskManager`, manual start/auto-resume, explicit dependency, isolated dispatch, official review와 serial integration orchestration을 변경할 때
- Node의 protobuf 요청/이벤트와 공통 runtime 사이 변환을 변경할 때
## 범위와 비범위
이 계약은 Node와 독립 agent host가 공유하는 host-neutral provider 실행 및 Agent Task orchestration 경계다. 공통 package는 provider lifecycle, 실행 요청, stream event, logical session, cancel, status/quota projection, typed failure와 registry lifecycle을 소유한다. agent 전용 catalog는 외부 CLI provider/model/profile의 공식 ID와 비밀정보 없는 실행·probe 선언, readiness와 공통 provider factory를 소유한다. `agentguard`는 unattended AgentTask provider 호출 직전의 canonical workspace와 capability admission을 소유한다. `agenttask.Manager`는 durable manual start intent부터 dependency-ready dispatch, submission/review, follow-up과 ordinal integration까지의 상태 전이를 단일 구현으로 소유한다.
Edge-Node protobuf field와 ordering 원문은 `iop.edge-node-runtime-wire`가 소유한다. 기존 Edge resource provider pool과 `models[]``iop.edge-config-runtime-refresh`가 소유하며 agent catalog와 이름이 비슷해도 schema와 의미를 섞지 않는다. 실제 workspace overlay 생성·change-set apply/rollback backend와 standalone `iop-agent` process lifecycle은 이 계약의 비범위다. `AgentTaskManager`는 이 backend들의 strict port와 호출 순서만 소유한다. Admission은 이미 생성된 isolation descriptor를 검증할 뿐 overlay/worktree/clone을 만들지 않는다.
## 최소 호출과 이벤트 형태
- host는 `Provider.Capabilities(ctx)`로 target과 concurrency capability를 읽고 `Provider.Execute(ctx, ExecutionSpec, EventSink)`로 실행한다.
- `ExecutionSpec``run_id`, `adapter`, `target`, `session_id`, `session_mode`, background, workspace, policy, input, timeout, metadata를 운반한다.
- `SessionModeCreateIfMissing`은 새 logical session 생성을 허용하고 `SessionModeRequireExisting`은 기존 session이 없으면 실패해야 한다.
- provider는 start, delta/reasoning_delta, complete/error/cancelled `RuntimeEvent`를 순서대로 보낸다. complete/error/cancelled 중 하나만 terminal이며 terminal 이후 event는 host에 노출하지 않는다.
- run cancel은 실행 context 취소와 `ErrRunCancelled`로 수렴한다. logical session 종료는 optional `SessionTerminator` 경계로 분리한다.
- 조회/제어는 실행 stream과 섞지 않고 optional `CommandHandler``CommandRequest`/`CommandResponse`로 처리한다. usage status는 `AgentUsageStatus`로 정규화한다.
## AgentTaskManager 명령과 durable 상태
- 공통 concrete 구현은 `packages/go/agenttask.Manager` 하나다. host는 `AgentTaskManager``StartProject`, `Reconcile`, `StopProject` lifecycle만 호출하고 Node나 독립 CLI에 state machine을 복제하지 않는다.
- `StartProject``command_id`, project/workspace/Milestone identity와 workflow/config/grant revision을 atomic CAS state에 manual `StartIntent`로 기록한다. 같은 command와 같은 immutable 입력은 idempotent이고, 같은 command를 다른 입력으로 재사용하면 오류다.
- `Reconcile``WorkflowAdapter.RegisteredProjects`와 project별 snapshot을 관측하되 `StartIntent`가 없는 ready Milestone을 실행하지 않는다. 수동 시작된 project만 진행하며 시작 기록이 있는 interrupted state는 `auto_resume_interrupted` 생략 시 `true`, 명시 `false`이면 stopped로 유지한다.
- durable identity는 project, workspace, Milestone, work unit, attempt, artifact, change set, workflow/config/grant/isolation revision과 dispatch/integration ordinal을 분리한다. corrupt 또는 drift한 identity를 빈 상태나 현재 설정으로 재선택하지 않고 typed task/project blocker로 남긴다.
- `StateStore`는 revision compare-and-swap을 제공해야 한다. manager는 project invocation lease와 workspace integration lease를 durable state에 claim하고 live 다른 owner가 있으면 중복 호출하지 않는다.
- work state는 `observed → ready → preparing → dispatching → submitted → reviewing → pending_integration → integrating → completed`를 기준으로 하며, `blocked`, `stopped`, `terminal_deferred`를 명시 terminal branch로 쓴다. 정의되지 않은 전이는 거부한다.
- `Event`와 모든 external port idempotency key는 length-prefixed injective canonical tuple로 구성하여 raw delimiter 충돌을 방지하고, command/workflow revision/change-set ID·revision/integration attempt 등의 logical discriminator를 보존하여 replay 시 동일 `event_id`로 수렴해야 한다. sink는 같은 `event_id` replay를 idempotent하게 처리해야 한다.
## Dependency, isolated dispatch와 review/integration
- readiness gate는 workflow snapshot의 `ExplicitPredecessors`만 사용한다. task 번호, directory 순서, write-set 비중첩·중첩·unknown은 dependency를 만들지 않는다. predecessor reference가 없거나 둘 이상이면 각각 typed missing/ambiguous blocker다.
- `Selector`는 immutable config revision의 provider/model/profile과 capacity를 반환한다. `Scheduler`는 provider/profile capacity와 work-attempt ticket을 결합하며 cancel/release가 capacity를 정확히 반환하도록 한다.
- 실행 전에 `IsolationBackend.Prepare`가 task별 `overlay | worktree | clone` descriptor와 exact grant/profile revision을 반환해야 한다. backend 미설정, identity mismatch, admission 차단은 provider invocation 0회이며 canonical workspace direct-write fallback은 없다.
- manager는 `agentguard.Admit`의 opaque Permit을 invocation 직전에 `agentguard.Invoke`로 재검증하고 그 canonical task view만 `ProviderInvoker`에 전달한다.
- provider submission이 complete이고 project/work/attempt/artifact identity가 일치한 뒤에만 `Reviewer`를 호출한다. PASS는 exact artifact의 immutable change set을 integration queue에 넣고 WARN/FAIL rework는 같은 dispatch ordinal의 새 attempt로 진행하며 USER_REVIEW는 해당 task만 terminal-deferred로 둔다.
- integration은 최초 dispatch ordinal 순서로 한 번에 하나씩 `Integrator`를 호출한다. 모든 external port call은 stable idempotency key를 받아 crash 후 replay가 같은 결과로 수렴해야 한다. conflict, unmanaged drift, validation/apply 오류는 partial completion 없이 retained change set과 blocker를 반환하며 뒤 independent ordinal은 계속 진행한다.
- project-local workflow, admission, invocation, review와 integration blocker는 다른 project나 independent sibling 진행을 중단하지 않는다.
## Workspace guardrail admission
- `WorkspaceGrant`는 project/workspace identity, canonical base root, immutable grant revision과 worktree가 사용할 수 있는 exact external Git metadata root allowance를 가진다.
- `IsolationDescriptor`는 immutable isolation/base revision, `overlay | worktree | clone` mode, canonical base/task/working root, task 내부 writable roots와 실제 writable-root confinement 여부를 가진다. task root와 canonical base가 같으면 admission을 거부한다.
- `ProviderProfile`은 provider/model/profile identity와 immutable revision, `unattended`, `approval_bypass`, `writable_root_confinement` capability를 가진다. 세 capability 중 하나라도 없으면 provider process를 호출하지 않는다.
- canonicalization은 absolute·clean·existing directory, symlink resolution, component-aware containment와 task root 및 effective working repository의 실제 `.git`/`gitdir`/`commondir`를 확인한다. task root 밖 Git metadata는 grant에 exact root로 등록된 경우만 허용한다.
- 성공한 admission은 process-local opaque `Permit`에 grant/isolation/profile revision, pinned base revision, canonical roots와 filesystem identity를 봉인한다. invocation 직전에 현재 입력과 filesystem identity를 다시 검증하며 stale, forged, replacement identity는 provider invocation 0회로 차단한다.
- unattended AgentTask caller는 `catalog.NewAdmittedProfileProvider`가 반환하는 facade의 `Admit`/`Execute`만 사용한다. facade는 caller가 제공한 raw `ExecutionSpec.Workspace`를 사용하지 않고 Permit의 canonical working directory로 덮어쓴다.
- `AdmissionResult``permitted | blocked`, typed `Blocker`, raw path를 포함하지 않는 actionable `Notification`을 반환한다. 차단은 task/project-local result이며 다른 project provider를 stop하지 않는다. interactive approval fallback은 없다.
- 기존 Node Edge-wire provider와 명시적인 authenticated smoke가 쓰는 `ProfileProvider.Execute`는 기존 실행 호환 경계다. AgentTask unattended 호출에서 이 compatibility 경로를 admission 우회로 사용하지 않는다.
## Agent provider catalog와 readiness
- `configs/iop-agent.providers.yaml``version`, `providers[]`, `models[]`, `profiles[]`의 비밀정보 없는 repo-owned 선언이다. 각 배열의 `id`는 배열 안에서 유일한 stable ID이며 profile은 정확히 하나의 provider와 그 provider가 소유한 model을 참조한다.
- provider는 CLI `command`, bounded version/authentication probe, optional model target probe와 지원 capability를 선언한다. model probe를 생략하면 검증된 static model target 선언이 기준이며, probe를 선언하면 출력의 exact line과 target을 비교한다.
- profile은 common CLI runtime args/resume args/mode/output format과 capability를 선언한다. `{{model}}`은 factory가 provider-native model target으로 치환하고 catalog 원본은 변경하지 않는다. `writable_root_confinement`는 task isolation owner와 결합해 provider process의 writable root를 제한할 수 있는 profile만 선언한다.
- loader는 YAML unknown field, multiple document, duplicate ID, dangling/cross-provider reference, invalid capability/mode/regex/timeout과 secret-like environment key를 거부하고 provider/model/profile을 ID 순서로 정규화한다.
- discovery는 PATH binary lookup, bounded version/authentication/model probe를 수행하고 공식 provider/model/profile ID와 함께 `ready`, `missing_binary`, `unauthenticated`, `unsupported_model`, `probe_error` 중 하나를 반환한다.
- 실행 불가 readiness는 각각 `ErrBinaryMissing`, `ErrAuthenticationRequired`, `ErrModelUnsupported`, `ErrProbeFailed``errors.Is` 가능한 `ReadinessError`를 반환한다. provider raw output, credential/token/header와 account identity는 redaction 후 bounded diagnostic에만 남긴다.
- profile factory는 동일 ID의 `ready` 결과만 받아 하나의 공통 CLI provider를 생성한다. runtime target은 profile ID이며 run/resume/cancel/status event·result metadata에 `provider_id`, `model_id`, `profile_id`를 보존한다.
- status는 predecessor 공통 CLI status API를 호출해 구조화 usage/quota를 얻고 discovery snapshot의 ID, readiness와 version을 `AgentUsageStatus.Metadata`에 병합한다. provider가 별도 status surface를 제공하지 못하면 직전에 검증한 readiness snapshot을 `status_probe=readiness_fallback`으로 명시해 반환하며 ready로 새로 추정하지 않는다.
## Typed failure codec
- `Failure`은 stable `FailureCode`, 사용자/운영 진단 `message`, `retryable`, 비민감 metadata를 가진다.
- durable boundary는 `EncodeFailure`/`DecodeFailure`의 versioned JSON envelope를 사용한다.
- 알 수 없는 미래 code는 실패를 버리지 않고 `unknown`으로 정규화하며 원래 code를 metadata에 보존한다.
- `ErrRunCancelled``context.Canceled``cancelled`, `context.DeadlineExceeded`는 retryable `deadline_exceeded`다.
- provider별 raw output, credential, token과 private endpoint를 failure metadata에 넣지 않는다.
- readiness error는 실행 `Failure` codec과 별도 preflight 타입이다. readiness를 실행 실패처럼 codec에 강제로 넣지 않는다.
## Node bridge 호환 규칙
- Node만 protobuf를 import하고 `runtime_bridge.go`에서 `RunRequest`를 공통 `RunRequest`로, 공통 `RuntimeEvent`를 기존 `RunEvent`로 변환한다.
- `RunEvent.type`, delta/message/error, usage, metadata, timestamp, session/background/node identity의 기존 wire 의미를 유지한다.
- typed failure가 있어도 기존 Node wire `error`에는 사람 읽기 가능한 message를 유지한다. protobuf 확장 없이 codec payload를 기존 field에 강제로 넣지 않는다.
- config refresh registry swap, in-flight snapshot, admission ticket release 뒤 terminal flush ordering은 공통 package 이동으로 바뀌지 않는다.
## 금지 사항
- `packages/go/agentruntime``packages/go/agentprovider`에서 `apps/*/internal` 또는 protobuf package를 import하지 않는다.
- Node와 독립 host에 CLI process/session/emitter/status/failure 구현을 복사하지 않는다.
- unattended AgentTask에서 raw `ProfileProvider.Execute`를 직접 호출하거나 invalid/stale Permit을 interactive fallback으로 우회하지 않는다.
- canonical base, task root 밖 writable root, grant에 없는 worktree Git metadata root를 Permit에 포함하지 않는다.
- agent provider catalog를 기존 Edge provider-pool `NodeProviderConf`/`ModelCatalogEntry` schema와 합치거나 서로의 ID 의미로 해석하지 않는다.
- tracked catalog에 raw token, credential, authorization header, password 또는 secret-bearing environment 값을 넣지 않는다.
- discovery timeout/cancel을 ready로 간주하거나 unknown provider/model/profile을 fallback target으로 선택하지 않는다.
- readiness ID와 factory profile ID가 다르거나 ready가 아닌 profile로 provider를 생성하지 않는다.
- provider-specific session/conversation id를 공통 execution identity로 승격하지 않는다.
- terminal event를 둘 이상 내보내거나 terminal 뒤 delta를 노출하지 않는다.
- cancel과 terminate-session을 같은 lifecycle action으로 취급하지 않는다.
- 기존 Edge-Node wire를 공통 runtime 타입과 같게 만들기 위해 proto 의미를 변경하지 않는다.
- manual `StartIntent`가 없는 ready project를 daemon start나 filesystem scan만으로 dispatch하지 않는다.
- explicit predecessor 외 번호, 경로, write-set overlap/unknown에서 암묵 dependency를 만들지 않는다.
- `IsolationBackend`, `ProviderInvoker`, `Reviewer`, `Integrator`가 없거나 실패했을 때 canonical workspace 직접 실행, review 생략, blind integration으로 fallback하지 않는다.
- artifact/change-set/revision identity mismatch를 성공으로 정규화하거나 새 identity로 조용히 재발급하지 않는다.
- worker 완료 순서로 integration ordinal을 바꾸거나 terminal-deferred task 하나로 뒤 independent queue를 멈추지 않는다.
## 변경 시 확인할 코드/테스트
- `packages/go/agentruntime/*_test.go`
- `packages/go/agentconfig/*_test.go`
- `packages/go/agentprovider/catalog/*_test.go`
- `packages/go/agentguard/*_test.go`
- `packages/go/agenttask/*_test.go`
- `packages/go/agentprovider/cli/*_test.go`
- `packages/go/agentprovider/cli/status/*_test.go`
- `apps/node/internal/node/*_test.go`
- `apps/node/internal/adapters/config_set_test.go`
- `apps/node/internal/router/router_test.go`
- `apps/node/internal/bootstrap/module_test.go`
- `cmd/iop-provider-smoke/main.go`
- `configs/iop-agent.providers.yaml`
- `agent-contract/inner/edge-node-runtime-wire.md`