독립 호스트에서 안전한 작업 실행과 복구를 제공하기 위해 런타임 설정, 정책, 상태 저장소, 워크스페이스 격리 및 AgentTask 오케스트레이션을 확장한다.
355 lines
11 KiB
Go
355 lines
11 KiB
Go
package agenttask
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"os/exec"
|
|
"time"
|
|
|
|
"iop/packages/go/agentconfig"
|
|
"iop/packages/go/agentguard"
|
|
"iop/packages/go/agentpolicy"
|
|
)
|
|
|
|
var ErrRevisionConflict = errors.New("agenttask state revision conflict")
|
|
var ErrDeviceLeaseHeld = errors.New("agenttask device singleton lease is held by another owner")
|
|
var ErrLeaseLost = errors.New("agenttask lease ownership lost during operation")
|
|
|
|
// AgentTaskManager is the host-neutral lifecycle contract. StartProject records
|
|
// a durable manual intent; only Reconcile may advance workflow state.
|
|
type AgentTaskManager interface {
|
|
StartProject(context.Context, StartRequest) error
|
|
Reconcile(context.Context) error
|
|
StopProject(context.Context, ProjectID) error
|
|
}
|
|
|
|
type Clock interface {
|
|
Now() time.Time
|
|
}
|
|
|
|
type StateStore interface {
|
|
Load(context.Context) (ManagerState, StateRevision, error)
|
|
CompareAndSwap(
|
|
context.Context,
|
|
StateRevision,
|
|
ManagerState,
|
|
) (StateRevision, error)
|
|
}
|
|
|
|
// WorkflowAdapter normalizes project-owned task artifacts. Listing projects is
|
|
// separate from loading snapshots so one corrupt project cannot stop siblings.
|
|
type WorkflowAdapter interface {
|
|
RegisteredProjects(context.Context) ([]ProjectID, error)
|
|
Snapshot(context.Context, ProjectID) (ProjectWorkflowSnapshot, error)
|
|
}
|
|
|
|
// ArtifactIdentity identifies one project-owned active plan/review artifact
|
|
// without making the common manager parse project filesystem conventions.
|
|
type ArtifactIdentity struct {
|
|
ProjectID ProjectID
|
|
WorkspaceID WorkspaceID
|
|
WorkUnitID WorkUnitID
|
|
AttemptID AttemptID
|
|
ArtifactID ArtifactID
|
|
}
|
|
|
|
type ArtifactCompleteness string
|
|
|
|
const (
|
|
ArtifactComplete ArtifactCompleteness = "complete"
|
|
ArtifactPlaceholder ArtifactCompleteness = "placeholder"
|
|
)
|
|
|
|
// ArtifactEvidence is the project adapter's normalized observation of the
|
|
// active artifact pair. The adapter owns filesystem parsing; the manager owns
|
|
// matching the observation against the durable submission identity.
|
|
type ArtifactEvidence struct {
|
|
Active bool
|
|
Identity ArtifactIdentity
|
|
Completeness ArtifactCompleteness
|
|
RepairIntent *EvidenceRepairIntent
|
|
}
|
|
|
|
type WorkflowEvidenceStatus string
|
|
|
|
const (
|
|
WorkflowEvidenceMatched WorkflowEvidenceStatus = "matched"
|
|
WorkflowEvidenceNotActive WorkflowEvidenceStatus = "not_active"
|
|
WorkflowEvidencePlaceholder WorkflowEvidenceStatus = "placeholder"
|
|
WorkflowEvidenceIdentityMismatch WorkflowEvidenceStatus = "identity_mismatch"
|
|
WorkflowEvidenceInvalid WorkflowEvidenceStatus = "invalid"
|
|
)
|
|
|
|
type WorkflowEvidenceRequest struct {
|
|
Project ProjectRecord
|
|
Work WorkRecord
|
|
Submission Submission
|
|
}
|
|
|
|
// EvidenceRepairIntent is valid only for a placeholder from Pi. It carries
|
|
// the exact persisted session locator and dispatch ordinal so repair cannot
|
|
// attach to a newer attempt or a different native conversation.
|
|
type EvidenceRepairIntent struct {
|
|
Identity ArtifactIdentity
|
|
NativeLocator LocatorRecord
|
|
DispatchOrdinal DispatchOrdinal
|
|
}
|
|
|
|
type WorkflowEvidenceRepairRequest struct {
|
|
Project ProjectRecord
|
|
Work WorkRecord
|
|
Submission Submission
|
|
Intent EvidenceRepairIntent
|
|
}
|
|
|
|
// WorkflowEvidence is a strict project workflow port. Observe must be
|
|
// side-effect free; Repair may change only Pi worker-owned fields in the
|
|
// native context identified by EvidenceRepairIntent.
|
|
type WorkflowEvidence interface {
|
|
Observe(context.Context, WorkflowEvidenceRequest) (ArtifactEvidence, error)
|
|
Repair(context.Context, WorkflowEvidenceRepairRequest) error
|
|
}
|
|
|
|
type SelectionRequest struct {
|
|
Project ProjectRecord
|
|
Work WorkRecord
|
|
}
|
|
|
|
type Selector interface {
|
|
Select(context.Context, SelectionRequest) (ExecutionTarget, error)
|
|
}
|
|
|
|
// PolicySelector evaluates the effective project policy identified by
|
|
// SelectionContext.ProjectID against an immutable runtime config snapshot,
|
|
// returning a durable RouteDecision. The agentpolicy.Evaluator implements this
|
|
// interface.
|
|
type PolicySelector interface {
|
|
SelectPolicy(
|
|
context.Context,
|
|
agentconfig.RuntimeSnapshot,
|
|
agentpolicy.SelectionContext,
|
|
) (agentpolicy.RouteDecision, error)
|
|
}
|
|
|
|
// FailureContinuationPolicyRequest identifies the immutable project/work
|
|
// policy source for one failed attempt. The source does not receive or return a
|
|
// final continuation decision.
|
|
type FailureContinuationPolicyRequest struct {
|
|
Project ProjectRecord
|
|
Work WorkRecord
|
|
CurrentTarget ExecutionTarget
|
|
}
|
|
|
|
// FailureContinuationCandidate binds an exact host execution target to the
|
|
// ordered eligibility and quota evidence supplied by immutable policy.
|
|
type FailureContinuationCandidate struct {
|
|
Target ExecutionTarget
|
|
Eligible bool
|
|
Quota agentpolicy.QuotaObservation
|
|
}
|
|
|
|
// FailureContinuationPolicy contains only declared policy inputs. Manager
|
|
// combines these values with its current target, durable attempted-target
|
|
// history, normalized observation, and authoritative failure budget before it
|
|
// invokes agentpolicy.DecideContinuation.
|
|
type FailureContinuationPolicy struct {
|
|
Policy agentpolicy.FailurePolicy
|
|
Candidates []FailureContinuationCandidate
|
|
}
|
|
|
|
// FailureContinuationPolicySource is an optional extension of Selector. It
|
|
// cannot authorize a fabricated action or target because it returns only
|
|
// immutable policy and ordered candidate inputs.
|
|
type FailureContinuationPolicySource interface {
|
|
Selector
|
|
ContinuationPolicy(
|
|
context.Context,
|
|
FailureContinuationPolicyRequest,
|
|
) (FailureContinuationPolicy, error)
|
|
}
|
|
|
|
// FailureObservedInvocation exposes an already-normalized, safe observation
|
|
// for a failed provider invocation. It contains no provider output or mutable
|
|
// diagnostic maps and is the only observation Manager persists.
|
|
type FailureObservedInvocation interface {
|
|
ProviderInvocation
|
|
FailureObservation() agentpolicy.AttemptObservation
|
|
}
|
|
|
|
type IsolationRequest struct {
|
|
Project ProjectRecord
|
|
Work WorkRecord
|
|
Target ExecutionTarget
|
|
IdempotencyKey string
|
|
}
|
|
|
|
type PreparedIsolation struct {
|
|
Grant *agentguard.WorkspaceGrant
|
|
Descriptor *agentguard.IsolationDescriptor
|
|
Profile agentguard.ProviderProfile
|
|
Confinement InvocationConfinement
|
|
}
|
|
|
|
type IsolationBackend interface {
|
|
Prepare(context.Context, IsolationRequest) (PreparedIsolation, error)
|
|
}
|
|
|
|
// ConfinementBinding is the immutable identity that an executable filesystem
|
|
// confinement proof must cover. RuntimeRoot and SnapshotRoot identify the
|
|
// protected device-local tree; only WritableRoots may be mutated by the child.
|
|
type ConfinementBinding struct {
|
|
Revision string
|
|
IsolationID string
|
|
IsolationRevision string
|
|
PinnedBaseRevision string
|
|
ConfigRevision string
|
|
GrantRevision string
|
|
ProfileRevision string
|
|
BaseRoot string
|
|
RuntimeRoot string
|
|
SnapshotRoot string
|
|
TaskRoot string
|
|
WorkingDir string
|
|
WritableRoots []string
|
|
}
|
|
|
|
// ConfinementCommand contains only non-I/O launch data. InvocationConfinement
|
|
// owns child stdio creation so callers cannot pass a pre-opened descriptor
|
|
// through the executable confinement boundary.
|
|
type ConfinementCommand struct {
|
|
Name string
|
|
Args []string
|
|
Env []string
|
|
}
|
|
|
|
// StartedConfinement is the exact child and parent-side pipe set created by a
|
|
// validated executable confinement proof. BindStarted assumes ownership after
|
|
// a successful bind. Before that transfer, Abort closes every pipe endpoint,
|
|
// terminates the child, and reaps it.
|
|
type StartedConfinement interface {
|
|
Child() *exec.Cmd
|
|
Stdin() io.WriteCloser
|
|
Stdout() io.ReadCloser
|
|
Stderr() io.ReadCloser
|
|
Abort() error
|
|
}
|
|
|
|
// InvocationConfinement is an executable, identity-bound child launcher.
|
|
// Isolation backends issue proofs; provider invokers consume the exact proof
|
|
// carried by DispatchRequest rather than asserting a capability boolean.
|
|
type InvocationConfinement interface {
|
|
Revision() string
|
|
Binding() ConfinementBinding
|
|
Validate(ConfinementBinding) error
|
|
Start(context.Context, ConfinementCommand) (StartedConfinement, error)
|
|
}
|
|
|
|
type DispatchRequest struct {
|
|
Project ProjectRecord
|
|
Work WorkRecord
|
|
Target ExecutionTarget
|
|
AdmissionRequest agentguard.AdmissionRequest
|
|
Permit *agentguard.Permit
|
|
Workspace agentguard.CanonicalWorkspace
|
|
Confinement InvocationConfinement
|
|
IdempotencyKey string
|
|
}
|
|
|
|
type ProviderInvocation interface {
|
|
Locators() []LocatorRecord
|
|
Wait(context.Context) (Submission, error)
|
|
Cancel(context.Context) error
|
|
}
|
|
|
|
// ProviderLaunch is a side-effect-free provider launch plan. The manager owns
|
|
// the only process start: it passes Command to the validated confinement proof
|
|
// and then binds the exact returned child and proof-owned pipes to a durable
|
|
// invocation handle.
|
|
type ProviderLaunch interface {
|
|
Command() ConfinementCommand
|
|
BindStarted(StartedConfinement) (ProviderInvocation, error)
|
|
}
|
|
|
|
type ProviderInvoker interface {
|
|
// Prepare must not start a process, session, or other provider side effect.
|
|
// Manager invokes the returned command through InvocationConfinement.Start.
|
|
Prepare(context.Context, DispatchRequest) (ProviderLaunch, error)
|
|
}
|
|
|
|
type RecoveryExecutionState string
|
|
|
|
const (
|
|
RecoveryExecutionAbsent RecoveryExecutionState = "absent"
|
|
RecoveryExecutionLive RecoveryExecutionState = "live"
|
|
RecoveryExecutionSubmitted RecoveryExecutionState = "submitted"
|
|
RecoveryExecutionExited RecoveryExecutionState = "exited"
|
|
RecoveryExecutionAmbiguous RecoveryExecutionState = "ambiguous"
|
|
)
|
|
|
|
type RecoveryCompletionState string
|
|
|
|
const (
|
|
RecoveryCompletionUnknown RecoveryCompletionState = "unknown"
|
|
RecoveryCompletionComplete RecoveryCompletionState = "complete"
|
|
RecoveryCompletionPartial RecoveryCompletionState = "partial"
|
|
RecoveryCompletionAmbiguous RecoveryCompletionState = "ambiguous"
|
|
)
|
|
|
|
type RecoveryRequest struct {
|
|
Project ProjectRecord
|
|
Work WorkRecord
|
|
Locators map[LocatorKind]LocatorRecord
|
|
}
|
|
|
|
// RecoveryObservation is identity-bound so stale process, session, overlay, or
|
|
// archive evidence cannot be rebound to a current attempt.
|
|
type RecoveryObservation struct {
|
|
ProjectID ProjectID
|
|
WorkspaceID WorkspaceID
|
|
WorkUnitID WorkUnitID
|
|
AttemptID AttemptID
|
|
Execution RecoveryExecutionState
|
|
Completion RecoveryCompletionState
|
|
Submission *Submission
|
|
}
|
|
|
|
type RecoveryInspector interface {
|
|
Inspect(context.Context, RecoveryRequest) (RecoveryObservation, error)
|
|
}
|
|
|
|
type ReviewRequest struct {
|
|
Project ProjectRecord
|
|
Work WorkRecord
|
|
Submission Submission
|
|
IdempotencyKey string
|
|
}
|
|
|
|
type Reviewer interface {
|
|
Review(context.Context, ReviewRequest) (ReviewResult, error)
|
|
}
|
|
|
|
type IntegrationRequest struct {
|
|
Project ProjectRecord
|
|
Work WorkRecord
|
|
ChangeSet ChangeSetIdentity
|
|
Ordinal DispatchOrdinal
|
|
Attempt IntegrationAttempt
|
|
IdempotencyKey string
|
|
}
|
|
|
|
type Integrator interface {
|
|
Integrate(context.Context, IntegrationRequest) (IntegrationResult, error)
|
|
}
|
|
|
|
type EventSink interface {
|
|
Emit(context.Context, Event) error
|
|
}
|
|
|
|
type nopEventSink struct{}
|
|
|
|
func (nopEventSink) Emit(context.Context, Event) error { return nil }
|
|
|
|
type systemClock struct{}
|
|
|
|
func (systemClock) Now() time.Time { return time.Now().UTC() }
|