iop/packages/go/agenttask/dispatch.go

994 lines
31 KiB
Go

package agenttask
import (
"context"
"fmt"
"reflect"
"iop/packages/go/agentguard"
"iop/packages/go/agentpolicy"
)
func (m *Manager) runWork(
ctx context.Context,
projectID ProjectID,
workID WorkUnitID,
) error {
for {
project, work, err := m.loadWork(ctx, projectID, workID)
if err != nil {
return err
}
switch work.State {
case WorkStateReviewing:
if work.Submission == nil {
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerArtifactMismatch,
Message: "reviewing work has no durable submission identity",
})
return nil
}
rework, reviewErr := m.reviewSubmission(ctx, project, work, *work.Submission)
if reviewErr != nil || !rework {
return reviewErr
}
continue
case WorkStateReady:
default:
return nil
}
if err := m.changeWork(ctx, projectID, workID, func(work *WorkRecord) error {
return transitionWork(work, WorkStatePreparing)
}); err != nil {
return err
}
project, work, err = m.loadWork(ctx, projectID, workID)
if err != nil {
return err
}
target, err := m.selectTarget(ctx, project, work)
if err != nil {
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerSelectionFailed, Message: err.Error(), Retryable: true,
})
return nil
}
if err := validateTarget(project, target); err != nil {
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerInvalidIdentity, Message: err.Error(),
})
return nil
}
isolation, err := m.isolation.Prepare(ctx, IsolationRequest{
Project: project, Work: work, Target: target,
IdempotencyKey: dispatchKey(projectID, workID, work.Attempt) + "/isolation",
})
if err != nil {
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerIsolationFailed, Message: err.Error(), Retryable: true,
})
return nil
}
admissionRequest, isolationIdentity, err := validatePreparedIsolation(project, work, target, isolation)
if err != nil {
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerIsolationFailed, Message: err.Error(),
})
return nil
}
admission := agentguard.Admit(admissionRequest)
if !admission.Allowed() {
detail := "workspace admission denied"
if admission.Blocker != nil {
detail = string(admission.Blocker.Code) + ": " + admission.Blocker.Message
}
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerAdmissionFailed, Message: detail,
})
return nil
}
lease, err := m.scheduler.Acquire(ctx, DispatchCandidate{
ProjectID: projectID, WorkspaceID: project.WorkspaceID,
WorkUnitID: workID, AttemptID: work.AttemptID, Target: target,
})
if err != nil {
if ctx.Err() != nil {
_ = m.changeWork(context.WithoutCancel(ctx), projectID, workID, func(work *WorkRecord) error {
return transitionWork(work, WorkStateReady)
})
return ctx.Err()
}
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerProviderCapacity, Message: err.Error(), Retryable: true,
})
return nil
}
err = m.changeWork(ctx, projectID, workID, func(work *WorkRecord) error {
if err := transitionWork(work, WorkStateDispatching); err != nil {
return err
}
work.Target = &target
work.Isolation = &isolationIdentity
if work.Locators == nil {
work.Locators = make(map[LocatorKind]LocatorRecord)
}
work.Locators[LocatorOverlay] = locatorForIsolation(project, *work, isolationIdentity)
return nil
})
if err != nil {
lease.Release()
return err
}
project, work, err = m.loadWork(ctx, projectID, workID)
if err != nil {
lease.Release()
return err
}
var cmdID CommandID
var wfRev WorkflowRevision
if project.Intent != nil {
cmdID = project.Intent.CommandID
wfRev = project.Intent.WorkflowRevision
}
m.emit(ctx, Event{
Type: EventDispatchStarted, ProjectID: projectID, WorkspaceID: project.WorkspaceID,
WorkUnitID: workID, CommandID: cmdID, WorkflowRevision: wfRev, AttemptID: work.AttemptID, Ordinal: work.DispatchOrdinal,
State: work.State, ProviderID: target.ProviderID, ProfileID: target.ProfileID,
WriteSetKind: work.Unit.WriteSetKind, IsolationMode: work.Unit.IsolationMode,
})
var invocation ProviderInvocation
var launch ProviderLaunch
confinementBinding := isolation.Confinement.Binding()
dispatchRequest := DispatchRequest{
Project: project, Work: work, Target: target, AdmissionRequest: admissionRequest,
Permit: admission.Permit, Workspace: *admission.Workspace,
Confinement: isolation.Confinement,
IdempotencyKey: dispatchKey(projectID, workID, work.Attempt),
}
validation, invokeErr := agentguard.Invoke(
ctx,
admission.Permit,
admissionRequest,
func(invokeCtx context.Context, workspace agentguard.CanonicalWorkspace) error {
if workspace.ConfinementRevision != isolation.Confinement.Revision() {
return fmt.Errorf(
"agenttask: permit confinement revision does not match the executable proof",
)
}
if err := isolation.Confinement.Validate(confinementBinding); err != nil {
return fmt.Errorf(
"agenttask: executable confinement proof became invalid before invocation: %w",
err,
)
}
dispatchRequest.Workspace = workspace
prepared, err := m.invoker.Prepare(invokeCtx, dispatchRequest)
if err != nil {
return err
}
if prepared == nil {
return fmt.Errorf("agenttask: provider returned a nil launch plan")
}
launch = prepared
command := launch.Command()
started, err := isolation.Confinement.Start(invokeCtx, command)
if err != nil {
return fmt.Errorf("agenttask: start provider through executable confinement: %w", err)
}
if err := validateStartedConfinement(started); err != nil {
if started != nil {
_ = started.Abort()
}
return err
}
invocation, err = launch.BindStarted(started)
if err != nil {
_ = started.Abort()
return fmt.Errorf("agenttask: bind confined provider child: %w", err)
}
if invocation == nil {
_ = started.Abort()
return fmt.Errorf("agenttask: provider bound a nil invocation handle")
}
return nil
},
)
if !validation.Allowed() {
lease.Release()
detail := "admission permit became invalid before invocation"
if validation.Blocker != nil {
detail = string(validation.Blocker.Code) + ": " + validation.Blocker.Message
}
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerAdmissionFailed, Message: detail,
})
return nil
}
if invokeErr != nil {
lease.Release()
if ctx.Err() != nil {
return ctx.Err()
}
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerInvocationFailed, Message: "provider launch failed before a normalized invocation was available", Retryable: true,
})
return nil
}
if launch == nil || invocation == nil {
lease.Release()
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerInvocationFailed, Message: "provider launch did not bind an invocation handle",
})
return nil
}
invocationLocators := invocation.Locators()
if err := validateInvocationLocators(project, work, invocationLocators); err != nil {
_ = invocation.Cancel(context.WithoutCancel(ctx))
lease.Release()
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerStaleCheckpoint, Message: err.Error(),
})
return nil
}
if err := m.changeWork(ctx, projectID, workID, func(work *WorkRecord) error {
for _, locator := range invocationLocators {
work.Locators[locator.Kind] = locator
}
return nil
}); err != nil {
_ = invocation.Cancel(context.WithoutCancel(ctx))
lease.Release()
return err
}
submission, invokeErr := invocation.Wait(ctx)
lease.Release()
if invokeErr != nil {
if ctx.Err() != nil {
return ctx.Err()
}
handled, continueWork, continuationErr := m.continueAfterInvocationFailure(
ctx, project, work, target, invocation,
)
if continuationErr != nil {
return continuationErr
}
if handled {
if continueWork {
continue
}
return nil
}
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerInvocationFailed, Message: "provider invocation failed without a normalized failure observation", Retryable: true,
})
return nil
}
project, work, err = m.loadWork(ctx, projectID, workID)
if err != nil {
return err
}
if err := validateSubmission(project, work, submission); err != nil {
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerArtifactMismatch, Message: err.Error(),
})
return nil
}
if !submission.Ready {
m.blockWork(ctx, projectID, workID, WorkStateBlocked, Blocker{
Code: BlockerSubmissionIncomplete,
Message: "worker submission did not pass the provider-neutral completeness gate",
})
return nil
}
if blocker := m.gateSubmissionEvidence(ctx, project, work, submission); blocker != nil {
m.blockWork(ctx, projectID, workID, WorkStateBlocked, *blocker)
return nil
}
if err := m.changeWork(ctx, projectID, workID, func(work *WorkRecord) error {
if err := transitionWork(work, WorkStateSubmitted); err != nil {
return err
}
work.Submission = &submission
for _, locator := range submission.Locators {
work.Locators[locator.Kind] = locator
}
resetFailure(work, FailureStageDispatch)
work.ContinuationTarget = nil
return nil
}); err != nil {
return err
}
m.emit(ctx, Event{
Type: EventSubmissionAccepted, ProjectID: projectID, WorkspaceID: project.WorkspaceID,
WorkUnitID: workID, CommandID: cmdID, WorkflowRevision: wfRev, AttemptID: work.AttemptID, Ordinal: work.DispatchOrdinal,
Detail: string(submission.ArtifactID),
})
if err := m.changeWork(ctx, projectID, workID, func(work *WorkRecord) error {
return transitionWork(work, WorkStateReviewing)
}); err != nil {
return err
}
project, work, err = m.loadWork(ctx, projectID, workID)
if err != nil {
return err
}
rework, err := m.reviewSubmission(ctx, project, work, submission)
if err != nil || !rework {
return err
}
}
}
// selectTarget preserves a policy-authorized continuation target across a
// crash or pre-invocation retry. New work still goes through the ordinary
// selector; no continuation target is inferred from a previous failure.
func (m *Manager) selectTarget(
ctx context.Context,
project ProjectRecord,
work WorkRecord,
) (ExecutionTarget, error) {
if work.ContinuationTarget != nil {
return *work.ContinuationTarget, nil
}
return m.selector.Select(ctx, SelectionRequest{Project: project, Work: work})
}
func (m *Manager) continueAfterInvocationFailure(
ctx context.Context,
project ProjectRecord,
work WorkRecord,
current ExecutionTarget,
invocation ProviderInvocation,
) (handled bool, continueWork bool, err error) {
observed, ok := invocation.(FailureObservedInvocation)
if !ok {
return false, false, nil
}
observation := agentpolicy.SanitizeAttemptObservation(
observed.FailureObservation(),
m.clock.Now(),
)
if !observationMatchesTarget(observation, current) {
observation.Quota = agentpolicy.CorruptQuotaObservation()
}
source, ok := m.selector.(FailureContinuationPolicySource)
if !ok {
return m.persistContinuation(
ctx,
project,
work,
current,
observation,
agentpolicy.ContinuationBlock,
nil,
Blocker{
Code: BlockerFailurePolicyUnavailable,
Message: "no declared failure continuation policy is available",
},
)
}
policy, policyErr := source.ContinuationPolicy(ctx, FailureContinuationPolicyRequest{
Project: project,
Work: work,
CurrentTarget: current,
Observation: observation,
})
if policyErr != nil {
return m.persistContinuation(
ctx,
project,
work,
current,
observation,
agentpolicy.ContinuationBlock,
nil,
Blocker{
Code: BlockerFailurePolicyDenied,
Message: "failure continuation policy rejected the observation",
},
)
}
request, candidateTargets, buildErr := m.continuationRequest(
project,
work,
current,
observation,
policy,
)
if buildErr != nil {
return m.persistContinuation(
ctx,
project,
work,
current,
observation,
agentpolicy.ContinuationBlock,
nil,
Blocker{
Code: BlockerFailurePolicyDenied,
Message: "failure continuation policy inputs are invalid",
},
)
}
decision, decisionErr := agentpolicy.DecideContinuation(request)
if decisionErr != nil {
return m.persistContinuation(
ctx,
project,
work,
current,
observation,
agentpolicy.ContinuationBlock,
nil,
Blocker{
Code: BlockerFailurePolicyDenied,
Message: "common failure continuation policy rejected its inputs",
},
)
}
switch decision.Action {
case agentpolicy.ContinuationBlock:
if decision.Blocker == "" {
return m.persistContinuation(
ctx,
project,
work,
current,
observation,
agentpolicy.ContinuationBlock,
nil,
Blocker{
Code: BlockerFailurePolicyDenied,
Message: "failure continuation policy returned an invalid block decision",
},
)
}
return m.persistContinuation(
ctx,
project,
work,
current,
observation,
decision.Action,
nil,
blockerFromContinuation(decision.Blocker),
)
case agentpolicy.ContinuationRetry:
if !targetMatchesIdentity(current, decision.Target) {
return m.persistContinuation(
ctx,
project,
work,
current,
observation,
agentpolicy.ContinuationBlock,
nil,
Blocker{
Code: BlockerFailurePolicyDenied,
Message: "failure continuation policy returned an invalid next target",
},
)
}
return m.persistContinuation(
ctx,
project,
work,
current,
observation,
decision.Action,
&current,
Blocker{},
)
case agentpolicy.ContinuationFailover:
next, exists := candidateTargets[decision.Target]
if !exists || sameExecutionTarget(next, current) {
return m.persistContinuation(
ctx,
project,
work,
current,
observation,
agentpolicy.ContinuationBlock,
nil,
Blocker{
Code: BlockerFailurePolicyDenied,
Message: "common failure policy selected an unknown failover target",
},
)
}
return m.persistContinuation(
ctx,
project,
work,
current,
observation,
decision.Action,
&next,
Blocker{},
)
default:
return m.persistContinuation(
ctx,
project,
work,
current,
observation,
agentpolicy.ContinuationBlock,
nil,
Blocker{
Code: BlockerFailurePolicyDenied,
Message: "failure continuation policy returned an unsupported action",
},
)
}
}
func (m *Manager) continuationRequest(
project ProjectRecord,
work WorkRecord,
current ExecutionTarget,
observation agentpolicy.AttemptObservation,
source FailureContinuationPolicy,
) (agentpolicy.ContinuationRequest, map[agentpolicy.TargetIdentity]ExecutionTarget, error) {
currentIdentity := executionTargetIdentity(current)
used := make([]agentpolicy.TargetIdentity, 0, len(work.AttemptObservations))
seenUsed := make(map[agentpolicy.TargetIdentity]struct{}, len(work.AttemptObservations))
for _, record := range work.AttemptObservations {
identity := executionTargetIdentity(record.Target)
if _, duplicate := seenUsed[identity]; duplicate {
continue
}
seenUsed[identity] = struct{}{}
used = append(used, identity)
}
candidates := make([]agentpolicy.ContinuationCandidate, 0, len(source.Candidates))
candidateTargets := make(map[agentpolicy.TargetIdentity]ExecutionTarget, len(source.Candidates))
for _, candidate := range source.Candidates {
if err := validateTarget(project, candidate.Target); err != nil {
return agentpolicy.ContinuationRequest{}, nil, err
}
identity := executionTargetIdentity(candidate.Target)
if _, duplicate := candidateTargets[identity]; duplicate {
return agentpolicy.ContinuationRequest{}, nil, fmt.Errorf(
"agenttask: continuation policy repeats a candidate target",
)
}
quota := agentpolicy.SanitizeQuotaObservation(candidate.Quota)
if quota.Validity != agentpolicy.ObservationCorrupt &&
(quota.Adapter != candidate.Target.ProviderID ||
quota.Target != candidate.Target.ProfileID) {
quota = agentpolicy.CorruptQuotaObservation()
}
candidateTargets[identity] = candidate.Target
candidates = append(candidates, agentpolicy.ContinuationCandidate{
Target: identity,
Eligible: candidate.Eligible,
Quota: quota,
})
}
return agentpolicy.ContinuationRequest{
Policy: source.Policy,
Current: currentIdentity,
Candidates: candidates,
Used: used,
Budget: m.pendingFailureBudget(work),
Observation: observation,
}, candidateTargets, nil
}
func (m *Manager) persistContinuation(
ctx context.Context,
project ProjectRecord,
failedWork WorkRecord,
current ExecutionTarget,
observation agentpolicy.AttemptObservation,
action agentpolicy.ContinuationAction,
next *ExecutionTarget,
blocker Blocker,
) (handled bool, continueWork bool, err error) {
var outcomeBlocker Blocker
var nextAttempt AttemptID
continued := false
err = m.changeWork(ctx, project.ProjectID, failedWork.Unit.ID, func(work *WorkRecord) error {
if work.AttemptID != failedWork.AttemptID || work.State != WorkStateDispatching {
return fmt.Errorf("agenttask: failed work changed before continuation was persisted")
}
if err := appendAttemptObservation(work, current, observation); err != nil {
return err
}
if action == agentpolicy.ContinuationBlock {
outcomeBlocker = m.recordFailure(work, FailureStageDispatch, blocker)
if err := transitionWork(work, WorkStateBlocked); err != nil {
return err
}
work.Blocker = &outcomeBlocker
return nil
}
outcomeBlocker = m.recordFailure(work, FailureStageDispatch, Blocker{
Code: BlockerInvocationFailed,
Message: "provider execution failed; continuation was policy-authorized",
Retryable: true,
})
if outcomeBlocker.Code == BlockerFailureBudgetExhausted {
if err := transitionWork(work, WorkStateBlocked); err != nil {
return err
}
work.Blocker = &outcomeBlocker
return nil
}
if next == nil {
return fmt.Errorf("agenttask: continuation has no next target")
}
if err := transitionWork(work, WorkStateReady); err != nil {
return err
}
work.Attempt++
work.AttemptID = attemptID(work.Unit.ID, work.Attempt)
target := *next
work.Target = nil
work.ContinuationTarget = &target
work.Isolation = nil
work.Submission = nil
work.Review = nil
work.ChangeSet = nil
work.Integration = nil
work.Locators = make(map[LocatorKind]LocatorRecord)
work.Blocker = nil
nextAttempt = work.AttemptID
continued = true
return nil
})
if err != nil {
return true, false, err
}
if continued {
var commandID CommandID
var workflowRevision WorkflowRevision
if project.Intent != nil {
commandID = project.Intent.CommandID
workflowRevision = project.Intent.WorkflowRevision
}
m.emit(ctx, Event{
Type: EventFollowup, ProjectID: project.ProjectID, WorkspaceID: project.WorkspaceID,
WorkUnitID: failedWork.Unit.ID, CommandID: commandID, WorkflowRevision: workflowRevision,
AttemptID: nextAttempt, Ordinal: failedWork.DispatchOrdinal, Detail: string(action),
})
return true, true, nil
}
m.emit(ctx, Event{
Type: EventBlocked, ProjectID: project.ProjectID, WorkspaceID: project.WorkspaceID,
WorkUnitID: failedWork.Unit.ID, AttemptID: failedWork.AttemptID,
Ordinal: failedWork.DispatchOrdinal, State: WorkStateBlocked, Detail: string(outcomeBlocker.Code),
})
return true, false, nil
}
func (m *Manager) pendingFailureBudget(work WorkRecord) agentpolicy.FailureBudget {
budget := work.FailureBudgets[FailureStageDispatch]
limit := budget.Limit
if limit == 0 {
limit = m.config.MaxFailureAttempts
}
used := budget.Consecutive
if used < limit {
used++
}
return agentpolicy.FailureBudget{Used: used, Limit: limit}
}
func appendAttemptObservation(
work *WorkRecord,
target ExecutionTarget,
observation agentpolicy.AttemptObservation,
) error {
record := AttemptObservationRecord{
AttemptID: work.AttemptID,
Target: target,
Observation: observation.Clone(),
}
for _, existing := range work.AttemptObservations {
if existing.AttemptID != record.AttemptID {
continue
}
if reflect.DeepEqual(existing, record) {
return nil
}
return fmt.Errorf("agenttask: a different failure observation already exists for this attempt")
}
work.AttemptObservations = append(work.AttemptObservations, record)
return nil
}
func observationMatchesTarget(
observation agentpolicy.AttemptObservation,
target ExecutionTarget,
) bool {
if observation.Quota.Validity == agentpolicy.ObservationCorrupt {
return true
}
return observation.Quota.Adapter == target.ProviderID &&
observation.Quota.Target == target.ProfileID
}
func targetMatchesIdentity(
target ExecutionTarget,
identity agentpolicy.TargetIdentity,
) bool {
return target.ProviderID == identity.ProviderID &&
target.ModelID == identity.ModelID &&
target.ProfileID == identity.ProfileID &&
target.ProfileRevision == identity.ProfileRevision
}
func executionTargetIdentity(target ExecutionTarget) agentpolicy.TargetIdentity {
return agentpolicy.TargetIdentity{
ProviderID: target.ProviderID,
ModelID: target.ModelID,
ProfileID: target.ProfileID,
ProfileRevision: target.ProfileRevision,
}
}
func sameExecutionTarget(left, right ExecutionTarget) bool {
return left.ProviderID == right.ProviderID &&
left.ModelID == right.ModelID &&
left.ProfileID == right.ProfileID &&
left.ProfileRevision == right.ProfileRevision &&
left.ConfigRevision == right.ConfigRevision &&
left.Capacity == right.Capacity
}
func blockerFromContinuation(code agentpolicy.ContinuationBlockerCode) Blocker {
switch code {
case agentpolicy.ContinuationBlockerUnknownQuota:
return Blocker{Code: BlockerFailureObservationUnknown, Message: "quota observation is unknown"}
case agentpolicy.ContinuationBlockerStaleObservation:
return Blocker{Code: BlockerFailureObservationStale, Message: "quota observation is stale"}
case agentpolicy.ContinuationBlockerCorruptObservation:
return Blocker{Code: BlockerFailureObservationCorrupt, Message: "quota observation is corrupt"}
case agentpolicy.ContinuationBlockerUnknownFailure:
return Blocker{Code: BlockerFailureUnknown, Message: "runtime failure is unknown"}
case agentpolicy.ContinuationBlockerBudgetExhausted:
return Blocker{Code: BlockerFailureBudgetExhausted, Message: "failure budget is exhausted"}
case agentpolicy.ContinuationBlockerNoAlternate:
return Blocker{Code: BlockerNoEligibleFailover, Message: "no eligible unused failover target remains"}
default:
return Blocker{Code: BlockerFailurePolicyDenied, Message: "failure is not declared by policy"}
}
}
func validateStartedConfinement(started StartedConfinement) error {
if started == nil {
return fmt.Errorf("agenttask: executable confinement returned a nil started handle")
}
if started.Child() == nil {
return fmt.Errorf("agenttask: executable confinement returned a started handle without a child")
}
if started.Stdin() == nil || started.Stdout() == nil || started.Stderr() == nil {
return fmt.Errorf("agenttask: executable confinement returned incomplete child I/O")
}
return nil
}
func validateInvocationLocators(
project ProjectRecord,
work WorkRecord,
locators []LocatorRecord,
) error {
if len(locators) == 0 {
return fmt.Errorf("agenttask: provider invocation returned no durable process or session locator")
}
seen := make(map[LocatorKind]struct{}, len(locators))
for _, locator := range locators {
if locator.Kind != LocatorProcess && locator.Kind != LocatorSession {
return fmt.Errorf("agenttask: provider returned unsupported %q invocation locator", locator.Kind)
}
if _, duplicate := seen[locator.Kind]; duplicate {
return fmt.Errorf("agenttask: provider returned duplicate %q invocation locator", locator.Kind)
}
seen[locator.Kind] = struct{}{}
if err := validateLocator(project, work, locator); err != nil {
return err
}
if existing, ok := work.Locators[locator.Kind]; ok &&
!reflect.DeepEqual(existing, locator) {
return fmt.Errorf("agenttask: provider replaced the durable %q locator", locator.Kind)
}
}
return nil
}
func validateTarget(project ProjectRecord, target ExecutionTarget) error {
for field, value := range map[string]string{
"provider": target.ProviderID,
"model": target.ModelID,
"profile": target.ProfileID,
"profile_revision": target.ProfileRevision,
} {
if err := validateIdentity(field, value); err != nil {
return err
}
}
if project.Intent == nil || target.ConfigRevision != project.Intent.ConfigRevision {
return fmt.Errorf("agenttask: selected target config revision does not match manual start")
}
if target.Capacity <= 0 {
return fmt.Errorf("agenttask: selected target capacity must be positive")
}
return nil
}
func validatePreparedIsolation(
project ProjectRecord,
work WorkRecord,
target ExecutionTarget,
prepared PreparedIsolation,
) (agentguard.AdmissionRequest, IsolationIdentity, error) {
if prepared.Grant == nil || prepared.Descriptor == nil ||
prepared.Confinement == nil {
return agentguard.AdmissionRequest{}, IsolationIdentity{},
fmt.Errorf("agenttask: isolation backend returned incomplete strict ports")
}
if project.Intent == nil {
return agentguard.AdmissionRequest{}, IsolationIdentity{},
fmt.Errorf("agenttask: project has no manual start intent")
}
if prepared.Grant.ProjectID != string(project.ProjectID) ||
prepared.Grant.WorkspaceID != string(project.WorkspaceID) ||
prepared.Grant.Revision != string(project.Intent.GrantRevision) {
return agentguard.AdmissionRequest{}, IsolationIdentity{},
fmt.Errorf("agenttask: workspace grant identity or revision mismatch")
}
if prepared.Descriptor.Mode != work.Unit.IsolationMode {
return agentguard.AdmissionRequest{}, IsolationIdentity{},
fmt.Errorf("agenttask: isolation mode differs from the workflow request")
}
if prepared.Profile.ProviderID != target.ProviderID ||
prepared.Profile.ModelID != target.ModelID ||
prepared.Profile.ProfileID != target.ProfileID ||
prepared.Profile.Revision != target.ProfileRevision {
return agentguard.AdmissionRequest{}, IsolationIdentity{},
fmt.Errorf("agenttask: admitted provider profile differs from selected target")
}
expectedConfinement := ConfinementBinding{
Revision: prepared.Descriptor.ConfinementRevision,
IsolationID: prepared.Descriptor.ID,
IsolationRevision: prepared.Descriptor.Revision,
PinnedBaseRevision: prepared.Descriptor.PinnedBaseRevision,
ConfigRevision: string(project.Intent.ConfigRevision),
GrantRevision: prepared.Grant.Revision,
ProfileRevision: prepared.Profile.Revision,
BaseRoot: prepared.Descriptor.BaseRoot,
TaskRoot: prepared.Descriptor.TaskRoot,
WorkingDir: prepared.Descriptor.WorkingDir,
WritableRoots: append([]string(nil), prepared.Descriptor.WritableRoots...),
}
actualConfinement := prepared.Confinement.Binding()
expectedConfinement.RuntimeRoot = actualConfinement.RuntimeRoot
expectedConfinement.SnapshotRoot = actualConfinement.SnapshotRoot
if prepared.Confinement.Revision() != expectedConfinement.Revision {
return agentguard.AdmissionRequest{}, IsolationIdentity{},
fmt.Errorf("agenttask: confinement proof revision differs from the isolation descriptor")
}
if err := prepared.Confinement.Validate(expectedConfinement); err != nil {
return agentguard.AdmissionRequest{}, IsolationIdentity{},
fmt.Errorf("agenttask: invalid executable confinement proof: %w", err)
}
identity := IsolationIdentity{
ID: prepared.Descriptor.ID, Revision: prepared.Descriptor.Revision,
Mode: prepared.Descriptor.Mode, PinnedBaseRevision: prepared.Descriptor.PinnedBaseRevision,
TaskRoot: prepared.Descriptor.TaskRoot,
}
for field, value := range map[string]string{
"isolation": identity.ID,
"isolation_revision": identity.Revision,
"pinned_base_revision": identity.PinnedBaseRevision,
} {
if err := validateIdentity(field, value); err != nil {
return agentguard.AdmissionRequest{}, IsolationIdentity{}, err
}
}
return agentguard.AdmissionRequest{
Grant: prepared.Grant, Isolation: prepared.Descriptor, Profile: prepared.Profile,
}, identity, nil
}
func validateSubmission(project ProjectRecord, work WorkRecord, submission Submission) error {
if submission.ProjectID != project.ProjectID || submission.WorkUnitID != work.Unit.ID ||
submission.AttemptID != work.AttemptID {
return fmt.Errorf("agenttask: submission durable identity mismatch")
}
if err := validateIdentity("artifact", string(submission.ArtifactID)); err != nil {
return err
}
seen := make(map[LocatorKind]struct{}, len(submission.Locators))
for _, locator := range submission.Locators {
if locator.Kind != LocatorProcess && locator.Kind != LocatorSession {
return fmt.Errorf("agenttask: provider returned unsupported %q submission locator", locator.Kind)
}
if _, duplicate := seen[locator.Kind]; duplicate {
return fmt.Errorf("agenttask: provider returned duplicate %q submission locator", locator.Kind)
}
seen[locator.Kind] = struct{}{}
if err := validateLocator(project, work, locator); err != nil {
return err
}
if existing, ok := work.Locators[locator.Kind]; ok &&
!reflect.DeepEqual(existing, locator) {
return fmt.Errorf("agenttask: submission replaced the durable %q locator", locator.Kind)
}
}
return nil
}
func (m *Manager) loadWork(
ctx context.Context,
projectID ProjectID,
workID WorkUnitID,
) (ProjectRecord, WorkRecord, error) {
state, err := m.load(ctx)
if err != nil {
return ProjectRecord{}, WorkRecord{}, err
}
project, ok := state.Projects[projectID]
if !ok {
return ProjectRecord{}, WorkRecord{}, fmt.Errorf("agenttask: project %q disappeared", projectID)
}
work, ok := project.Works[workID]
if !ok {
return ProjectRecord{}, WorkRecord{}, fmt.Errorf("agenttask: work %q disappeared", workID)
}
return project, work, nil
}
func (m *Manager) changeWork(
ctx context.Context,
projectID ProjectID,
workID WorkUnitID,
change func(*WorkRecord) error,
) error {
return m.mutate(ctx, func(state *ManagerState) error {
project, ok := state.Projects[projectID]
if !ok {
return fmt.Errorf("agenttask: project %q disappeared", projectID)
}
work, ok := project.Works[workID]
if !ok {
return fmt.Errorf("agenttask: work %q disappeared", workID)
}
if err := change(&work); err != nil {
return err
}
work.UpdatedAt = m.clock.Now()
project.Works[workID] = work
project.UpdatedAt = m.clock.Now()
state.Projects[projectID] = project
return nil
})
}
func (m *Manager) blockWork(
ctx context.Context,
projectID ProjectID,
workID WorkUnitID,
state WorkState,
blocker Blocker,
) {
_ = m.changeWork(ctx, projectID, workID, func(work *WorkRecord) error {
stage := failureStageForState(work.State)
blocker = m.recordFailure(work, stage, blocker)
if CanTransition(work.State, state) {
work.State = state
} else {
work.State = WorkStateBlocked
}
work.Blocker = &blocker
return nil
})
m.emit(ctx, Event{
Type: EventBlocked, ProjectID: projectID, WorkUnitID: workID,
State: state, Detail: string(blocker.Code),
})
}