diff --git a/apps/daemon/internal/agent/contracttest/text.go b/apps/daemon/internal/agent/contracttest/text.go index bc7f336a7..90e513753 100644 --- a/apps/daemon/internal/agent/contracttest/text.go +++ b/apps/daemon/internal/agent/contracttest/text.go @@ -66,12 +66,8 @@ func TextLifecycle(t *testing.T, fixture TextFixture) { if err := first.Cancel(ctx); err != nil { t.Fatal(err) } - steerer, ok := second.(agent.DurableSteerer) - if !ok { - t.Fatal("public text Turn must implement DurableSteerer") - } var written atomic.Int32 - if err := steerer.SteerWithReceipt(ctx, proto.PromptSteerPayload{InputID: "contract-input", Input: fixture.SteeringInput}, func() { written.Add(1) }); err != nil { + if err := second.SteerWithReceipt(ctx, proto.PromptSteerPayload{InputID: "contract-input", Input: fixture.SteeringInput}, func() { written.Add(1) }); err != nil { t.Fatalf("active input after stale cancellation: %v", err) } if written.Load() != 1 { diff --git a/apps/daemon/internal/agent/functions.go b/apps/daemon/internal/agent/functions.go deleted file mode 100644 index c34d8b32c..000000000 --- a/apps/daemon/internal/agent/functions.go +++ /dev/null @@ -1,7 +0,0 @@ -package agent - -import ( - "errors" -) - -var ErrUnknownFunctionCall = errors.New("agent: function call is no longer pending") diff --git a/apps/daemon/internal/agent/harness.go b/apps/daemon/internal/agent/harness.go index 53aaeb02a..25b5ff61e 100644 --- a/apps/daemon/internal/agent/harness.go +++ b/apps/daemon/internal/agent/harness.go @@ -28,6 +28,7 @@ package agent import ( "context" + "errors" "io" "reflect" "strings" @@ -145,6 +146,7 @@ type Executor interface { type Turn interface { Session DurableSteerer + FunctionResultSubmitter // Success confirms closed output and settled native input, function and // child-work obligations. Errors cannot prove cancellation. AwaitSettlement(context.Context) (TurnSettlement, error) @@ -182,12 +184,42 @@ type DurableSteerer interface { SteerWithReceipt(context.Context, proto.PromptSteerPayload, func()) error } -// FunctionResultSubmitter delivers a result for an outstanding native call. +// FunctionResultSubmitter is mandatory on every public Turn; support comes from the declaration. It delivers a result for an outstanding native call. // A successful return requires its native application receipt. type FunctionResultSubmitter interface { SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error } +// Protocol errors. + +// ErrUnsupportedOperation marks an intentional absence of optional behavior. +// Implementations wrap it with a static reason and perform no native effects. +// It is distinct from unavailable resources, failures and uncertain outcomes. +var ErrUnsupportedOperation = errors.New("agent: unsupported operation") + +// ErrUnknownFunctionCall means the function call is no longer pending. +var ErrUnknownFunctionCall = errors.New("agent: function call is no longer pending") + +// ErrSteeringNotReady means no input was sent because the turn is starting. +var ErrSteeringNotReady = errors.New("agent: turn is not ready for input") + +// ErrSteeringInactive means no input was sent because the run ended or was cancelled. +var ErrSteeringInactive = errors.New("agent: run is no longer active") + +// ErrSteeringRejected means the engine explicitly rejected the input. +var ErrSteeringRejected = errors.New("agent: input rejected") + +// Installation is an optional adapter-owned native distribution contract. +// It supplies activation paths and readiness checks, never execution or model +// configuration. Runtime owns copying, checksums, publication and installation locks. +type Installation struct { + AgentKind string + Version string + Supported func() bool + Environment func(directory, node string) map[string]string + Check func(context.Context, string, string, []string) error +} + // Kind registration. // RegisterKind installs the heartbeat descriptor and model configuration for an diff --git a/apps/daemon/internal/agent/installation.go b/apps/daemon/internal/agent/installation.go deleted file mode 100644 index 2083b8052..000000000 --- a/apps/daemon/internal/agent/installation.go +++ /dev/null @@ -1,14 +0,0 @@ -package agent - -import "context" - -// Installation is an optional adapter-owned native distribution contract. -// It supplies activation paths and readiness checks, never execution or model -// configuration. Runtime owns copying, checksums, publication and installation locks. -type Installation struct { - AgentKind string - Version string - Supported func() bool - Environment func(directory, node string) map[string]string - Check func(context.Context, string, string, []string) error -} diff --git a/apps/daemon/internal/agent/steering.go b/apps/daemon/internal/agent/steering.go deleted file mode 100644 index aea8885bc..000000000 --- a/apps/daemon/internal/agent/steering.go +++ /dev/null @@ -1,14 +0,0 @@ -package agent - -import ( - "errors" -) - -// ErrSteeringNotReady means no input was sent because the turn is starting. -var ErrSteeringNotReady = errors.New("agent: turn is not ready for input") - -// ErrSteeringInactive means no input was sent because the run ended or was cancelled. -var ErrSteeringInactive = errors.New("agent: run is no longer active") - -// ErrSteeringRejected means the engine explicitly rejected the input. -var ErrSteeringRejected = errors.New("agent: input rejected") diff --git a/apps/daemon/internal/agent/unsupported.go b/apps/daemon/internal/agent/unsupported.go deleted file mode 100644 index 55cca49e6..000000000 --- a/apps/daemon/internal/agent/unsupported.go +++ /dev/null @@ -1,8 +0,0 @@ -package agent - -import "errors" - -// ErrUnsupportedOperation marks an intentional absence of optional behavior. -// Implementations wrap it with a static reason and perform no native effects. -// It is distinct from unavailable resources, failures and uncertain outcomes. -var ErrUnsupportedOperation = errors.New("agent: unsupported operation") diff --git a/apps/daemon/internal/agent/workspace_directory.go b/apps/daemon/internal/agent/workspace_directory.go deleted file mode 100644 index d344f49a9..000000000 --- a/apps/daemon/internal/agent/workspace_directory.go +++ /dev/null @@ -1,14 +0,0 @@ -package agent - -// WorkspaceDirectoryEntry describes an entry observed without following its final symlink. -type WorkspaceDirectoryEntry struct { - Name string - Kind string - SizeBytes *int64 -} - -// WorkspaceDirectoryResult is a live, bounded observation, not a filesystem snapshot. -type WorkspaceDirectoryResult struct { - Entries []WorkspaceDirectoryEntry - Truncated bool -} diff --git a/apps/daemon/internal/agent/workspace_read.go b/apps/daemon/internal/agent/workspace_read.go deleted file mode 100644 index c69f31321..000000000 --- a/apps/daemon/internal/agent/workspace_read.go +++ /dev/null @@ -1,18 +0,0 @@ -package agent - -import "errors" - -type WorkspaceReadResult struct { - Data []byte - Truncated bool -} - -var ( - ErrWorkspaceReadUnavailable = errors.New("workspace read unavailable") - ErrWorkspaceReadBusy = errors.New("workspace read busy") - ErrWorkspaceReadInvalid = errors.New("workspace read invalid") - ErrWorkspaceReadUncertain = errors.New("workspace read outcome uncertain") - // ErrWorkspaceNotDirectory reports that a directory request's own path is - // missing, a regular file or a symbolic link; the link was not followed. - ErrWorkspaceNotDirectory = errors.New("workspace path is not a directory") -) diff --git a/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go b/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go index ecfdde7a0..e5b6cddaf 100644 --- a/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go +++ b/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go @@ -103,6 +103,10 @@ func (t *receiptCancelTurn) AwaitSettlement(ctx context.Context) (agent.TurnSett func (t *receiptCancelTurn) CancellationOutcome() proto.DonePayload { return proto.DonePayload{Content: "observed", Metadata: map[string]any{proto.DoneMetaAgentSessionID: "native-session"}} } +func (*receiptCancelTurn) SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error { + return agent.ErrUnsupportedOperation +} + func (t *receiptCancelTurn) SteerWithReceipt(ctx context.Context, _ proto.PromptSteerPayload, written func()) error { written() close(t.written) diff --git a/apps/daemon/internal/dispatch/executor_handoff_test.go b/apps/daemon/internal/dispatch/executor_handoff_test.go index 31f0b9ed3..7962ee220 100644 --- a/apps/daemon/internal/dispatch/executor_handoff_test.go +++ b/apps/daemon/internal/dispatch/executor_handoff_test.go @@ -228,6 +228,10 @@ func TestPreparedDonePublishesAfterExecutorHandoff(t *testing.T) { } // These fixtures exercise settlement only; active input is deliberately rejected. +func (*terminalHandoffTurn) SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error { + return agent.ErrUnsupportedOperation +} + func (*terminalHandoffTurn) SteerWithReceipt(context.Context, proto.PromptSteerPayload, func()) error { return agent.ErrSteeringRejected } diff --git a/apps/daemon/internal/dispatch/executor_test.go b/apps/daemon/internal/dispatch/executor_test.go index e5ed86a39..500b28f4e 100644 --- a/apps/daemon/internal/dispatch/executor_test.go +++ b/apps/daemon/internal/dispatch/executor_test.go @@ -56,6 +56,10 @@ func (t *reusableTurn) finish() { close(t.settled) }) } +func (*reusableTurn) SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error { + return agent.ErrUnsupportedOperation +} + func (t *reusableTurn) Cancel(context.Context) error { t.cancels.Add(1); t.finish(); return nil } func (t *reusableTurn) CancellationOutcome() proto.DonePayload { return proto.DonePayload{Content: t.id, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "native-session"}} diff --git a/apps/daemon/internal/dispatch/functions.go b/apps/daemon/internal/dispatch/functions.go index 14252711b..d84716a7d 100644 --- a/apps/daemon/internal/dispatch/functions.go +++ b/apps/daemon/internal/dispatch/functions.go @@ -44,10 +44,6 @@ func (r *Router) handleFunctionResult(ctx context.Context, env proto.Envelope) e r.mu.Lock() state := r.sessions[env.ID] session, finishOperation, ready := r.preparedOperationLocked(state) - var submitter agent.FunctionResultSubmitter - if ready { - submitter, _ = session.(agent.FunctionResultSubmitter) - } r.mu.Unlock() if state != nil && !ready { return r.sendInteractionDecisionAck(ctx, env.ID, result.DeliveryID, false, "not_ready", "function call is waiting for the native session") @@ -61,13 +57,10 @@ func (r *Router) handleFunctionResult(ctx context.Context, env proto.Envelope) e if state != nil && !state.capabilities.FunctionTools.IsSupported() { return r.sendInteractionDecisionAck(ctx, env.ID, result.DeliveryID, false, "unsupported", "The runtime declaration does not support function results.") } - if ready && submitter == nil { - return r.sendInteractionDecisionAck(ctx, env.ID, result.DeliveryID, false, "contract_violation", "Declared function capability has no implementation.") - } - if submitter == nil { + if session == nil { return r.sendInteractionDecisionAck(ctx, env.ID, result.DeliveryID, false, "not_pending", "function call is no longer pending") } - if err := submitter.SubmitFunctionResult(ctx, result); err != nil { + if err := session.SubmitFunctionResult(ctx, result); err != nil { code := "runtime_error" if errors.Is(err, agent.ErrUnsupportedOperation) { code = "contract_violation" diff --git a/apps/daemon/internal/dispatch/native_file_results_test.go b/apps/daemon/internal/dispatch/native_file_results_test.go index dcc8b83bf..fda03adbb 100644 --- a/apps/daemon/internal/dispatch/native_file_results_test.go +++ b/apps/daemon/internal/dispatch/native_file_results_test.go @@ -3,6 +3,7 @@ package dispatch import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/google/uuid" "io/fs" @@ -14,13 +15,13 @@ func TestNativeDirectoryFailureMapping(t *testing.T) { err error outcome, code string }{ - {agent.ErrWorkspaceNotDirectory, "rejected", proto.WorkspaceReadNotDirectory}, - {agent.ErrWorkspaceReadInvalid, "rejected", "invalid_request"}, + {localworkspace.ErrWorkspaceNotDirectory, "rejected", proto.WorkspaceReadNotDirectory}, + {localworkspace.ErrWorkspaceReadInvalid, "rejected", "invalid_request"}, {fs.ErrNotExist, "rejected", "not_found"}, {fs.ErrPermission, "rejected", "permission_denied"}, - {agent.ErrWorkspaceReadUncertain, "unknown", "read_unconfirmed"}, + {localworkspace.ErrWorkspaceReadUncertain, "unknown", "read_unconfirmed"}, } { - got := workspaceReadResult(agent.WorkspaceReadResult{}, test.err, 0) + got := workspaceReadResult(localworkspace.WorkspaceReadResult{}, test.err, 0) if got.Outcome != test.outcome || got.ErrorCode != test.code { t.Fatal(test.err, got) } @@ -32,7 +33,7 @@ func TestLocalUploadUnknownRetainsOwner(t *testing.T) { if err != nil { t.Fatal(err) } - got := workspaceWriteResult(agent.WorkspaceWriteResult{}, errors.New("unconfirmed mutation"), 3) + got := workspaceWriteResult(localworkspace.WorkspaceWriteResult{}, errors.New("unconfirmed mutation"), 3) if got.Outcome != "unknown" { t.Fatal(got) } diff --git a/apps/daemon/internal/dispatch/workspace_read.go b/apps/daemon/internal/dispatch/workspace_read.go index ead6b1896..b82d22bab 100644 --- a/apps/daemon/internal/dispatch/workspace_read.go +++ b/apps/daemon/internal/dispatch/workspace_read.go @@ -7,7 +7,6 @@ import ( "strings" "time" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) @@ -82,19 +81,9 @@ func executeWorkspaceRead(ctx context.Context, binding *localworkspace.Binding, if request.Operation == "directory" { read, err := binding.ListWorkspaceDirectory(ctx, request.Path, request.MaxEntries) if err != nil { - return workspaceReadResult(agent.WorkspaceReadResult{}, err, 0) + return workspaceReadResult(localworkspace.WorkspaceReadResult{}, err, 0) } - if read.Entries == nil || len(read.Entries) > request.MaxEntries { - return workspaceReadResult(agent.WorkspaceReadResult{}, agent.ErrWorkspaceReadUncertain, 0) - } - directory := &proto.WorkspaceDirectoryResult{Entries: make([]proto.WorkspaceDirectoryEntry, 0, len(read.Entries)), Truncated: read.Truncated} - for _, entry := range read.Entries { - directory.Entries = append(directory.Entries, proto.WorkspaceDirectoryEntry{Name: entry.Name, Kind: entry.Kind, SizeBytes: entry.SizeBytes}) - } - if !proto.ValidWorkspaceDirectory(directory, request.MaxEntries) { - return workspaceReadResult(agent.WorkspaceReadResult{}, agent.ErrWorkspaceReadUncertain, 0) - } - return proto.WorkspaceReadResultPayload{Outcome: "completed", Directory: directory, CloseAcknowledged: true} + return proto.WorkspaceReadResultPayload{Outcome: "completed", Directory: read, CloseAcknowledged: true} } read, err := binding.ReadWorkspaceFile(ctx, request.Path, request.MaxBytes) return workspaceReadResult(read, err, request.MaxBytes) @@ -104,7 +93,7 @@ func rejectedWorkspaceRead(code string) proto.WorkspaceReadResultPayload { return proto.WorkspaceReadResultPayload{Outcome: "rejected", ErrorCode: code} } -func workspaceReadResult(read agent.WorkspaceReadResult, err error, limit int) proto.WorkspaceReadResultPayload { +func workspaceReadResult(read localworkspace.WorkspaceReadResult, err error, limit int) proto.WorkspaceReadResultPayload { if err == nil && len(read.Data) <= limit && (!read.Truncated || len(read.Data) == limit) { return proto.WorkspaceReadResultPayload{Outcome: "completed", Data: read.Data, Truncated: read.Truncated, CloseAcknowledged: true} } @@ -112,10 +101,10 @@ func workspaceReadResult(read agent.WorkspaceReadResult, err error, limit int) p err error code string }{ - {agent.ErrWorkspaceReadUnavailable, "resource_unavailable"}, - {agent.ErrWorkspaceReadBusy, "read_capacity"}, - {agent.ErrWorkspaceReadInvalid, "invalid_request"}, - {agent.ErrWorkspaceNotDirectory, proto.WorkspaceReadNotDirectory}, + {localworkspace.ErrWorkspaceReadUnavailable, "resource_unavailable"}, + {localworkspace.ErrWorkspaceReadBusy, "read_capacity"}, + {localworkspace.ErrWorkspaceReadInvalid, "invalid_request"}, + {localworkspace.ErrWorkspaceNotDirectory, proto.WorkspaceReadNotDirectory}, {fs.ErrNotExist, "not_found"}, {fs.ErrPermission, "permission_denied"}, } { diff --git a/apps/daemon/internal/dispatch/workspace_write.go b/apps/daemon/internal/dispatch/workspace_write.go index 7aa41f555..50ef6c5ff 100644 --- a/apps/daemon/internal/dispatch/workspace_write.go +++ b/apps/daemon/internal/dispatch/workspace_write.go @@ -7,7 +7,7 @@ import ( "errors" "time" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/google/uuid" ) @@ -139,7 +139,7 @@ func rejectedWorkspaceWrite(code string) proto.WorkspaceWriteResultPayload { return proto.WorkspaceWriteResultPayload{Outcome: "rejected", ErrorCode: code} } -func workspaceWriteResult(write agent.WorkspaceWriteResult, err error, size int) proto.WorkspaceWriteResultPayload { +func workspaceWriteResult(write localworkspace.WorkspaceWriteResult, err error, size int) proto.WorkspaceWriteResultPayload { if err == nil && write.SizeBytes == int64(size) { return proto.WorkspaceWriteResultPayload{Outcome: "completed", SizeBytes: size} } @@ -147,8 +147,8 @@ func workspaceWriteResult(write agent.WorkspaceWriteResult, err error, size int) err error reason string }{ - {agent.ErrWorkspaceWriteDirectory, proto.WorkspaceWriteReasonDirectory}, - {agent.ErrWorkspaceWriteUnsafe, proto.WorkspaceWriteReasonUnsafe}, + {localworkspace.ErrWorkspaceWriteDirectory, proto.WorkspaceWriteReasonDirectory}, + {localworkspace.ErrWorkspaceWriteUnsafe, proto.WorkspaceWriteReasonUnsafe}, } { if errors.Is(err, conflict.err) { return proto.WorkspaceWriteResultPayload{Outcome: "rejected", ErrorCode: "write_rejected", Reason: conflict.reason} @@ -158,11 +158,11 @@ func workspaceWriteResult(write agent.WorkspaceWriteResult, err error, size int) err error code string }{ - {agent.ErrWorkspaceWriteUnsupported, "write_unsupported"}, - {agent.ErrWorkspaceWriteUnavailable, "resource_unavailable"}, - {agent.ErrWorkspaceWriteBusy, "write_capacity"}, - {agent.ErrWorkspaceWriteInvalid, "invalid_request"}, - {agent.ErrWorkspaceWriteRejected, "write_rejected"}, + {localworkspace.ErrWorkspaceWriteUnsupported, "write_unsupported"}, + {localworkspace.ErrWorkspaceWriteUnavailable, "resource_unavailable"}, + {localworkspace.ErrWorkspaceWriteBusy, "write_capacity"}, + {localworkspace.ErrWorkspaceWriteInvalid, "invalid_request"}, + {localworkspace.ErrWorkspaceWriteRejected, "write_rejected"}, } { if errors.Is(err, failure.err) { return rejectedWorkspaceWrite(failure.code) diff --git a/apps/daemon/internal/localworkspace/directory.go b/apps/daemon/internal/localworkspace/directory.go index 3104ed8ef..b40a7decb 100644 --- a/apps/daemon/internal/localworkspace/directory.go +++ b/apps/daemon/internal/localworkspace/directory.go @@ -5,13 +5,12 @@ import ( "io/fs" "strings" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -func (b *Binding) ListWorkspaceDirectory(ctx context.Context, path string, limit int) (agent.WorkspaceDirectoryResult, error) { +func (b *Binding) ListWorkspaceDirectory(ctx context.Context, path string, limit int) (*proto.WorkspaceDirectoryResult, error) { if limit < 1 || limit > proto.WorkspaceDirectoryMaxEntries || len(path) > 4096 || strings.ContainsAny(path, "\\\x00\r\n") || (path != "" && (path == "." || !fs.ValidPath(path))) { - return agent.WorkspaceDirectoryResult{}, agent.ErrWorkspaceReadInvalid + return nil, ErrWorkspaceReadInvalid } return b.listNativeDirectory(ctx, path, limit) } diff --git a/apps/daemon/internal/localworkspace/directory_native_test.go b/apps/daemon/internal/localworkspace/directory_native_test.go index ef22420f6..4f0b5ffc4 100644 --- a/apps/daemon/internal/localworkspace/directory_native_test.go +++ b/apps/daemon/internal/localworkspace/directory_native_test.go @@ -2,7 +2,6 @@ package localworkspace import ( "errors" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "os" "path/filepath" "testing" @@ -21,7 +20,7 @@ func TestNativeDirectoryAPI(t *testing.T) { t.Fatal(got, err) } for _, path := range []string{"missing", "nested/data.bin"} { - if _, err := b.ListWorkspaceDirectory(t.Context(), path, 10); !errors.Is(err, agent.ErrWorkspaceNotDirectory) { + if _, err := b.ListWorkspaceDirectory(t.Context(), path, 10); !errors.Is(err, ErrWorkspaceNotDirectory) { t.Fatal(path, err) } } @@ -47,7 +46,7 @@ func TestNativeFileReadPrefixAndEntryKinds(t *testing.T) { if err != nil || got.Truncated || len(got.Data) != 3 { t.Fatal(got, err) } - if _, err = b.ReadWorkspaceFile(t.Context(), "nested", 3); !errors.Is(err, agent.ErrWorkspaceReadInvalid) { + if _, err = b.ReadWorkspaceFile(t.Context(), "nested", 3); !errors.Is(err, ErrWorkspaceReadInvalid) { t.Fatal(err) } if _, err = b.ReadWorkspaceFile(t.Context(), "missing", 3); !os.IsNotExist(err) { diff --git a/apps/daemon/internal/localworkspace/native_files.go b/apps/daemon/internal/localworkspace/native_files.go index 7748ca783..c8a0bdf86 100644 --- a/apps/daemon/internal/localworkspace/native_files.go +++ b/apps/daemon/internal/localworkspace/native_files.go @@ -11,7 +11,6 @@ import ( "sort" "strings" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/google/uuid" ) @@ -28,18 +27,17 @@ func nativeAPIPath(path string) (string, error) { return filepath.Localize(path) } -func (b *Binding) listNativeDirectory(ctx context.Context, path string, limit int) (agent.WorkspaceDirectoryResult, error) { - result := agent.WorkspaceDirectoryResult{Entries: []agent.WorkspaceDirectoryEntry{}} +func (b *Binding) listNativeDirectory(ctx context.Context, path string, limit int) (*proto.WorkspaceDirectoryResult, error) { if b == nil || ctx.Err() != nil { - return result, agent.ErrWorkspaceReadUnavailable + return nil, ErrWorkspaceReadUnavailable } local, err := nativeAPIPath(path) if err != nil { - return result, agent.ErrWorkspaceReadInvalid + return nil, ErrWorkspaceReadInvalid } root, err := os.OpenRoot(b.workspace) if err != nil { - return result, agent.ErrWorkspaceReadUnavailable + return nil, ErrWorkspaceReadUnavailable } defer root.Close() if local != "." { @@ -48,38 +46,38 @@ func (b *Binding) listNativeDirectory(ctx context.Context, path string, limit in partial = filepath.Join(partial, part) info, err := root.Lstat(partial) if errors.Is(err, fs.ErrPermission) { - return result, fs.ErrPermission + return nil, fs.ErrPermission } if err != nil || !info.IsDir() { - return result, agent.ErrWorkspaceNotDirectory + return nil, ErrWorkspaceNotDirectory } } } dir, err := openNativePath(root, local) if errors.Is(err, fs.ErrPermission) { - return result, fs.ErrPermission + return nil, fs.ErrPermission } if err != nil { - return result, agent.ErrWorkspaceNotDirectory + return nil, ErrWorkspaceNotDirectory } defer dir.Close() entries, err := dir.ReadDir(limit + 1) if err != nil && err != io.EOF { - return result, agent.ErrWorkspaceNotDirectory + return nil, ErrWorkspaceNotDirectory } if ctx.Err() != nil { - return result, agent.ErrWorkspaceReadUnavailable + return nil, ErrWorkspaceReadUnavailable } - result.Truncated = len(entries) > limit - if result.Truncated { + truncated := len(entries) > limit + if truncated { entries = entries[:limit] } sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) - wire := &proto.WorkspaceDirectoryResult{Entries: []proto.WorkspaceDirectoryEntry{}, Truncated: result.Truncated} + result := &proto.WorkspaceDirectoryResult{Entries: []proto.WorkspaceDirectoryEntry{}, Truncated: truncated} for _, entry := range entries { info, err := entry.Info() if err != nil { - return result, agent.ErrWorkspaceReadUncertain + return nil, ErrWorkspaceReadUncertain } item := proto.WorkspaceDirectoryEntry{Name: entry.Name(), Kind: "other"} switch { @@ -92,26 +90,23 @@ func (b *Binding) listNativeDirectory(ctx context.Context, path string, limit in case info.Mode()&os.ModeSymlink != 0: item.Kind = "symlink" } - wire.Entries = append(wire.Entries, item) - } - if !proto.ValidWorkspaceDirectory(wire, limit) { - return result, agent.ErrWorkspaceReadInvalid + result.Entries = append(result.Entries, item) } - for _, item := range wire.Entries { - result.Entries = append(result.Entries, agent.WorkspaceDirectoryEntry{Name: item.Name, Kind: item.Kind, SizeBytes: item.SizeBytes}) + if !proto.ValidWorkspaceDirectory(result, limit) { + return nil, ErrWorkspaceReadInvalid } return result, nil } -func (b *Binding) writeNativeFile(ctx context.Context, path string, data []byte) (agent.WorkspaceWriteResult, error) { - result := agent.WorkspaceWriteResult{} +func (b *Binding) writeNativeFile(ctx context.Context, path string, data []byte) (WorkspaceWriteResult, error) { + result := WorkspaceWriteResult{} local, err := nativeAPIPath(path) if err != nil { - return result, agent.ErrWorkspaceWriteInvalid + return result, ErrWorkspaceWriteInvalid } root, err := os.OpenRoot(b.workspace) if err != nil { - return result, agent.ErrWorkspaceWriteUnavailable + return result, ErrWorkspaceWriteUnavailable } defer root.Close() if err = root.MkdirAll(filepath.Dir(local), 0700); err != nil { @@ -135,24 +130,24 @@ func (b *Binding) writeNativeFile(ctx context.Context, path string, data []byte) if err = root.Link(temporary, local); err != nil { if info, e := root.Lstat(local); e == nil { if info.IsDir() { - return result, agent.ErrWorkspaceWriteDirectory + return result, ErrWorkspaceWriteDirectory } - return result, agent.ErrWorkspaceWriteUnsafe + return result, ErrWorkspaceWriteUnsafe } else if errors.Is(e, fs.ErrNotExist) { return result, unpublishedWriteError(err) } - return result, agent.ErrWorkspaceWriteRejected + return result, ErrWorkspaceWriteRejected } - return agent.WorkspaceWriteResult{SizeBytes: int64(len(data))}, nil + return WorkspaceWriteResult{SizeBytes: int64(len(data))}, nil } // unpublishedWriteError applies only before this operation publishes a file. // Parent directories or a temporary file may already have been created. func unpublishedWriteError(err error) error { if storageExhausted(err) { - return agent.ErrWorkspaceWriteUnavailable + return ErrWorkspaceWriteUnavailable } - return agent.ErrWorkspaceWriteRejected + return ErrWorkspaceWriteRejected } const artifactFileBytes int64 = 200 << 20 @@ -291,21 +286,21 @@ func (r nativeContextReader) Read(data []byte) (int, error) { // ReadWorkspaceFile returns the existing protocol's bounded prefix; the wire // has no offset field. Non-regular entries cannot become unbounded streams. -func (b *Binding) ReadWorkspaceFile(ctx context.Context, path string, limit int) (agent.WorkspaceReadResult, error) { - result := agent.WorkspaceReadResult{} +func (b *Binding) ReadWorkspaceFile(ctx context.Context, path string, limit int) (WorkspaceReadResult, error) { + result := WorkspaceReadResult{} if b == nil || ctx.Err() != nil { - return result, agent.ErrWorkspaceReadUnavailable + return result, ErrWorkspaceReadUnavailable } if path == "" || limit < 1 || limit > proto.WorkspaceReadMaxBytes { - return result, agent.ErrWorkspaceReadInvalid + return result, ErrWorkspaceReadInvalid } local, err := nativeAPIPath(path) if err != nil { - return result, agent.ErrWorkspaceReadInvalid + return result, ErrWorkspaceReadInvalid } root, err := os.OpenRoot(b.workspace) if err != nil { - return result, agent.ErrWorkspaceReadUnavailable + return result, ErrWorkspaceReadUnavailable } defer root.Close() info, err := root.Lstat(local) @@ -313,7 +308,7 @@ func (b *Binding) ReadWorkspaceFile(ctx context.Context, path string, limit int) return result, err } if !info.Mode().IsRegular() { - return result, agent.ErrWorkspaceReadInvalid + return result, ErrWorkspaceReadInvalid } file, err := openNativePath(root, local) if err != nil { @@ -322,14 +317,14 @@ func (b *Binding) ReadWorkspaceFile(ctx context.Context, path string, limit int) defer file.Close() info, err = file.Stat() if err != nil { - return result, agent.ErrWorkspaceReadUncertain + return result, ErrWorkspaceReadUncertain } if !info.Mode().IsRegular() { - return result, agent.ErrWorkspaceReadInvalid + return result, ErrWorkspaceReadInvalid } raw, err := io.ReadAll(io.LimitReader(nativeContextReader{ctx: ctx, reader: file}, int64(limit)+1)) if err != nil { - return result, agent.ErrWorkspaceReadUncertain + return result, ErrWorkspaceReadUncertain } result.Truncated = len(raw) > limit if result.Truncated { diff --git a/apps/daemon/internal/localworkspace/native_write_error_unix_test.go b/apps/daemon/internal/localworkspace/native_write_error_unix_test.go index 9c4b62370..20e89635a 100644 --- a/apps/daemon/internal/localworkspace/native_write_error_unix_test.go +++ b/apps/daemon/internal/localworkspace/native_write_error_unix_test.go @@ -8,20 +8,18 @@ import ( "os" "syscall" "testing" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" ) func TestNativeUnpublishedWriteStorageFailure(t *testing.T) { for _, cause := range []error{syscall.ENOSPC, syscall.EDQUOT} { for _, err := range []error{cause, &os.PathError{Op: "write", Path: "private/path", Err: cause}, fmt.Errorf("sync failed: %w", cause), errors.Join(syscall.EIO, cause)} { - if got := unpublishedWriteError(err); got != agent.ErrWorkspaceWriteUnavailable { + if got := unpublishedWriteError(err); got != ErrWorkspaceWriteUnavailable { t.Fatalf("storage exhaustion: got %v, want unavailable", got) } } } for _, cause := range []error{syscall.EIO, syscall.EACCES, syscall.EEXIST, syscall.ENOTDIR} { - if got := unpublishedWriteError(&os.PathError{Op: "write", Path: "private/path", Err: cause}); got != agent.ErrWorkspaceWriteRejected { + if got := unpublishedWriteError(&os.PathError{Op: "write", Path: "private/path", Err: cause}); got != ErrWorkspaceWriteRejected { t.Fatalf("changed ordinary rejection: %v", got) } } diff --git a/apps/daemon/internal/localworkspace/native_write_error_windows_test.go b/apps/daemon/internal/localworkspace/native_write_error_windows_test.go index c250a0cd3..bbcd5b8bd 100644 --- a/apps/daemon/internal/localworkspace/native_write_error_windows_test.go +++ b/apps/daemon/internal/localworkspace/native_write_error_windows_test.go @@ -4,17 +4,16 @@ import ( "os" "testing" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "golang.org/x/sys/windows" ) func TestNativeUnpublishedWriteStorageFailure(t *testing.T) { for _, cause := range []error{windows.ERROR_DISK_FULL, windows.ERROR_HANDLE_DISK_FULL, windows.ERROR_DISK_QUOTA_EXCEEDED} { - if got := unpublishedWriteError(&os.PathError{Op: "write", Path: "private/path", Err: cause}); got != agent.ErrWorkspaceWriteUnavailable { + if got := unpublishedWriteError(&os.PathError{Op: "write", Path: "private/path", Err: cause}); got != ErrWorkspaceWriteUnavailable { t.Fatalf("storage exhaustion: got %v, want unavailable", got) } } - if got := unpublishedWriteError(windows.ERROR_ACCESS_DENIED); got != agent.ErrWorkspaceWriteRejected { + if got := unpublishedWriteError(windows.ERROR_ACCESS_DENIED); got != ErrWorkspaceWriteRejected { t.Fatalf("changed ordinary rejection: %v", got) } } diff --git a/apps/daemon/internal/agent/workspace_write.go b/apps/daemon/internal/localworkspace/results.go similarity index 55% rename from apps/daemon/internal/agent/workspace_write.go rename to apps/daemon/internal/localworkspace/results.go index e584843f8..45639c64c 100644 --- a/apps/daemon/internal/agent/workspace_write.go +++ b/apps/daemon/internal/localworkspace/results.go @@ -1,16 +1,33 @@ -package agent +package localworkspace import ( "errors" "fmt" + + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" ) +type WorkspaceReadResult struct { + Data []byte + Truncated bool +} + type WorkspaceWriteResult struct { SizeBytes int64 } var ( - ErrWorkspaceWriteUnsupported = fmt.Errorf("%w: workspace write", ErrUnsupportedOperation) + ErrWorkspaceReadUnavailable = errors.New("workspace read unavailable") + ErrWorkspaceReadBusy = errors.New("workspace read busy") + ErrWorkspaceReadInvalid = errors.New("workspace read invalid") + ErrWorkspaceReadUncertain = errors.New("workspace read outcome uncertain") + // ErrWorkspaceNotDirectory reports that a directory request's own path is + // missing, a regular file or a symbolic link; the link was not followed. + ErrWorkspaceNotDirectory = errors.New("workspace path is not a directory") +) + +var ( + ErrWorkspaceWriteUnsupported = fmt.Errorf("%w: workspace write", agent.ErrUnsupportedOperation) ErrWorkspaceWriteUnavailable = errors.New("workspace write unavailable") ErrWorkspaceWriteBusy = errors.New("workspace write busy") ErrWorkspaceWriteInvalid = errors.New("workspace write invalid") diff --git a/apps/daemon/internal/localworkspace/write.go b/apps/daemon/internal/localworkspace/write.go index 69d91895d..1047ff497 100644 --- a/apps/daemon/internal/localworkspace/write.go +++ b/apps/daemon/internal/localworkspace/write.go @@ -6,7 +6,6 @@ import ( "io/fs" "strings" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) @@ -19,25 +18,25 @@ func (b *Binding) AcceptsFileWrite(environment, session string) bool { // WriteWorkspaceFile starts only after the caller supplies the complete bounded // body. Core must persist mutation ownership before invoking this operation. -func (b *Binding) WriteWorkspaceFile(ctx context.Context, path string, data []byte) (result agent.WorkspaceWriteResult, err error) { +func (b *Binding) WriteWorkspaceFile(ctx context.Context, path string, data []byte) (result WorkspaceWriteResult, err error) { if b == nil || b.writer == nil { - return result, agent.ErrWorkspaceWriteUnsupported + return result, ErrWorkspaceWriteUnsupported } if len(data) > WriteMaxBytes || len(path) > 4096 || path == "." || !fs.ValidPath(path) || strings.ContainsAny(path, "\\\x00\r\n") { - return result, agent.ErrWorkspaceWriteInvalid + return result, ErrWorkspaceWriteInvalid } if ctx.Err() != nil { - return result, agent.ErrWorkspaceWriteUnavailable + return result, ErrWorkspaceWriteUnavailable } w := b.writer if !w.mu.TryLock() { - return result, agent.ErrWorkspaceWriteBusy + return result, ErrWorkspaceWriteBusy } defer w.mu.Unlock() if w.uncertain { - return result, agent.ErrWorkspaceWriteUncertain + return result, ErrWorkspaceWriteUncertain } - defer func() { w.uncertain = errors.Is(err, agent.ErrWorkspaceWriteUncertain) }() + defer func() { w.uncertain = errors.Is(err, ErrWorkspaceWriteUncertain) }() // Once admitted, finish this synchronous mutation before returning ownership. return b.writeNativeFile(context.WithoutCancel(ctx), path, data) } diff --git a/apps/daemon/internal/localworkspace/write_test.go b/apps/daemon/internal/localworkspace/write_test.go index fe15e99f9..a7087340f 100644 --- a/apps/daemon/internal/localworkspace/write_test.go +++ b/apps/daemon/internal/localworkspace/write_test.go @@ -5,7 +5,6 @@ import ( "context" "errors" "fmt" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "os" "path/filepath" "testing" @@ -24,7 +23,7 @@ func TestNativeFileCreateAndNoReplace(t *testing.T) { if err != nil || got.SizeBytes != int64(size) { t.Fatal(size, got, err) } - if _, err = b.WriteWorkspaceFile(t.Context(), name, []byte("overwrite")); !errors.Is(err, agent.ErrWorkspaceWriteUnsafe) { + if _, err = b.WriteWorkspaceFile(t.Context(), name, []byte("overwrite")); !errors.Is(err, ErrWorkspaceWriteUnsafe) { t.Fatal("existing file replaced", err) } raw, err := os.ReadFile(filepath.Join(b.workspace, filepath.FromSlash(name))) @@ -32,7 +31,7 @@ func TestNativeFileCreateAndNoReplace(t *testing.T) { t.Fatal("file content changed", err) } } - if _, err := b.WriteWorkspaceFile(t.Context(), "nested", nil); !errors.Is(err, agent.ErrWorkspaceWriteDirectory) { + if _, err := b.WriteWorkspaceFile(t.Context(), "nested", nil); !errors.Is(err, ErrWorkspaceWriteDirectory) { t.Fatal(err) } if _, err := b.WriteWorkspaceFile(t.Context(), "after-rejection", nil); err != nil { @@ -51,16 +50,16 @@ func TestNativeFileCreateAndNoReplace(t *testing.T) { func TestNativeFileAdmission(t *testing.T) { b := nativeFileBinding(t) for _, path := range []string{"", ".", "..", "/etc/passwd", "a/../b", "a//b", "a\\b", "a\nb"} { - if _, err := b.WriteWorkspaceFile(t.Context(), path, nil); !errors.Is(err, agent.ErrWorkspaceWriteInvalid) { + if _, err := b.WriteWorkspaceFile(t.Context(), path, nil); !errors.Is(err, ErrWorkspaceWriteInvalid) { t.Fatal(path, err) } } - if _, err := b.WriteWorkspaceFile(t.Context(), "large", make([]byte, WriteMaxBytes+1)); !errors.Is(err, agent.ErrWorkspaceWriteInvalid) { + if _, err := b.WriteWorkspaceFile(t.Context(), "large", make([]byte, WriteMaxBytes+1)); !errors.Is(err, ErrWorkspaceWriteInvalid) { t.Fatal(err) } ctx, cancel := context.WithCancel(t.Context()) cancel() - if _, err := b.WriteWorkspaceFile(ctx, "cancelled", nil); !errors.Is(err, agent.ErrWorkspaceWriteUnavailable) { + if _, err := b.WriteWorkspaceFile(ctx, "cancelled", nil); !errors.Is(err, ErrWorkspaceWriteUnavailable) { t.Fatal(err) } if _, err := os.Stat(filepath.Join(b.workspace, "cancelled")); !os.IsNotExist(err) { @@ -69,11 +68,11 @@ func TestNativeFileAdmission(t *testing.T) { b.writer.mu.Lock() _, err := b.WriteWorkspaceFile(t.Context(), "busy", nil) b.writer.mu.Unlock() - if !errors.Is(err, agent.ErrWorkspaceWriteBusy) { + if !errors.Is(err, ErrWorkspaceWriteBusy) { t.Fatal(err) } b.writer.uncertain = true - if _, err = b.WriteWorkspaceFile(t.Context(), "uncertain", nil); !errors.Is(err, agent.ErrWorkspaceWriteUncertain) { + if _, err = b.WriteWorkspaceFile(t.Context(), "uncertain", nil); !errors.Is(err, ErrWorkspaceWriteUncertain) { t.Fatal(err) } } diff --git a/apps/daemon/internal/wireconformance/wire_test.go b/apps/daemon/internal/wireconformance/wire_test.go index 80b93e394..3fef719dc 100644 --- a/apps/daemon/internal/wireconformance/wire_test.go +++ b/apps/daemon/internal/wireconformance/wire_test.go @@ -363,6 +363,10 @@ func (turn *controlledTurn) AwaitSettlement(ctx context.Context) (agent.TurnSett } // These fixtures exercise settlement only; active input is deliberately rejected. +func (*controlledTurn) SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error { + return agent.ErrUnsupportedOperation +} + func (*controlledTurn) SteerWithReceipt(context.Context, proto.PromptSteerPayload, func()) error { return agent.ErrSteeringRejected } diff --git a/apps/daemon/testdata/onboarding/main.go b/apps/daemon/testdata/onboarding/main.go index 5f25d52af..59bc3e2ea 100644 --- a/apps/daemon/testdata/onboarding/main.go +++ b/apps/daemon/testdata/onboarding/main.go @@ -146,6 +146,11 @@ func (s *session) SteerWithReceipt(_ context.Context, p proto.PromptSteerPayload return nil } +// The fixture declares no function tools. +func (s *session) SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error { + return fmt.Errorf("%w: fixture has no function tools", agent.ErrUnsupportedOperation) +} + func (s *session) AwaitSettlement(ctx context.Context) (agent.TurnSettlement, error) { select { case <-s.settled: diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index ef49f3cb7..55a247b3b 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -8,7 +8,7 @@ A **Harness** is a native agent engine (Codex, Claude Code, MiniMax Code) that r | --- | --- | | Caller | The Runtime ([dispatch](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/apps/daemon/internal/dispatch)) | | Implementations | The Codex, Claude Code and MiniMax Code adapters ([Native references](#native-references)). [Harness capabilities](./harness-capabilities.md) records what each supports. | -| Code | [`agent/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/agent/harness.go): the execution lifecycle, the extension contracts and the registration methods. [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go): the `Declaration` every Harness provides, the registry of declarations and the shared model configuration contract. | +| Code | [`agent/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/agent/harness.go): the execution lifecycle, the extension contracts, the protocol errors and the registration methods. [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go): the `Declaration` every Harness provides, the registry of declarations and the shared model configuration contract. | | Transport | In-process Go interfaces. The Runtime speaks the [Core–Runtime protocol](../../docs/runtime-protocol.md) to Core. | | Version rule | A change to the interfaces edits this page and every adapter in one change. | @@ -50,7 +50,7 @@ Implement the mandatory text lifecycle and handle every extension explicitly. Qu ## Architecture rules [Complexity stays in the adapter](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/AGENTS.md#complexity-stays-in-the-adapter) and [Core assembles, Harnesses run natively](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/AGENTS.md#core-assembles-harnesses-run-natively) apply to every Harness. These rules are Harness-specific: -- Keep required lifecycle declarations, extension interfaces and registration methods in `agent/harness.go`. Result types, errors and Registry storage may stay in focused files. +- Keep required lifecycle declarations, extension interfaces and registration methods in `agent/harness.go`. Registry storage and helpers that are not protocol stay in focused files. - Use the existing `proto.SupportedAgentKind` and `AgentKindCapabilities` schema. Do not add a second capability descriptor or a combined optional interface. - Onboarding does not require feature equality. Harnesses need not match each other's optional features, and MCP, functions, images or verbosity control are not required to register. Verify the common lifecycle obligations and use the same public assertions for each declared operation. An omitted declaration or a missing extension implementation blocks onboarding; a native difference does not. - The declaration is the support boundary. An unregistered kind fails closed, and a Runtime heartbeat cannot authorize new public functionality or widen the declaration: discovery may only narrow it. Schema validity, declared support and the available Runtime are independent checks. @@ -58,7 +58,7 @@ Implement the mandatory text lifecycle and handle every extension explicitly. Qu ## Required adapter interfaces -[`agent/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/agent/harness.go) is the interface entry point. The required lifecycle is `ExecutorFactory`, `Executor`, `Turn` (including `DurableSteerer`) and `TurnSettlement`. Required methods perform their native obligations; returning Unsupported is not an implementation of cancellation, receipts, settlement or cleanup. Turn extension interfaces stay small and separate, but every public adapter implements each one explicitly. All use the neutral protocol types. +[`agent/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/agent/harness.go) is the interface entry point. The required lifecycle is `ExecutorFactory`, `Executor`, `Turn` (including `DurableSteerer` and `FunctionResultSubmitter`) and `TurnSettlement`. Required methods perform their native obligations; returning Unsupported is not an implementation of cancellation, receipts, settlement or cleanup. Turn extension interfaces stay small and separate, but every public adapter implements each one explicitly. All use the neutral protocol types. For example, the Codex adapter keeps its app-server and thread, the Claude adapter one streaming Query, and the MiniMax adapter its ACP connection and native session. All expose the same Executor and Turn contract. Native callbacks and resources stay inside the adapter; the Runtime owns admission, idle expiry and replacement. Cancellation targets the exact Turn through `agent.Session`, and the adapter supplies native completion evidence to the Runtime. @@ -261,7 +261,6 @@ Keep provider keys in private operator files, never in commits or logs. Existing | Claude input ownership, cancellation and preparation cleanup | `claudesdk/executor_test.go`, `cancellation_test.go`, `preparation_test.go` | | MiniMax cancellation retirement, failed Start and cleanup retry | `mcode/executor_test.go`, `executor_backpressure_test.go` | | MiniMax native history binding | `mcode/session_test.go` | -| Explicit refusals without native effects or fabricated results | Each adapter's `unsupported_test.go` | ## Native references diff --git a/contracts/agents-api/zh/harness-onboarding.md b/contracts/agents-api/zh/harness-onboarding.md index f9b3a18ed..1ea07e9ff 100644 --- a/contracts/agents-api/zh/harness-onboarding.md +++ b/contracts/agents-api/zh/harness-onboarding.md @@ -1,7 +1,7 @@ --- title: "Harness 协议" source: contracts/agents-api/harness-onboarding.md -source_hash: 35704f5aa719f0f151c6dde461ce5cf49863a517ef433b06829fd6fe6f9fe2bd +source_hash: 7658191e20060c3227a3604a5095383bcddded2ea71970d0bdde74421396b36d --- **Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code);**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。Harness 协议是 Runtime 与适配器之间的契约:适配器接口及其生命周期义务、声明、注册和验收。本文面向添加或维护 Harness 的开发者。 @@ -10,7 +10,7 @@ source_hash: 35704f5aa719f0f151c6dde461ce5cf49863a517ef433b06829fd6fe6f9fe2bd | --- | --- | | 调用方 | Runtime([dispatch](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/apps/daemon/internal/dispatch)) | | 实现方 | Codex、Claude Code 和 MiniMax Code 适配器([原生参考](#native-references))。[Harness capabilities](harness-capabilities.md) 记录了每个 Harness 支持的功能。 | -| 代码 | [`agent/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/agent/harness.go):执行生命周期、扩展契约和注册方法。[`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go):每个 Harness 提供的 `Declaration`、声明注册表以及共享模型配置契约。 | +| 代码 | [`agent/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/agent/harness.go):执行生命周期、扩展契约、协议错误和注册方法。[`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go):每个 Harness 提供的 `Declaration`、声明注册表以及共享模型配置契约。 | | 传输 | 进程内 Go 接口。Runtime 通过 [Core–Runtime 协议](../../../docs/zh/runtime-protocol.md)与 Core 通信。 | | 版本规则 | 接口变更须在同一次变更中修改本文和每个适配器。 | @@ -52,7 +52,7 @@ Environment 提供执行资源。受管 E2B、Docker 和 microsandbox 机器以 ## 架构规则 {#architecture-rules} [Complexity stays in the adapter](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/AGENTS.md#complexity-stays-in-the-adapter) 和 [Core assembles, Harnesses run natively](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/AGENTS.md#core-assembles-harnesses-run-natively) 适用于每个 Harness。以下规则为 Harness 专有: -- 将必需的生命周期声明、扩展接口和注册方法保留在 `agent/harness.go` 中。结果类型、错误和 Registry 存储可以保留在聚焦的文件中。 +- 将必需的生命周期声明、扩展接口和注册方法保留在 `agent/harness.go` 中。Registry 存储和非协议辅助代码保留在聚焦的文件中。 - 使用现有的 `proto.SupportedAgentKind` 和 `AgentKindCapabilities` schema。不要添加第二套能力描述符或组合式可选接口。 - 接入不要求功能完全一致。Harness 不必匹配彼此的可选功能,并且注册时不强制要求 MCP、函数、图像或详细程度控制。验证通用生命周期义务,并对每个声明的操作使用相同的公共断言。缺少声明或扩展实现会阻止接入;原生差异不会。 - 声明是支持边界。未注册的 kind 以关闭方式失败,Runtime 心跳无法授权新的公共功能,也无法扩大声明:发现过程只能收窄。Schema 有效性、声明的支持范围和可用 Runtime 是相互独立的检查。 @@ -60,7 +60,7 @@ Environment 提供执行资源。受管 E2B、Docker 和 microsandbox 机器以 ## 必需的适配器接口 {#required-adapter-interfaces} -[`agent/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/agent/harness.go) 是接口入口。必需的生命周期包括 `ExecutorFactory`、`Executor`、`Turn`(包括 `DurableSteerer`)和 `TurnSettlement`。必需方法必须履行其原生义务;返回 Unsupported 并不构成对取消、回执、结算或清理的实现。Turn 扩展接口应保持小而独立,但每个公共适配器都必须明确实现每一个接口。所有接口都使用中立协议类型。 +[`agent/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/agent/harness.go) 是接口入口。必需的生命周期包括 `ExecutorFactory`、`Executor`、`Turn`(包括 `DurableSteerer` 和 `FunctionResultSubmitter`)和 `TurnSettlement`。必需方法必须履行其原生义务;返回 Unsupported 并不构成对取消、回执、结算或清理的实现。Turn 扩展接口应保持小而独立,但每个公共适配器都必须明确实现每一个接口。所有接口都使用中立协议类型。 例如,Codex 适配器保留其 app-server 和 thread,Claude 适配器保留一个流式 Query,MiniMax 适配器保留其 ACP 连接和原生 session。它们都公开相同的 Executor 和 Turn 契约。原生回调和资源保留在适配器内部;Runtime 负责准入、空闲过期和替换。取消通过 `agent.Session` 精确定位到目标 Turn,适配器则向 Runtime 提供原生完成证据。 @@ -263,7 +263,6 @@ Environment 验收使用 `services/core/tests/official_environment_{templates,se | Claude 输入所有权、取消和准备清理 | `claudesdk/executor_test.go`、`cancellation_test.go`、`preparation_test.go` | | MiniMax 取消退役、Start 失败和清理重试 | `mcode/executor_test.go`、`executor_backpressure_test.go` | | MiniMax 原生历史绑定 | `mcode/session_test.go` | -| 无原生副作用或伪造结果的明确拒绝 | 每个适配器的 `unsupported_test.go` | ## 原生参考 {#native-references} diff --git a/docs/sandbox-provider.md b/docs/sandbox-provider.md index a46638a76..0d81fb3da 100644 --- a/docs/sandbox-provider.md +++ b/docs/sandbox-provider.md @@ -326,4 +326,4 @@ The node uses the explicit Unix socket in its [provider configuration](./configu ### Seccomp profile -[`seccomp.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/codex/seccomp.json) is the Moby default profile at [revision 65adc7e](https://github.com/moby/profiles/blob/65adc7e022c97f55e45c054ff012988027733b87/seccomp/default.json) (Apache-2.0, see [seccomp.LICENSE](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/codex/seccomp.LICENSE); upstream file SHA-256 `785b2429264afba4d594320337cb17f144f3c7d51585f9805eef72e28f4f9334`) with one appended rule that allows `clone`, `unshare`, `setns`, `mount`, `umount2` and `pivot_root`. The distribution ships this file to every Docker node as `runtime/seccomp.json`. +[`seccomp.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/docker/seccomp.json) is the Moby default profile at [revision 65adc7e](https://github.com/moby/profiles/blob/65adc7e022c97f55e45c054ff012988027733b87/seccomp/default.json) (Apache-2.0, see [seccomp.LICENSE](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/docker/seccomp.LICENSE); upstream file SHA-256 `785b2429264afba4d594320337cb17f144f3c7d51585f9805eef72e28f4f9334`) with one appended rule that allows `clone`, `unshare`, `setns`, `mount`, `umount2` and `pivot_root`. The distribution ships this file to every Docker node as `runtime/seccomp.json`. diff --git a/docs/zh/sandbox-provider.md b/docs/zh/sandbox-provider.md index 2aae07456..9a5b772dd 100644 --- a/docs/zh/sandbox-provider.md +++ b/docs/zh/sandbox-provider.md @@ -1,7 +1,7 @@ --- title: "沙箱提供商协议" source: docs/sandbox-provider.md -source_hash: 5cb1e7a7139fceaf0e9efb1833959b067d6a7b313484b1d6c9c652e3ab9191f5 +source_hash: 93958e2d8c2c983cd63b9a3b75ab2e29322100ba3f175622037dd5b2f6ff4c1b --- 沙箱提供商协议连接 Core 与为 Core 管理的(`openai_hosted`)Environment 提供计算资源的 adapter,并由该 adapter 运行启动 Runtime daemon 的有界引导流程。本页面向 adapter 作者:说明提供商必须实现什么,以及可以依赖 Core 做什么。Docker、microsandbox 和 E2B 实现了该协议。 @@ -328,4 +328,4 @@ node 使用 [提供商配置](configuration.md#docker-node-configuration)中的 ### Seccomp profile {#seccomp-profile} -[`seccomp.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/codex/seccomp.json) 是 [revision 65adc7e](https://github.com/moby/profiles/blob/65adc7e022c97f55e45c054ff012988027733b87/seccomp/default.json) 的 Moby default profile(Apache-2.0,参见 [seccomp.LICENSE](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/codex/seccomp.LICENSE);上游文件 SHA-256 为 `785b2429264afba4d594320337cb17f144f3c7d51585f9805eef72e28f4f9334`),追加一条允许 `clone`、`unshare`、`setns`、`mount`、`umount2` 和 `pivot_root` 的规则。发行包将此文件作为 `runtime/seccomp.json` 交付每个 Docker node。 +[`seccomp.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/docker/seccomp.json) 是 [revision 65adc7e](https://github.com/moby/profiles/blob/65adc7e022c97f55e45c054ff012988027733b87/seccomp/default.json) 的 Moby default profile(Apache-2.0,参见 [seccomp.LICENSE](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/docker/seccomp.LICENSE);上游文件 SHA-256 为 `785b2429264afba4d594320337cb17f144f3c7d51585f9805eef72e28f4f9334`),追加一条允许 `clone`、`unshare`、`setns`、`mount`、`umount2` 和 `pivot_root` 的规则。发行包将此文件作为 `runtime/seccomp.json` 交付每个 Docker node。 diff --git a/scripts/build-core-distribution.sh b/scripts/build-core-distribution.sh index 50a920b7a..a4b67eaee 100755 --- a/scripts/build-core-distribution.sh +++ b/scripts/build-core-distribution.sh @@ -111,7 +111,7 @@ python3 scripts/core-distribution-manifest.py bootstraps "$bundle" "$source_epoc # The bundled docs (BUNDLED_DOCS); links that leave them point at this commit on GitHub. python3 scripts/core-distribution-manifest.py docs . "$bundle" "$revision" mkdir -p "$bundle/runtime" -cp services/core/deploy/codex/seccomp.json "$bundle/runtime/" +cp services/core/internal/sandbox/docker/seccomp.json "$bundle/runtime/" mkdir -p "$bundle/storage" install -m 0755 services/core/internal/workspacefs/nfs/quota/oac-workspace-quota "$bundle/storage/" cp LICENSE "$bundle/" diff --git a/scripts/compose-smoke.py b/scripts/compose-smoke.py index 671b08e54..659f6dfc4 100644 --- a/scripts/compose-smoke.py +++ b/scripts/compose-smoke.py @@ -45,7 +45,7 @@ def prepare_payload(destination, revision, source_time): shutil.copyfile(path, sources / path.name) distribution.bootstraps(sources, source_time, revision) shutil.copyfile(sources / 'node-install.pyz', destination / 'node-install.pyz') - shutil.copyfile(ROOT / 'services/core/deploy/codex/seccomp.json', destination / 'runtime/seccomp.json') + shutil.copyfile(ROOT / 'services/core/internal/sandbox/docker/seccomp.json', destination / 'runtime/seccomp.json') (destination / 'manifest.json').write_text(json.dumps( {'source_commit': revision, 'platform': 'linux/amd64'})) distribution.checksums(destination) diff --git a/services/core/internal/deployment/service.go b/services/core/internal/deployment/service.go index 1cf87452a..9cb9da8d1 100644 --- a/services/core/internal/deployment/service.go +++ b/services/core/internal/deployment/service.go @@ -115,6 +115,9 @@ func (s *Service) setup(d Record) (Setup, error) { return result, err } +// DefaultIdleSeconds is the idle suspension policy a Provider selection starts with. +const DefaultIdleSeconds int64 = 300 + // describe adds the provider's declared operations, suspension policy and // credential use. func (s *Service) describe(setup Setup) (Setup, error) { @@ -123,7 +126,7 @@ func (s *Service) describe(setup Setup) (Setup, error) { return Setup{}, err } setup.Operations = adapter.Operations() - if setup.Suspension, err = s.suspension(setup.Provider, 300); err != nil { + if setup.Suspension, err = s.suspension(setup.Provider, DefaultIdleSeconds); err != nil { return Setup{}, err } setup.UsesCredential, err = s.registry.UsesCredential(setup.Provider) diff --git a/services/core/internal/sandbox/docker/provider_test.go b/services/core/internal/sandbox/docker/provider_test.go index 81c9ec95d..6a91295ae 100644 --- a/services/core/internal/sandbox/docker/provider_test.go +++ b/services/core/internal/sandbox/docker/provider_test.go @@ -43,7 +43,7 @@ func TestDockerProviderLifecycle(t *testing.T) { if image == "" { t.Skip("explicit Docker fixture image required") } - seccomp, e := os.ReadFile("../../../deploy/codex/seccomp.json") + seccomp, e := os.ReadFile("seccomp.json") if e != nil { t.Fatal(e) } diff --git a/services/core/internal/sandbox/docker/recovery_test.go b/services/core/internal/sandbox/docker/recovery_test.go index 11066f5b8..ebb888fdb 100644 --- a/services/core/internal/sandbox/docker/recovery_test.go +++ b/services/core/internal/sandbox/docker/recovery_test.go @@ -27,7 +27,7 @@ func TestDockerProviderRecoveryObservations(t *testing.T) { if image == "" { t.Skip("explicit Docker fixture image required") } - seccomp, err := os.ReadFile("../../../deploy/codex/seccomp.json") + seccomp, err := os.ReadFile("seccomp.json") if err != nil { t.Fatal(err) } diff --git a/services/core/deploy/codex/seccomp.LICENSE b/services/core/internal/sandbox/docker/seccomp.LICENSE similarity index 100% rename from services/core/deploy/codex/seccomp.LICENSE rename to services/core/internal/sandbox/docker/seccomp.LICENSE diff --git a/services/core/deploy/codex/seccomp.json b/services/core/internal/sandbox/docker/seccomp.json similarity index 100% rename from services/core/deploy/codex/seccomp.json rename to services/core/internal/sandbox/docker/seccomp.json diff --git a/services/core/internal/sandbox/node/docker_live_test.go b/services/core/internal/sandbox/node/docker_live_test.go index 7b2e33e1a..e9d2c3168 100644 --- a/services/core/internal/sandbox/node/docker_live_test.go +++ b/services/core/internal/sandbox/node/docker_live_test.go @@ -24,7 +24,7 @@ func TestDockerNodeTransportLifecycle(t *testing.T) { if image == "" { t.Skip("explicit Docker fixture image required") } - seccomp, err := os.ReadFile("../../../deploy/codex/seccomp.json") + seccomp, err := os.ReadFile("../docker/seccomp.json") if err != nil { t.Fatal(err) } diff --git a/services/core/migrations/000006_runtime_deployment_idle_default.sql b/services/core/migrations/000006_runtime_deployment_idle_default.sql new file mode 100644 index 000000000..09d580a1d --- /dev/null +++ b/services/core/migrations/000006_runtime_deployment_idle_default.sql @@ -0,0 +1,3 @@ +-- +goose Up +-- Core owns the default idle policy; the singleton row keeps its stored value. +ALTER TABLE runtime_deployment ALTER COLUMN idle_seconds DROP DEFAULT;