package taskloop import ( "bytes" "context" "crypto/sha256" "encoding/json" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "sync" "syscall" "time" "iop/packages/go/agentconfig" "iop/packages/go/agentpolicy" "iop/packages/go/agentruntime" "iop/packages/go/agenttask" ) const ( maxProviderDiagnosticBytes = 32 << 10 nativeSessionLocatorSchemaVersion = 1 ) // Provider converts one catalog profile into the side-effect-free launch plan // required by agenttask.Manager. The executable confinement proof remains the // sole process and stdio owner. type Provider struct { catalog agentconfig.Catalog quota QuotaObserver now func() time.Time } var _ agenttask.ProviderInvoker = (*Provider)(nil) func NewProvider( catalog agentconfig.Catalog, quotaObservers ...QuotaObserver, ) (*Provider, error) { normalized, err := agentconfig.Normalize(catalog) if err != nil { return nil, fmt.Errorf("taskloop: normalize provider catalog: %w", err) } var quota QuotaObserver if len(quotaObservers) > 1 { return nil, errors.New("taskloop: provider accepts at most one quota observer") } if len(quotaObservers) == 1 { quota = quotaObservers[0] } return &Provider{catalog: normalized, quota: quota, now: time.Now}, nil } func (provider *Provider) Prepare( ctx context.Context, request agenttask.DispatchRequest, ) (agenttask.ProviderLaunch, error) { if err := ctx.Err(); err != nil { return nil, err } if request.Permit == nil || request.Confinement == nil { return nil, errors.New("taskloop: provider launch requires an admitted confinement") } resolved, ok := provider.catalog.ResolveProfile(request.Target.ProfileID) if !ok { return nil, fmt.Errorf( "taskloop: selected provider profile %q is not in the catalog", request.Target.ProfileID, ) } if resolved.Provider.ID != request.Target.ProviderID || resolved.Model.ID != request.Target.ModelID { return nil, errors.New("taskloop: selected provider identity does not match the catalog") } plan := request.Work.Unit.Metadata["plan_path"] review := request.Work.Unit.Metadata["review_path"] if plan == "" || review == "" { return nil, errors.New("taskloop: work unit is missing its active artifact locators") } prompt := workerPrompt(plan, review) binding := request.Confinement.Binding() if request.Confinement.Revision() != binding.Revision || binding.ProfileRevision != request.Target.ProfileRevision { return nil, errors.New("taskloop: admitted confinement identity mismatch") } if err := request.Confinement.Validate(binding); err != nil { return nil, fmt.Errorf("taskloop: validate admitted confinement: %w", err) } command, session, err := prepareFreshCatalogCommand( resolved, request.Target, binding, request.Workspace.WorkingDir, prompt, request.Project, request.Work, "worker", ) if err != nil { return nil, err } return &providerLaunch{ command: command, project: request.Project, work: request.Work, artifact: artifactIdentity(request.Work), profileID: resolved.Profile.ID, session: session, target: request.Target, quota: provider.quota, now: provider.now, }, nil } func workerPrompt(plan, review string) string { return fmt.Sprintf( "This is a direct implementing-worker assignment owned by the task loop. "+ "Read %s and execute it directly; do not invoke a task-loop dispatcher, "+ "plan workflow, or code-review workflow. Keep artifact content in English. "+ "Fill only the implementation-owned sections in %s and stop ready for "+ "official review. Do not append a review verdict, modify review-only fields, "+ "write completion metadata, update a roadmap, or archive, move, rename, or "+ "delete either active artifact.", plan, review, ) } type providerLaunch struct { command agenttask.ConfinementCommand project agenttask.ProjectRecord work agenttask.WorkRecord artifact agenttask.ArtifactID profileID string session *agenttask.LocatorRecord target agenttask.ExecutionTarget quota QuotaObserver now func() time.Time } func (launch *providerLaunch) Command() agenttask.ConfinementCommand { return agenttask.ConfinementCommand{ Name: launch.command.Name, Args: append([]string(nil), launch.command.Args...), Env: append([]string(nil), launch.command.Env...), } } func (launch *providerLaunch) BindStarted( started agenttask.StartedConfinement, ) (agenttask.ProviderInvocation, error) { if started == nil || started.Child() == nil || started.Child().Process == nil || started.Stdin() == nil || started.Stdout() == nil || started.Stderr() == nil { return nil, errors.New("taskloop: confined provider child is incomplete") } opaque, err := processLocator(started.Child().Process.Pid) if err != nil { return nil, err } locator := agenttask.LocatorRecord{ Kind: agenttask.LocatorProcess, Opaque: opaque, Revision: digestStrings("process-locator", opaque), ProjectID: launch.project.ProjectID, WorkspaceID: launch.project.WorkspaceID, WorkUnitID: launch.work.Unit.ID, AttemptID: launch.work.AttemptID, } locators := []agenttask.LocatorRecord{locator} if launch.session != nil { locators = append(locators, *launch.session) } invocation := &providerInvocation{ started: started, project: launch.project, work: launch.work, artifact: launch.artifact, locators: locators, target: launch.target, quota: launch.quota, now: launch.now, done: make(chan invocationResult, 1), } invocation.begin() return invocation, nil } type invocationResult struct { err error } type providerInvocation struct { started agenttask.StartedConfinement project agenttask.ProjectRecord work agenttask.WorkRecord artifact agenttask.ArtifactID locators []agenttask.LocatorRecord target agenttask.ExecutionTarget quota QuotaObserver now func() time.Time stdout boundedBuffer stderr boundedBuffer done chan invocationResult once sync.Once failureMu sync.Mutex failure agentpolicy.AttemptObservation } var _ agenttask.ProviderInvocation = (*providerInvocation)(nil) var _ agenttask.FailureObservedInvocation = (*providerInvocation)(nil) func (invocation *providerInvocation) begin() { invocation.once.Do(func() { _ = invocation.started.Stdin().Close() var drains sync.WaitGroup drains.Add(2) go func() { defer drains.Done() _, _ = io.Copy(&invocation.stdout, invocation.started.Stdout()) _ = invocation.started.Stdout().Close() }() go func() { defer drains.Done() _, _ = io.Copy(&invocation.stderr, invocation.started.Stderr()) _ = invocation.started.Stderr().Close() }() go func() { waitErr := invocation.started.Child().Wait() drains.Wait() invocation.done <- invocationResult{err: waitErr} close(invocation.done) }() }) } func (invocation *providerInvocation) Locators() []agenttask.LocatorRecord { return append([]agenttask.LocatorRecord(nil), invocation.locators...) } func (invocation *providerInvocation) Wait( ctx context.Context, ) (agenttask.Submission, error) { select { case <-ctx.Done(): _ = invocation.Cancel(context.WithoutCancel(ctx)) invocation.recordFailure( context.WithoutCancel(ctx), &agentruntime.Failure{ Code: agentruntime.FailureCodeCancelled, Retryable: false, }, false, ) return agenttask.Submission{}, ctx.Err() case result := <-invocation.done: if result.err != nil { if ctx.Err() != nil { invocation.recordFailure( context.WithoutCancel(ctx), &agentruntime.Failure{ Code: agentruntime.FailureCodeCancelled, Retryable: false, }, false, ) return agenttask.Submission{}, ctx.Err() } invocation.recordFailure( ctx, &agentruntime.Failure{ Code: agentruntime.FailureCodeProcessExit, Retryable: true, }, true, ) return agenttask.Submission{}, errors.New("taskloop: provider exited unsuccessfully") } return agenttask.Submission{ ProjectID: invocation.project.ProjectID, WorkUnitID: invocation.work.Unit.ID, AttemptID: invocation.work.AttemptID, ArtifactID: invocation.artifact, Ready: true, Metadata: map[string]string{ "provider_output": "bounded-and-discarded", }, Locators: invocation.Locators(), }, nil } } func (invocation *providerInvocation) FailureObservation() agentpolicy.AttemptObservation { invocation.failureMu.Lock() defer invocation.failureMu.Unlock() return invocation.failure.Clone() } func (invocation *providerInvocation) recordFailure( ctx context.Context, failure *agentruntime.Failure, observeQuota bool, ) { now := time.Now if invocation.now != nil { now = invocation.now } observedAt := now().UTC() quota := agentpolicy.CorruptQuotaObservation() if observeQuota && invocation.quota != nil { if observed, err := invocation.quota.ObserveQuota( ctx, invocation.target, observedAt, ); err == nil { quota = observed } } invocation.failureMu.Lock() invocation.failure = agentpolicy.SanitizeAttemptObservation( agentpolicy.AttemptObservation{ ObservedAt: observedAt, Quota: quota, Failure: agentpolicy.NormalizeFailureObservation(failure), }, observedAt, ) invocation.failureMu.Unlock() } func (invocation *providerInvocation) Cancel(ctx context.Context) error { if err := ctx.Err(); err != nil { return err } child := invocation.started.Child() if child == nil || child.Process == nil { return errors.New("taskloop: provider process identity is unavailable") } if err := child.Process.Signal(syscall.SIGTERM); err != nil && !errors.Is(err, os.ErrProcessDone) { if killErr := child.Process.Kill(); killErr != nil && !errors.Is(killErr, os.ErrProcessDone) { return errors.Join(err, killErr) } } return nil } type boundedBuffer struct { mu sync.Mutex buffer bytes.Buffer truncated bool } func (buffer *boundedBuffer) Write(data []byte) (int, error) { buffer.mu.Lock() defer buffer.mu.Unlock() remaining := maxProviderDiagnosticBytes - buffer.buffer.Len() if remaining > 0 { write := len(data) if write > remaining { write = remaining } _, _ = buffer.buffer.Write(data[:write]) } if len(data) > remaining { buffer.truncated = true } return len(data), nil } type processReference struct { PID int `json:"pid"` StartToken string `json:"start_token"` } func processLocator(pid int) (string, error) { if pid <= 0 { return "", errors.New("taskloop: provider PID is invalid") } token, err := processStartToken(pid) if err != nil { return "", fmt.Errorf("taskloop: capture provider process identity: %w", err) } encoded, err := json.Marshal(processReference{PID: pid, StartToken: token}) if err != nil { return "", err } return string(encoded), nil } func processStartToken(pid int) (string, error) { if stat, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat"); err == nil { text := string(stat) if closeIndex := strings.LastIndex(text, ")"); closeIndex >= 0 { fields := strings.Fields(text[closeIndex+1:]) // Fields after the executable name begin at proc field 3. The // process start time is field 22, therefore index 19 here. if len(fields) > 19 && fields[19] != "" { return "proc:" + fields[19], nil } } } output, err := exec.Command("ps", "-o", "lstart=", "-p", strconv.Itoa(pid)).Output() if err != nil { return "", err } token := strings.Join(strings.Fields(string(output)), " ") if token == "" { return "", errors.New("empty process start token") } return "ps:" + token, nil } func artifactIdentity(work agenttask.WorkRecord) agenttask.ArtifactID { return agenttask.ArtifactID( digestStrings("artifact", string(work.Unit.ID), string(work.AttemptID)), ) } func expandModelTarget(arguments []string, target string) []string { expanded := make([]string, len(arguments)) for index, argument := range arguments { expanded[index] = strings.ReplaceAll(argument, "{{model}}", target) } return expanded } func replaceEnvironment(environment []string, key, value string) []string { prefix := key + "=" out := make([]string, 0, len(environment)+1) for _, entry := range environment { if !strings.HasPrefix(entry, prefix) { out = append(out, entry) } } return append(out, prefix+value) } type nativeSessionReference struct { SchemaVersion uint32 `json:"schema_version"` ProviderID string `json:"provider_id"` ModelID string `json:"model_id"` ProfileID string `json:"profile_id"` ProfileRevision string `json:"profile_revision"` SessionID string `json:"session_id"` SessionDirectory string `json:"session_directory"` ConfinementRevision string `json:"confinement_revision"` IsolationID string `json:"isolation_id"` IsolationRevision string `json:"isolation_revision"` } func prepareFreshCatalogCommand( resolved agentconfig.ResolvedProfile, target agenttask.ExecutionTarget, binding agenttask.ConfinementBinding, workingDirectory, prompt string, project agenttask.ProjectRecord, work agenttask.WorkRecord, purpose string, ) (agenttask.ConfinementCommand, *agenttask.LocatorRecord, error) { if err := validateCatalogTarget(resolved, target); err != nil { return agenttask.ConfinementCommand{}, nil, err } args := expandModelTarget(resolved.Profile.Args, resolved.Model.Target) var locator *agenttask.LocatorRecord if strings.EqualFold(resolved.Provider.ID, "pi") { reference, err := newNativeSessionReference( resolved, target, binding, project, work, purpose, ) if err != nil { return agenttask.ConfinementCommand{}, nil, err } args = append(args, "--session-id", reference.SessionID, "--session-dir", reference.SessionDirectory, ) encoded, err := json.Marshal(reference) if err != nil { return agenttask.ConfinementCommand{}, nil, err } record := agenttask.LocatorRecord{ Kind: agenttask.LocatorSession, Opaque: string(encoded), Revision: digestStrings("native-session-locator", string(encoded)), ProjectID: project.ProjectID, WorkspaceID: project.WorkspaceID, WorkUnitID: work.Unit.ID, AttemptID: work.AttemptID, } locator = &record } args = append(args, prompt) environment, err := providerEnvironment( resolved, binding, workingDirectory, ) if err != nil { return agenttask.ConfinementCommand{}, nil, err } return agenttask.ConfinementCommand{ Name: resolved.Provider.Command, Args: args, Env: environment, }, locator, nil } func prepareResumeCatalogCommand( resolved agentconfig.ResolvedProfile, target agenttask.ExecutionTarget, binding agenttask.ConfinementBinding, workingDirectory, prompt, nativeSessionFile string, reference nativeSessionReference, ) (agenttask.ConfinementCommand, error) { if err := validateCatalogTarget(resolved, target); err != nil { return agenttask.ConfinementCommand{}, err } if !strings.EqualFold(resolved.Provider.ID, "pi") { return agenttask.ConfinementCommand{}, errors.New( "taskloop: native session resume is supported only for Pi", ) } if err := validateNativeSessionReference(reference, resolved, target, binding); err != nil { return agenttask.ConfinementCommand{}, err } if err := validateContainedRegularFile( reference.SessionDirectory, nativeSessionFile, ); err != nil { return agenttask.ConfinementCommand{}, err } baseArgs := resolved.Profile.ResumeArgs if len(baseArgs) == 0 { baseArgs = resolved.Profile.Args } args := expandModelTarget(baseArgs, resolved.Model.Target) args = append(args, "--session", nativeSessionFile, "--session-dir", reference.SessionDirectory, prompt, ) environment, err := providerEnvironment( resolved, binding, workingDirectory, ) if err != nil { return agenttask.ConfinementCommand{}, err } return agenttask.ConfinementCommand{ Name: resolved.Provider.Command, Args: args, Env: environment, }, nil } func validateCatalogTarget( resolved agentconfig.ResolvedProfile, target agenttask.ExecutionTarget, ) error { if resolved.Provider.ID != target.ProviderID || resolved.Model.ID != target.ModelID || resolved.Profile.ID != target.ProfileID || guardProfile(resolved).Revision != target.ProfileRevision { return errors.New("taskloop: selected provider identity does not match the catalog") } return nil } func providerEnvironment( resolved agentconfig.ResolvedProfile, binding agenttask.ConfinementBinding, workingDirectory string, ) ([]string, error) { environment := append([]string(nil), os.Environ()...) environment = append(environment, resolved.Profile.Env...) if workingDirectory != "" { environment = replaceEnvironment(environment, "PWD", workingDirectory) } if binding.TaskRoot != "" { environment = replaceEnvironment( environment, "IOP_AGENT_TASK_ROOT", binding.TaskRoot, ) } if len(binding.WritableRoots) == 3 { tempRoot := binding.WritableRoots[1] for _, key := range []string{"TMPDIR", "TMP", "TEMP"} { environment = replaceEnvironment(environment, key, tempRoot) } } if !strings.EqualFold(resolved.Provider.ID, "codex") { return environment, nil } if len(binding.WritableRoots) != 3 { return nil, errors.New("taskloop: codex confinement has invalid writable roots") } codexHome, err := prepareCodexRuntimeHome(binding.WritableRoots[2]) if err != nil { return nil, err } environment = replaceEnvironment(environment, "CODEX_HOME", codexHome) if runtime.GOOS == "darwin" { const systemCABundle = "/etc/ssl/cert.pem" if info, statErr := os.Stat(systemCABundle); statErr == nil && info.Mode().IsRegular() { environment = replaceEnvironment( environment, "SSL_CERT_FILE", systemCABundle, ) } } return environment, nil } func prepareCodexRuntimeHome(cacheRoot string) (string, error) { if cacheRoot == "" || !filepath.IsAbs(cacheRoot) { return "", errors.New("taskloop: codex runtime cache root is invalid") } codexHome := filepath.Join(cacheRoot, "codex-home") if err := validateContainedPath(cacheRoot, codexHome); err != nil || codexHome == cacheRoot { return "", errors.New("taskloop: codex runtime home escapes the cache root") } if err := os.MkdirAll(codexHome, 0o700); err != nil { return "", fmt.Errorf("taskloop: create codex runtime home: %w", err) } if err := os.Chmod(codexHome, 0o700); err != nil { return "", fmt.Errorf("taskloop: protect codex runtime home: %w", err) } sourceHome := strings.TrimSpace(os.Getenv("CODEX_HOME")) if sourceHome == "" { userHome, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("taskloop: resolve codex authentication home: %w", err) } sourceHome = filepath.Join(userHome, ".codex") } sourceHome, err := filepath.Abs(sourceHome) if err != nil { return "", fmt.Errorf("taskloop: resolve codex authentication home: %w", err) } for _, name := range []string{"auth.json", "config.toml"} { source := filepath.Join(sourceHome, name) info, statErr := os.Stat(source) if statErr != nil { if name == "config.toml" && os.IsNotExist(statErr) { continue } return "", fmt.Errorf( "taskloop: inspect codex authentication file %s: %w", name, statErr, ) } if !info.Mode().IsRegular() { return "", fmt.Errorf( "taskloop: codex authentication file %s is not regular", name, ) } destination := filepath.Join(codexHome, name) if existing, linkErr := os.Readlink(destination); linkErr == nil { if existing != source { return "", fmt.Errorf( "taskloop: codex runtime link %s changed identity", name, ) } continue } else if !os.IsNotExist(linkErr) { return "", fmt.Errorf( "taskloop: inspect codex runtime link %s: %w", name, linkErr, ) } if err := os.Symlink(source, destination); err != nil { return "", fmt.Errorf( "taskloop: project codex runtime link %s: %w", name, err, ) } } return codexHome, nil } func newNativeSessionReference( resolved agentconfig.ResolvedProfile, target agenttask.ExecutionTarget, binding agenttask.ConfinementBinding, project agenttask.ProjectRecord, work agenttask.WorkRecord, purpose string, ) (nativeSessionReference, error) { if len(binding.WritableRoots) == 0 { return nativeSessionReference{}, errors.New( "taskloop: Pi native session requires a confined writable cache root", ) } cacheRoot := binding.WritableRoots[len(binding.WritableRoots)-1] if !filepath.IsAbs(cacheRoot) || filepath.Clean(cacheRoot) != cacheRoot { return nativeSessionReference{}, errors.New( "taskloop: Pi native session cache root is invalid", ) } sessionID := deterministicSessionID( string(project.ProjectID), string(project.WorkspaceID), string(work.Unit.ID), string(work.AttemptID), target.ProfileRevision, binding.Revision, purpose, ) return nativeSessionReference{ SchemaVersion: nativeSessionLocatorSchemaVersion, ProviderID: resolved.Provider.ID, ModelID: resolved.Model.ID, ProfileID: resolved.Profile.ID, ProfileRevision: target.ProfileRevision, SessionID: sessionID, SessionDirectory: filepath.Join(cacheRoot, "pi-sessions", sessionID), ConfinementRevision: binding.Revision, IsolationID: binding.IsolationID, IsolationRevision: binding.IsolationRevision, }, nil } func deterministicSessionID(parts ...string) string { sum := sha256.Sum256([]byte(strings.Join(parts, "\x00"))) sum[6] = (sum[6] & 0x0f) | 0x40 sum[8] = (sum[8] & 0x3f) | 0x80 return fmt.Sprintf( "%x-%x-%x-%x-%x", sum[0:4], sum[4:6], sum[6:8], sum[8:10], sum[10:16], ) } func validateNativeSessionReference( reference nativeSessionReference, resolved agentconfig.ResolvedProfile, target agenttask.ExecutionTarget, binding agenttask.ConfinementBinding, ) error { if reference.SchemaVersion != nativeSessionLocatorSchemaVersion || reference.ProviderID != resolved.Provider.ID || reference.ModelID != resolved.Model.ID || reference.ProfileID != resolved.Profile.ID || reference.ProfileRevision != target.ProfileRevision || reference.ConfinementRevision != binding.Revision || reference.IsolationID != binding.IsolationID || reference.IsolationRevision != binding.IsolationRevision || reference.SessionID == "" { return errors.New("taskloop: native session locator identity mismatch") } if len(binding.WritableRoots) == 0 { return errors.New("taskloop: native session confinement has no writable roots") } cacheRoot := binding.WritableRoots[len(binding.WritableRoots)-1] if err := validateContainedPath(cacheRoot, reference.SessionDirectory); err != nil { return fmt.Errorf("taskloop: native session directory: %w", err) } if filepath.Base(reference.SessionDirectory) != reference.SessionID { return errors.New("taskloop: native session directory identity mismatch") } return nil } func resolveNativeSessionFile(reference nativeSessionReference) (string, error) { matches, err := filepath.Glob( filepath.Join(reference.SessionDirectory, "*"+reference.SessionID+"*.jsonl"), ) if err != nil { return "", err } if len(matches) != 1 { return "", errors.New( "taskloop: Pi repair requires exactly one matching native session file", ) } if err := validateContainedRegularFile(reference.SessionDirectory, matches[0]); err != nil { return "", err } return matches[0], nil } func validateContainedPath(root, path string) error { if !filepath.IsAbs(root) || !filepath.IsAbs(path) || filepath.Clean(root) != root || filepath.Clean(path) != path { return errors.New("path must be absolute and clean") } relative, err := filepath.Rel(root, path) if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) { return errors.New("path escapes its owned root") } return nil } func validateContainedRegularFile(root, path string) error { if err := validateContainedPath(root, path); err != nil { return err } info, err := os.Lstat(path) if err != nil { return err } if !info.Mode().IsRegular() || info.Size() <= 0 || info.Size() > maxWorkflowArtifactBytes { return errors.New("taskloop: native session is not one bounded regular file") } return nil } func invokeConfined( ctx context.Context, confinement agenttask.InvocationConfinement, command agenttask.ConfinementCommand, ) error { if confinement == nil { return errors.New("taskloop: executable confinement is unavailable") } binding := confinement.Binding() if confinement.Revision() != binding.Revision { return errors.New("taskloop: executable confinement revision mismatch") } if err := confinement.Validate(binding); err != nil { return fmt.Errorf("taskloop: validate executable confinement: %w", err) } started, err := confinement.Start(ctx, command) if err != nil { return err } if started == nil || started.Child() == nil || started.Child().Process == nil || started.Stdin() == nil || started.Stdout() == nil || started.Stderr() == nil { if started != nil { _ = started.Abort() } return errors.New("taskloop: confined provider child is incomplete") } _ = started.Stdin().Close() var stdout, stderr boundedBuffer var drains sync.WaitGroup drains.Add(2) go func() { defer drains.Done() _, _ = io.Copy(&stdout, started.Stdout()) _ = started.Stdout().Close() }() go func() { defer drains.Done() _, _ = io.Copy(&stderr, started.Stderr()) _ = started.Stderr().Close() }() done := make(chan error, 1) go func() { waitErr := started.Child().Wait() drains.Wait() done <- waitErr }() select { case <-ctx.Done(): if signalErr := started.Child().Process.Signal(syscall.SIGTERM); signalErr != nil && !errors.Is(signalErr, os.ErrProcessDone) { _ = started.Child().Process.Kill() } <-done return ctx.Err() case waitErr := <-done: if waitErr != nil { return errors.New("taskloop: confined provider exited unsuccessfully") } return nil } }