diff --git a/apps/daemon/internal/agent/claudesdk/declaration.go b/apps/daemon/internal/agent/claudesdk/declaration.go index 97caee159..c6d497577 100644 --- a/apps/daemon/internal/agent/claudesdk/declaration.go +++ b/apps/daemon/internal/agent/claudesdk/declaration.go @@ -85,7 +85,7 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, d for _, capability := range []*proto.CapabilitySupport{&caps.LocalEnvironment, &caps.WorkspaceReadPreparation, &caps.WorkspaceOutputExport, &caps.NativeSessionRecovery} { agent.Narrow(capability, local) } - agent.Narrow(&caps.RetainedNativeHistory, local && info.SDK == "0.3.269" && info.Native == "2.1.269 (Claude Code)") + agent.Narrow(&caps.RetainedNativeHistory, local && info.SDK == declaration.Declaration.Native.Version) agent.Narrow(&caps.FunctionTools, !local || info.SupportsWorkspaceFunctions()) agent.Narrow(&caps.MessageImages, info.SupportsMessageImages()) agent.Narrow(&caps.FunctionResultImages, info.SupportsFunctionResultImages()) diff --git a/apps/daemon/internal/agent/claudesdk/declaration_test.go b/apps/daemon/internal/agent/claudesdk/declaration_test.go index f5376c81b..1e6510eaa 100644 --- a/apps/daemon/internal/agent/claudesdk/declaration_test.go +++ b/apps/daemon/internal/agent/claudesdk/declaration_test.go @@ -12,6 +12,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/contracttest" + declaration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/claudesdk" ) func TestClaudeSDKInvalidPathsFailBeforeProbe(t *testing.T) { @@ -47,7 +48,7 @@ func TestClaudeSDKFeatureDiscovery(t *testing.T) { t.Setenv(claudeSDKNodeEnv, node) for _, features := range [][]string{nil, {"mcp_http_tools"}, {"mcp_http_bearer_auth"}, {"mcp_http_tools", "mcp_http_bearer_auth"}, {"mcp_http_required"}, {"mcp_http_tools", "mcp_http_required"}, {"subagent_resources"}, {"structured_output"}} { out := discoverWithCheck(t.Context(), agent.DiscoveryOptions{RuntimeRoot: root, StateRoot: t.TempDir(), Profile: "default", Stdout: &strings.Builder{}, Stderr: &strings.Builder{}}, Declaration.Info(), func(context.Context, Config) (RuntimeInfo, error) { - info := RuntimeInfo{SDK: "0.3.269", Native: "2.1.269 (Claude Code)", Features: features} + info := RuntimeInfo{SDK: declaration.Declaration.Native.Version, Native: "native", Features: features} return info, nil }) supported := len(features) > 0 && features[0] == "mcp_http_tools" @@ -75,7 +76,7 @@ func TestDiscoveryOnlyNarrowsDeclaration(t *testing.T) { t.Setenv(claudeSDKNodeEnv, node) features := []string{"message_images", "function_result_images", "tool_search", "structured_output", "subagent_resources", "mcp_http_tools", "mcp_http_bearer_auth", "mcp_http_required"} out := discoverWithCheck(t.Context(), agent.DiscoveryOptions{RuntimeRoot: root, StateRoot: t.TempDir(), Profile: "default", Stdout: io.Discard, Stderr: io.Discard}, Declaration.Info(), func(context.Context, Config) (RuntimeInfo, error) { - return RuntimeInfo{SDK: "0.3.269", Native: "2.1.269 (Claude Code)", Features: features}, nil + return RuntimeInfo{SDK: declaration.Declaration.Native.Version, Native: "native", Features: features}, nil }) if out == nil || !out.Info.Available { t.Fatal("runtime unavailable") diff --git a/apps/daemon/internal/agent/claudesdk/executor_fixture_test.go b/apps/daemon/internal/agent/claudesdk/executor_fixture_test.go index 15d683ccf..e6ad19996 100644 --- a/apps/daemon/internal/agent/claudesdk/executor_fixture_test.go +++ b/apps/daemon/internal/agent/claudesdk/executor_fixture_test.go @@ -36,11 +36,11 @@ func helperTurn(scanner *bufio.Scanner, request *startRequest) (func(bridgeEvent os.Exit(4) } var start struct { - Type string `json:"type"` - TurnID string `json:"turn_id"` - Input json.RawMessage `json:"input"` + Type string `json:"type"` + TurnID string `json:"turn_id"` + Input proto.MessageInput `json:"input"` } - if json.Unmarshal(scanner.Bytes(), &start) != nil || start.Type != "turn_start" || start.TurnID == "" || json.Unmarshal(start.Input, &request.Input) != nil { + if json.Unmarshal(scanner.Bytes(), &start) != nil || start.Type != "turn_start" || start.TurnID == "" { os.Exit(5) } return helperTurnOutput(scanner, start.TurnID) diff --git a/apps/daemon/internal/agent/claudesdk/installation.go b/apps/daemon/internal/agent/claudesdk/installation.go index 50e280368..c4b5fa196 100644 --- a/apps/daemon/internal/agent/claudesdk/installation.go +++ b/apps/daemon/internal/agent/claudesdk/installation.go @@ -10,9 +10,9 @@ import ( ) func Installation() agent.Installation { - return agent.Installation{AgentKind: "claude_sdk", Version: declaration.Declaration.Native.Version, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows" }, + return agent.Installation{AgentKind: declaration.Declaration.Kind, Version: declaration.Declaration.Native.Version, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows" }, Environment: func(dir, node string) map[string]string { - return map[string]string{"OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT": filepath.Join(dir, "dist", "main.js"), "OAC_RUNTIME_CLAUDE_SDK_NODE": node} + return map[string]string{claudeSDKEntrypointEnv: filepath.Join(dir, "dist", "main.js"), claudeSDKNodeEnv: node} }, Check: func(ctx context.Context, dir, node string, env []string) error { got, err := CheckRuntime(ctx, Config{Node: node, Entrypoint: filepath.Join(dir, "dist", "main.js"), Env: env}) diff --git a/apps/daemon/internal/agent/claudesdk/options.go b/apps/daemon/internal/agent/claudesdk/options.go index 696006e8b..d82cfd9d1 100644 --- a/apps/daemon/internal/agent/claudesdk/options.go +++ b/apps/daemon/internal/agent/claudesdk/options.go @@ -30,7 +30,6 @@ type startRequest struct { Subagents *subagentOptions `json:"subagents,omitempty"` OutputFormat *proto.OutputFormat `json:"output_format,omitempty"` Type string `json:"type"` - Input proto.MessageInput `json:"input,omitempty"` Model string `json:"model"` SystemPrompt string `json:"system_prompt"` Cwd string `json:"cwd"` @@ -42,7 +41,7 @@ type startRequest struct { } func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startRequest, []string, error) { - start := startRequest{Type: "start", Resume: req.AgentSessionID, RequireHistory: req.RequireExistingNativeSession, Functions: req.FunctionTools} + start := startRequest{Resume: req.AgentSessionID, RequireHistory: req.RequireExistingNativeSession, Functions: req.FunctionTools} fail := func(reason string) (startRequest, []string, error) { return startRequest{}, nil, fmt.Errorf("claudesdk: %s", reason) } diff --git a/apps/daemon/internal/agent/claudesdk/preparation_test.go b/apps/daemon/internal/agent/claudesdk/preparation_test.go index 8040a0b4d..27a4436ad 100644 --- a/apps/daemon/internal/agent/claudesdk/preparation_test.go +++ b/apps/daemon/internal/agent/claudesdk/preparation_test.go @@ -60,7 +60,7 @@ func TestPreparationWaitsForReceiptAndRetainsConfiguration(t *testing.T) { if err := json.Unmarshal(raw, &frozen); err != nil { t.Fatal(err) } - if frozen.Model != "fixture" || frozen.Resume != "native-session" || frozen.Workspace == nil || len(frozen.Input) != 0 { + if frozen.Model != "fixture" || frozen.Resume != "native-session" || frozen.Workspace == nil { t.Fatal("configuration-only request was not retained") } config.Env[0] = "ANTHROPIC_AUTH_TOKEN=changed" diff --git a/apps/daemon/internal/agent/codex/installation.go b/apps/daemon/internal/agent/codex/installation.go index b9540a2d0..70b2e1d09 100644 --- a/apps/daemon/internal/agent/codex/installation.go +++ b/apps/daemon/internal/agent/codex/installation.go @@ -21,7 +21,7 @@ func Installation() agent.Installation { } return filepath.Join(dir, "bin", name) } - return agent.Installation{AgentKind: "codex", Version: declaration.Declaration.Native.Version, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows" }, + return agent.Installation{AgentKind: declaration.Declaration.Kind, Version: declaration.Declaration.Native.Version, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows" }, Environment: func(dir, node string) map[string]string { return map[string]string{binaryEnv: binary(dir)} }, diff --git a/apps/daemon/internal/agent/mcode/discovery_workspace.go b/apps/daemon/internal/agent/mcode/discovery_workspace.go index 932164d8d..b213c9054 100644 --- a/apps/daemon/internal/agent/mcode/discovery_workspace.go +++ b/apps/daemon/internal/agent/mcode/discovery_workspace.go @@ -46,7 +46,7 @@ func discoverWorkspace(parent context.Context, options agent.DiscoveryOptions, r fail(err) return nil } - c, err := ConfigureLocal(binary, node, os.Getenv("OAC_RUNTIME_MCODE_COMPANION"), binding.Workspace()) + c, err := ConfigureLocal(binary, node, os.Getenv(companionEnv), binding.Workspace()) if err == nil { err = CheckWorkspace(parent, c) } diff --git a/apps/daemon/internal/agent/mcode/installation.go b/apps/daemon/internal/agent/mcode/installation.go index 385114a4c..d6af008a0 100644 --- a/apps/daemon/internal/agent/mcode/installation.go +++ b/apps/daemon/internal/agent/mcode/installation.go @@ -5,14 +5,15 @@ import ( "fmt" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/installroot" + declaration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/mcode" "path/filepath" "runtime" ) func Installation() agent.Installation { - return agent.Installation{AgentKind: "mcode", Version: SupportedVersion, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" }, + return agent.Installation{AgentKind: declaration.Declaration.Kind, Version: SupportedVersion, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" }, Environment: func(dir, node string) map[string]string { - return map[string]string{binaryEnv: filepath.Join(dir, "native", "cli.js"), nodeEnv: node, "OAC_RUNTIME_MCODE_COMPANION": dir} + return map[string]string{binaryEnv: filepath.Join(dir, "native", "cli.js"), nodeEnv: node, companionEnv: dir} }, Check: func(ctx context.Context, dir, node string, env []string) error { got, err := installroot.Probe(ctx, node, []string{filepath.Join(dir, "native", "cli.js"), "--version"}, env, dir) diff --git a/apps/daemon/internal/agent/mcode/subagents.go b/apps/daemon/internal/agent/mcode/subagents.go index 2bdf26040..61f6bc179 100644 --- a/apps/daemon/internal/agent/mcode/subagents.go +++ b/apps/daemon/internal/agent/mcode/subagents.go @@ -52,13 +52,14 @@ type nativeSubagentTask struct { } const nodeEnv = "OAC_RUNTIME_MCODE_NODE" +const companionEnv = "OAC_RUNTIME_MCODE_COMPANION" // runtimeNode is the Node executable the installation or image provides for the companion scripts. func runtimeNode() string { return os.Getenv(nodeEnv) } func subagentReader() (string, string, error) { node := runtimeNode() - reader := filepath.Join(os.Getenv("OAC_RUNTIME_MCODE_COMPANION"), "subagent-snapshot.mjs") + reader := filepath.Join(os.Getenv(companionEnv), "subagent-snapshot.mjs") for _, path := range []string{node, reader} { resolved, err := filepath.EvalSymlinks(path) if err != nil || !filepath.IsAbs(path) || resolved != path { diff --git a/contracts/agents-api/core.openapi.yaml b/contracts/agents-api/core.openapi.yaml index 0ff190fa2..e0d305ff2 100644 --- a/contracts/agents-api/core.openapi.yaml +++ b/contracts/agents-api/core.openapi.yaml @@ -1351,7 +1351,7 @@ definitions: compute_phase: type: string compute_phase_changed_at: - description: The time the allocation entered its current compute_phase, or null when unknown; an allocation that existed before Core recorded it reports null until its next phase change. For a suspended microsandbox allocation, this time plus the deployment's snapshot retention tells roughly when Core reclaims it. + description: The time the allocation entered its current compute_phase, or null when unknown; an allocation that existed before Core recorded it reports null until its next phase change. For a suspended allocation, this time plus the deployment's snapshot retention tells roughly when Core reclaims it. type: string x-nullable: true created_at: @@ -1545,7 +1545,7 @@ definitions: max_active: type: integer max_retained: - description: Docker never suspends, so Core replaces this with max_active; microsandbox uses both limits. + description: A Provider that does not checkpoint never suspends, so Core replaces this with max_active; a Provider that checkpoints uses both limits. type: integer name: type: string diff --git a/packages/claude-sdk-adapter/tests/executor.test.mjs b/packages/claude-sdk-adapter/tests/executor.test.mjs index a10f30b6c..199aebe5c 100644 --- a/packages/claude-sdk-adapter/tests/executor.test.mjs +++ b/packages/claude-sdk-adapter/tests/executor.test.mjs @@ -20,7 +20,7 @@ globalThis.startupFixture=async({options})=>{ const [a,b]=InMemoryTransport.createLinkedPair(); await Promise.all([client.connect(a),options.mcpServers.functions.instance.connect(b)]); } - let queried=false,interrupt,number=0; + let queried=false,interrupt,number=0,later; const close=()=>{child.stdin.end();interrupt?.()}; options.abortController.signal.addEventListener('abort',close); return {close,query(prompt){assert.equal(queried,false);queried=true;process.send({type:'query'});return { @@ -32,6 +32,18 @@ globalThis.startupFixture=async({options})=>{ const text=user.message.content[0].text; process.send({type:'input',text}); yield {type:'system',subtype:'init',session_id:'native',tools:client ? ['mcp__functions__lookup'] : [],mcp_servers:client ? [{name:'functions',status:'connected'}] : []}; + if(process.argv[1]==='later-function'){ + // The call is dispatched independently of the result stream and belongs to the next native turn. + if(number===1){ + later=client.callTool({name:'lookup',arguments:{text},_meta:{'claudecode/toolUseId':'later-call'}}); + await new Promise(resolve=>process.once('message',resolve)); + }else{ + const called=await later; + yield {type:'user',session_id:'native',parent_tool_use_id:null,message:{content:[{type:'tool_result',tool_use_id:'later-call',content:called.content,is_error:called.isError}]}}; + } + yield {type:'result',uuid:'result-'+number,session_id:'native',user_message_uuids:[user.uuid],subtype:'success',is_error:false,result:'answer-'+number,usage:{input_tokens:1,output_tokens:1},modelUsage:{},total_cost_usd:0}; + continue; + } if(client){ if(text==='pending-function'){ void client.callTool({name:'lookup',arguments:{text},_meta:{'claudecode/toolUseId':'pending-call'}}).catch(()=>{}); @@ -79,7 +91,7 @@ async function launch(t,mode="normal") { const send=value=>child.stdin.write(JSON.stringify(value)+"\n"); const start=(id,text)=>send({type:"turn_start",turn_id:id,input:[{content:[{type:"input_text",text}]}]}); send({type:"executor_prepare",cwd:"/tmp",model:"fixture",system_prompt:"", - ...(mode==="features" || mode.startsWith("pending-function") ? {functions:[{name:"lookup",description:"lookup",parameters:{type:"object",properties:{text:{type:"string"}}}}]} : {})}); + ...(mode==="features" || mode==="later-function" || mode.startsWith("pending-function") ? {functions:[{name:"lookup",description:"lookup",parameters:{type:"object",properties:{text:{type:"string"}}}}]} : {})}); await wait(()=>events.some(event=>event.type==="executor_ready")); assert.equal(observations.filter(event=>event.type==="input").length,0); return {child,events,observations,closed,wait,send,start}; @@ -203,3 +215,18 @@ test("executor preparation reports missing native history before readiness",{tim assert.equal(await closed,0); assert.deepEqual(stdout.trim().split("\n").map(line=>JSON.parse(line)),[{type:"error",code:"history_unavailable"}]); }); + +test("a function call outstanding across a native result is applied by the next native turn of the same Turn", {timeout:10000}, async t => { + const {child,events,closed,wait,send,start}=await launch(t,"later-function"); + start("only","later-function"); + await wait(()=>events.some(event=>event.type==="function_call")); + send({type:"steer",turn_id:"only",input_id:"extra",input:[{content:[{type:"input_text",text:"later text"}]}]}); + child.send({type:"go"}); + await wait(()=>events.filter(event=>event.type==="usage").length===1); + send({type:"function_result",turn_id:"only",call_id:"later-call",delivery_id:"delivery",success:true,content:[{type:"input_text",text:"value"}]}); + await wait(()=>events.some(event=>event.type==="turn_settled")); + assert.equal(events.filter(event=>event.type==="function_applied").length,1); + assert.equal(events.filter(event=>event.type==="usage").length,2); + assert.equal(events.find(event=>event.type==="turn_settled").confirmed,true); + child.stdin.end();assert.deepEqual(await closed,{code:0,signal:null}); +}); diff --git a/scripts/build-native-installer.mjs b/scripts/build-native-installer.mjs index 07c94d775..474d73a34 100644 --- a/scripts/build-native-installer.mjs +++ b/scripts/build-native-installer.mjs @@ -126,7 +126,7 @@ export async function validateComponents(root, names, daemonVersion) { await requireFile(join(component, 'pnpm-lock.yaml')); await requireFile(join(component, 'dist/main.js')); const info = JSON.parse(probe(node, [join(component, 'dist/runtime_check.js')], component, env)); - if (info.type !== 'runtime_ready' || info.protocol !== 3 || info.sdk !== pins.claude || info.native !== '2.1.269 (Claude Code)' || !info.features?.includes('local_runtime_v2')) throw new Error('Claude runtime compatibility failed'); + if (info.type !== 'runtime_ready' || info.protocol !== 3 || info.sdk !== pins.claude || !info.features?.includes('local_runtime_v2')) throw new Error('Claude runtime compatibility failed'); } else if (name === 'minimax') { if (windows) throw new Error('MiniMax is not supported on Windows'); const upstream = await json(join(component, 'source.json')); diff --git a/scripts/generate-harness-catalog.py b/scripts/generate-harness-catalog.py index fb7a9047b..3b92174f0 100644 --- a/scripts/generate-harness-catalog.py +++ b/scripts/generate-harness-catalog.py @@ -138,6 +138,7 @@ def render_docs(harnesses): "claude": { "scripts/native-harness-smoke.mjs": ["!== '{v}'", "info.sdk !== '{v}'"], "packages/claude-sdk-adapter/package.json": ['"@anthropic-ai/claude-agent-sdk": "{v}"'], + "services/core/deploy/claude/README.md": ["Agent SDK `{v}`"], }, "minimax": { "scripts/build-mcode-runtime.sh": ["= {v}\n"], diff --git a/scripts/native-harness-smoke.mjs b/scripts/native-harness-smoke.mjs index 1a87d969b..7d0699f19 100644 --- a/scripts/native-harness-smoke.mjs +++ b/scripts/native-harness-smoke.mjs @@ -88,7 +88,7 @@ async function claudeSmoke() { if (result.error || result.status !== 0) throw failure('claude_runtime_unavailable'); let info; try { info = JSON.parse(result.stdout); } catch { throw failure('claude_invalid_receipt'); } - if (info.type !== 'runtime_ready' || info.sdk !== '0.3.269' || info.native !== '2.1.269 (Claude Code)' || !info.features?.includes('local_runtime_v2')) throw failure('claude_runtime_identity'); + if (info.type !== 'runtime_ready' || info.sdk !== '0.3.269' || !info.features?.includes('local_runtime_v2')) throw failure('claude_runtime_identity'); return { status: 'passed', sdk: info.sdk, native: info.native }; } diff --git a/services/core/deploy/claude/README.md b/services/core/deploy/claude/README.md index acc298cc5..2d7b89d95 100644 --- a/services/core/deploy/claude/README.md +++ b/services/core/deploy/claude/README.md @@ -6,7 +6,7 @@ Native tools run with the daemon user's permissions; the outer sandbox provides ## Native pin and readiness -The bundle pins Claude Agent SDK `0.3.269` ([`package.json`](../../../../packages/claude-sdk-adapter/package.json)), which reports native Claude Code `2.1.269`. The bridge uses protocol 3 for a prepared Executor with separately identified Turns; the Go adapter's readiness and Executor checks reject any other protocol version ([`readiness.go`](../../../../apps/daemon/internal/agent/claudesdk/readiness.go), [`executor.go`](../../../../apps/daemon/internal/agent/claudesdk/executor.go)). +The bundle pins Claude Agent SDK `0.3.269` ([`package.json`](../../../../packages/claude-sdk-adapter/package.json)), which fixes the native Claude Code it bundles. The bridge uses protocol 3 for a prepared Executor with separately identified Turns; the Go adapter's readiness and Executor checks reject any other protocol version ([`readiness.go`](../../../../apps/daemon/internal/agent/claudesdk/readiness.go), [`executor.go`](../../../../apps/daemon/internal/agent/claudesdk/executor.go)). Workspace execution requires the bundle to report the `workspace_tools`, `workspace_prepare`, `workspace_command_observations` and `local_runtime_v2` features; matching SDK versions alone do not establish compatibility. Function tools in workspace execution additionally require `workspace_functions`. The native installer rejects a bundle that lacks the workspace features ([`installation.go`](../../../../apps/daemon/internal/agent/claudesdk/installation.go)). @@ -33,7 +33,7 @@ The bridge ([`native_failure.ts`](../../../../packages/claude-sdk-adapter/src/na | `server_error` right after an `api_retry` whose `error_status` is null, with no model output between them | `connection_failed` | | any other `server_error` | `server_error` | -Claude Code `2.1.269` reports both a request that got no HTTP response, such as a failed DNS lookup, a refused connection or a timeout, and a server error in a response that already started streaming as `server_error` without a status. Only the first comes right after an `api_retry` whose `error_status` is null, with no model output in between. Claude Code retries such a request up to 10 times, with backoff that grows to about 35 seconds, so a model host that does not resolve or refuses connections fails the Turn after about three minutes. The adapter leaves that bound as it is. +The pinned Claude Code reports both a request that got no HTTP response, such as a failed DNS lookup, a refused connection or a timeout, and a server error in a response that already started streaming as `server_error` without a status. Only the first comes right after an `api_retry` whose `error_status` is null, with no model output in between. Claude Code retries such a request up to 10 times, with backoff that grows to about 35 seconds, so a model host that does not resolve or refuses connections fails the Turn after about three minutes. The adapter leaves that bound as it is. `unknown`, `max_output_tokens` and other results stay unclassified. The bridge drops the classification when any other error ends the Turn, when cleanup fails or when no native process ran. The Go adapter keeps it only for the reported failed result of the same native Session, and only after it received the Turn settlement ([`executor_turn.go`](../../../../apps/daemon/internal/agent/claudesdk/executor_turn.go)). diff --git a/services/core/deploy/e2b/README.md b/services/core/deploy/e2b/README.md index 70a5a6aa8..93a7d6c55 100644 --- a/services/core/deploy/e2b/README.md +++ b/services/core/deploy/e2b/README.md @@ -80,7 +80,7 @@ Neither record proves enrollment, native readiness or a successful Turn; check t ## Startup and security boundary -`init.py` runs once as root. It restores the ownership and modes that E2B finalization changes under `/usr/local` and on `envd`, `/etc/inittab` and `/etc/init.d/rcS`, locks E2B's passwordless `user` account, bind-mounts `/environment/workspace` at `/workspace`. It writes the executor key to `/home/runtime/.oac/daemon/executor-key.json` (mode 0600, owned by UID 1000), deletes the startup input and starts `oac-daemon connect --profile default --remote … --environment-id … --credential-file …` as UID/GID 1000. No credential enters the daemon's arguments or inherited environment. The daemon owns enrollment and the local binding. E2B clears `/run` at boot, so the records live under `/root/.oac/e2b`. +`init.py` runs once as root. It restores the ownership and modes that E2B finalization changes under `/usr/local` and on `envd`, `/etc/inittab` and `/etc/init.d/rcS`, locks E2B's passwordless `user` account and bind-mounts `/environment/workspace` at `/workspace`. It writes the executor key to `/home/runtime/.oac/daemon/executor-key.json` (mode 0600, owned by UID 1000), deletes the startup input and starts `oac-daemon connect --profile default --remote … --environment-id … --workspace /environment/workspace --credential-file …` as UID/GID 1000. No credential enters the daemon's arguments or inherited environment. The daemon owns enrollment and the local binding. E2B clears `/run` at boot, so the records live under `/root/.oac/e2b`. The E2B VM is the isolation boundary; tools have UID 1000's access to Runtime state ([Runtime and outer isolation](../../../../docs/concepts.md#runtime-and-outer-isolation)). diff --git a/services/core/internal/deployment/allocation.go b/services/core/internal/deployment/allocation.go index 2095fa67c..5abde50d6 100644 --- a/services/core/internal/deployment/allocation.go +++ b/services/core/internal/deployment/allocation.go @@ -159,7 +159,7 @@ type NodeAllocation struct { EnvironmentID string `json:"environment_id" binding:"required"` State string `json:"state" binding:"required"` ComputePhase string `json:"compute_phase" binding:"required"` - // The time the allocation entered its current compute_phase, or null when unknown; an allocation that existed before Core recorded it reports null until its next phase change. For a suspended microsandbox allocation, this time plus the deployment's snapshot retention tells roughly when Core reclaims it. + // The time the allocation entered its current compute_phase, or null when unknown; an allocation that existed before Core recorded it reports null until its next phase change. For a suspended allocation, this time plus the deployment's snapshot retention tells roughly when Core reclaims it. ComputePhaseChangedAt *time.Time `json:"compute_phase_changed_at" extensions:"x-nullable" binding:"required"` Initialization string `json:"initialization" binding:"required"` CreatedAt time.Time `json:"created_at" binding:"required"` diff --git a/services/core/internal/deployment/node.go b/services/core/internal/deployment/node.go index 154d364f7..9dcbe7cf9 100644 --- a/services/core/internal/deployment/node.go +++ b/services/core/internal/deployment/node.go @@ -70,7 +70,7 @@ type Node struct { type NodeUpdate struct { Name string `json:"name" binding:"required"` MaxActive int `json:"max_active" binding:"required"` - // Docker never suspends, so Core replaces this with max_active; microsandbox uses both limits. + // A Provider that does not checkpoint never suspends, so Core replaces this with max_active; a Provider that checkpoints uses both limits. MaxRetained int `json:"max_retained"` } diff --git a/services/core/internal/deployment/service.go b/services/core/internal/deployment/service.go index 9cb9da8d1..2d4a2648b 100644 --- a/services/core/internal/deployment/service.go +++ b/services/core/internal/deployment/service.go @@ -115,18 +115,16 @@ 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. +// credential use. The idle policy lives in the stored deployment row, so a +// setup that is not read from it leaves IdleSeconds zero. func (s *Service) describe(setup Setup) (Setup, error) { adapter, err := s.registry.Lookup(setup.Provider) if err != nil { return Setup{}, err } setup.Operations = adapter.Operations() - if setup.Suspension, err = s.suspension(setup.Provider, DefaultIdleSeconds); err != nil { + if setup.Suspension, err = s.suspension(setup.Provider, 0); err != nil { return Setup{}, err } setup.UsesCredential, err = s.registry.UsesCredential(setup.Provider) diff --git a/services/core/internal/execution/controls_test.go b/services/core/internal/execution/controls_test.go index 7bfc57455..bbdcf0e15 100644 --- a/services/core/internal/execution/controls_test.go +++ b/services/core/internal/execution/controls_test.go @@ -10,7 +10,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/harnesstest" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -86,7 +85,7 @@ func TestExecutionControlsCarryOnlyOfficialValues(t *testing.T) { snapshot := Snapshot{Agent: tc.agent} before, _ := json.Marshal(snapshot) for _, nativeID := range []string{"", "native-session"} { - request, err := (&Dispatcher{}).executionRequest(t.Context(), sessions.Session{ID: "session"}, snapshot, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{NativeSessionID: nativeID}) + request, err := (&Dispatcher{}).executionRequest(t.Context(), sessions.Session{ID: "session"}, snapshot, proto.AgentKindCapabilities{}, sessions.ExecutionBinding{NativeSessionID: nativeID}) if err != nil || !reflect.DeepEqual(*request.ExecutionControls, tc.want) || request.AgentSessionID != nativeID { t.Fatalf("%+v %v", request.ExecutionControls, err) } diff --git a/services/core/internal/execution/environment_directory.go b/services/core/internal/execution/environment_directory.go index 34a533197..5be347d7b 100644 --- a/services/core/internal/execution/environment_directory.go +++ b/services/core/internal/execution/environment_directory.go @@ -129,8 +129,8 @@ func (w *Worker) directoryDeviceReady(ctx context.Context, id, engine string, pl return false } info, found, known := peer.AgentKindStatus(engine) - placementReady := info.Capabilities.LocalEnvironment - return known && found && info.Available && placementReady && (!prepare || (info.Capabilities.Preparation && info.Capabilities.WorkspaceReadPreparation)) + placementReady := info.Capabilities.LocalEnvironment.IsSupported() + return known && found && info.Available && placementReady && (!prepare || (info.Capabilities.Preparation.IsSupported() && info.Capabilities.WorkspaceReadPreparation.IsSupported())) } func readEnvironmentDirectory(ctx context.Context, peer *runtimegateway.Session, request proto.WorkspaceReadPayload) directoryReadResult { diff --git a/services/core/internal/execution/environment_file_write.go b/services/core/internal/execution/environment_file_write.go index 28546d452..2053a697e 100644 --- a/services/core/internal/execution/environment_file_write.go +++ b/services/core/internal/execution/environment_file_write.go @@ -102,7 +102,7 @@ func (w *Worker) runFileWrite(owner context.Context, request fileWriteRequest) f return unavailable } info, found, known := peer.AgentKindStatus(session.Engine) - if !known || !found || !info.Available || !info.Capabilities.LocalEnvironment { + if !known || !found || !info.Available || !info.Capabilities.LocalEnvironment.IsSupported() { return unavailable } dataDigest := sha256.Sum256(request.data) diff --git a/services/core/internal/execution/functions.go b/services/core/internal/execution/functions.go index 19ef8d43d..8ae50acc6 100644 --- a/services/core/internal/execution/functions.go +++ b/services/core/internal/execution/functions.go @@ -208,7 +208,7 @@ func requireFunctionResultImages(peer *runtimegateway.Session, kind string, resu return nil } info, found, known := peer.AgentKindStatus(kind) - if !found || !known || !info.Available || !info.Capabilities.FunctionResultImages { + if !found || !known || !info.Available || !info.Capabilities.FunctionResultImages.IsSupported() { return errors.New("Runtime does not support function result images") } return nil diff --git a/services/core/internal/execution/mcp_support.go b/services/core/internal/execution/mcp_support.go index 1e7910a6c..b8e6069fd 100644 --- a/services/core/internal/execution/mcp_support.go +++ b/services/core/internal/execution/mcp_support.go @@ -4,7 +4,6 @@ import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" ) @@ -22,7 +21,7 @@ func (p Policy) mcpCredentialBindings(engine string, snapshot Snapshot) (map[str // Selection, final preclaim and request construction use the same combination // checks. This function never reads plaintext credentials or native configuration. -func (p Policy) mcpExecutionCredentials(engine string, snapshot Snapshot, servers []proto.MCPHTTPServer, caps runtimedevice.KindCapabilities) (map[string]vaults.MCPCredentialBinding, error) { +func (p Policy) mcpExecutionCredentials(engine string, snapshot Snapshot, servers []proto.MCPHTTPServer, caps proto.AgentKindCapabilities) (map[string]vaults.MCPCredentialBinding, error) { fail := func(message string) (map[string]vaults.MCPCredentialBinding, error) { return nil, errors.New(message) } @@ -30,7 +29,7 @@ func (p Policy) mcpExecutionCredentials(engine string, snapshot Snapshot, server if err := declaration.ValidateMCPOrigins(snapshot.Environment.Type, servers); err != nil { return nil, err } - if len(servers) > 0 && !caps.MCPHTTPTools { + if len(servers) > 0 && !caps.MCPHTTPTools.IsSupported() { return fail("device must advertise mcp_http_tools") } selected, err := p.mcpCredentialBindings(engine, snapshot) @@ -38,14 +37,14 @@ func (p Policy) mcpExecutionCredentials(engine string, snapshot Snapshot, server return nil, err } for _, server := range servers { - if server.Required && !caps.MCPHTTPRequired { + if server.Required && !caps.MCPHTTPRequired.IsSupported() { return fail("device must advertise mcp_http_required") } } - if len(selected) > 0 && !caps.MCPHTTPBearerAuth { + if len(selected) > 0 && !caps.MCPHTTPBearerAuth.IsSupported() { return fail("device must advertise mcp_http_bearer_auth") } - if len(servers) > 0 && snapshot.Environment.Type == "none" && !caps.EnvironmentNone { + if len(servers) > 0 && snapshot.Environment.Type == "none" && !caps.EnvironmentNone.IsSupported() { return fail("device must advertise environment_none") } return selected, nil diff --git a/services/core/internal/execution/mcp_support_test.go b/services/core/internal/execution/mcp_support_test.go index 7d3921e3a..725ccee36 100644 --- a/services/core/internal/execution/mcp_support_test.go +++ b/services/core/internal/execution/mcp_support_test.go @@ -9,7 +9,6 @@ import ( v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" "github.com/google/uuid" @@ -26,7 +25,7 @@ func (c *recordingCredentials) MCPBearerToken(_ context.Context, command vaults. return c.token, nil } -func mcpSupportFixture(t *testing.T) (Snapshot, []proto.MCPHTTPServer, runtimedevice.KindCapabilities) { +func mcpSupportFixture(t *testing.T) (Snapshot, []proto.MCPHTTPServer, proto.AgentKindCapabilities) { t.Helper() vault, credential := uuid.NewString(), uuid.NewString() tool := json.RawMessage(`{"type":"mcp","server_label":"tickets","connection_origin":"service","transport":{"type":"http","server_url":"https://mcp.example/tools"}}`) @@ -36,8 +35,8 @@ func mcpSupportFixture(t *testing.T) (Snapshot, []proto.MCPHTTPServer, runtimede if err != nil { t.Fatal(err) } - caps := runtimedevice.KindCapabilities{EnvironmentNone: true, MCPHTTPTools: true, MCPHTTPBearerAuth: true, MCPHTTPRequired: true, - Preparation: true} + caps := proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilitySupported, MCPHTTPBearerAuth: proto.CapabilitySupported, MCPHTTPRequired: proto.CapabilitySupported, + Preparation: proto.CapabilitySupported} return snapshot, tools.MCP, caps } @@ -93,15 +92,15 @@ func TestMCPExecutionChecksRequireVerifiedCapabilityCombinations(t *testing.T) { snapshot.Agent.Tools[0], _ = json.Marshal(tool) switch missing { case "mcp": - caps.MCPHTTPTools = false + caps.MCPHTTPTools = proto.CapabilityUnsupported case "bearer": - caps.MCPHTTPBearerAuth = false + caps.MCPHTTPBearerAuth = proto.CapabilityUnsupported case "placement": - caps.EnvironmentNone = false + caps.EnvironmentNone = proto.CapabilityUnsupported case "required": - caps.MCPHTTPRequired = false + caps.MCPHTTPRequired = proto.CapabilityUnsupported case "preparation": - caps.Preparation = false + caps.Preparation = proto.CapabilityUnsupported case "environment": snapshot.Environment = v1.Environment{} } @@ -127,7 +126,7 @@ func TestMCPAnonymousExecutionPreservesFrozenDecision(t *testing.T) { for _, mode := range []string{"unattached", "frozen anonymous", "invalid binding"} { t.Run(engine+"/"+mode, func(t *testing.T) { snapshot, _, caps := mcpSupportFixture(t) - caps.MCPHTTPBearerAuth = false + caps.MCPHTTPBearerAuth = proto.CapabilityUnsupported if mode == "unattached" { snapshot.VaultIDs, snapshot.MCPCredentials = nil, nil } else { diff --git a/services/core/internal/execution/message_support.go b/services/core/internal/execution/message_support.go index 631741bec..8ec5994d6 100644 --- a/services/core/internal/execution/message_support.go +++ b/services/core/internal/execution/message_support.go @@ -72,7 +72,7 @@ func requireMessageImages(peer *runtimegateway.Session, kind string, input proto return nil } info, found, known := peer.AgentKindStatus(kind) - if !found || !known || !info.Available || !info.Capabilities.MessageImages { + if !found || !known || !info.Available || !info.Capabilities.MessageImages.IsSupported() { return errors.New("Runtime does not support message images") } return nil diff --git a/services/core/internal/execution/model_execution_test.go b/services/core/internal/execution/model_execution_test.go index dd1a0aa87..e7a4dea91 100644 --- a/services/core/internal/execution/model_execution_test.go +++ b/services/core/internal/execution/model_execution_test.go @@ -7,9 +7,9 @@ import ( "testing" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) @@ -18,14 +18,14 @@ func TestSessionModelExecutionNeverFallsBack(t *testing.T) { reader, _ := testSessions(t, pgtest.Open(t), nil) d := Dispatcher{SessionsReader: reader} session := sessions.Session{TenantID: uuid.NewString(), ID: uuid.NewString(), Engine: "codex"} - if _, err := d.executionRequest(t.Context(), session, Snapshot{ModelProviderConfigured: true}, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{}); !errors.Is(err, sessions.ErrNotFound) { + if _, err := d.executionRequest(t.Context(), session, Snapshot{ModelProviderConfigured: true}, proto.AgentKindCapabilities{}, sessions.ExecutionBinding{}); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("missing Session credentials fell back", err) } // A none device without a frozen provider uses its own provider environment: // Core sends only the Agent's model and instructions. instructions := "Keep this instruction." snapshot := Snapshot{Agent: v1.Agent{Model: "device-model", Instructions: &instructions}, Environment: v1.Environment{Type: "none"}} - request, err := d.executionRequest(t.Context(), sessions.Session{Engine: "codex"}, snapshot, runtimedevice.KindCapabilities{}, sessions.ExecutionBinding{}) + request, err := d.executionRequest(t.Context(), sessions.Session{Engine: "codex"}, snapshot, proto.AgentKindCapabilities{}, sessions.ExecutionBinding{}) if err != nil || !reflect.DeepEqual(request.AgentOptions, map[string]any{"model": "device-model", "system_prompt": &instructions}) { t.Fatal("none Session received adapter options Core does not own", request.AgentOptions, err) } diff --git a/services/core/internal/execution/recovery_test.go b/services/core/internal/execution/recovery_test.go index b50620f10..ca99e03b8 100644 --- a/services/core/internal/execution/recovery_test.go +++ b/services/core/internal/execution/recovery_test.go @@ -3,7 +3,7 @@ package execution import ( "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -13,7 +13,7 @@ func TestExistingSessionRecoveryRequiresVerifiedCapability(t *testing.T) { for _, nativeID := range []string{"", "native"} { for _, capable := range []bool{false, true} { wantRecovery := started && nativeID == "" - req, err := (&Dispatcher{}).executionRequest(t.Context(), sessions.Session{ID: "session", Engine: engine}, Snapshot{}, runtimedevice.KindCapabilities{NativeSessionRecovery: capable}, sessions.ExecutionBinding{HasStartedTurn: started, NativeSessionID: nativeID}) + req, err := (&Dispatcher{}).executionRequest(t.Context(), sessions.Session{ID: "session", Engine: engine}, Snapshot{}, proto.AgentKindCapabilities{NativeSessionRecovery: proto.CapabilityFromBool(capable)}, sessions.ExecutionBinding{HasStartedTurn: started, NativeSessionID: nativeID}) if wantRecovery && !capable { if err == nil { t.Fatal("unverified recovery admitted", engine) diff --git a/services/core/internal/execution/request.go b/services/core/internal/execution/request.go index 17a7e3085..2cf366442 100644 --- a/services/core/internal/execution/request.go +++ b/services/core/internal/execution/request.go @@ -6,14 +6,13 @@ import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/vaults" ) -func (d *Dispatcher) executionRequest(ctx context.Context, session sessions.Session, snapshot Snapshot, caps runtimedevice.KindCapabilities, bound sessions.ExecutionBinding) (proto.PromptRequestPayload, error) { +func (d *Dispatcher) executionRequest(ctx context.Context, session sessions.Session, snapshot Snapshot, caps proto.AgentKindCapabilities, bound sessions.ExecutionBinding) (proto.PromptRequestPayload, error) { recoverNativeSession := bound.HasStartedTurn && bound.NativeSessionID == "" - if recoverNativeSession && !caps.NativeSessionRecovery { + if recoverNativeSession && !caps.NativeSessionRecovery.IsSupported() { return proto.PromptRequestPayload{}, errors.New("native session recovery is unavailable") } tools, err := executionTools(snapshot.Agent.Tools) diff --git a/services/core/internal/execution/runtime_initialization.go b/services/core/internal/execution/runtime_initialization.go index 2dd4f9d86..3b01cde3e 100644 --- a/services/core/internal/execution/runtime_initialization.go +++ b/services/core/internal/execution/runtime_initialization.go @@ -137,7 +137,7 @@ func (w *Worker) prepareEnvironment(ctx context.Context, owner sessions.Environm } if placement.Type == "openai_hosted" { info, found, known := peer.AgentKindStatus(owner.Engine) - if !known || !found || !info.Available || w.dispatcher.validateEnvironmentHistory(owner.Engine, environment, info.Capabilities.RetainedNativeHistory) != nil { + if !known || !found || !info.Available || w.dispatcher.validateEnvironmentHistory(owner.Engine, environment, info.Capabilities.RetainedNativeHistory.IsSupported()) != nil { *failure = sessions.ProvisioningFailure{Step: sessions.ProvisioningHarness} return sessions.ErrInvalidInput } diff --git a/services/core/internal/execution/structured_output_test.go b/services/core/internal/execution/structured_output_test.go index e4a7d9414..7072fb259 100644 --- a/services/core/internal/execution/structured_output_test.go +++ b/services/core/internal/execution/structured_output_test.go @@ -9,7 +9,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/harnesstest" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -34,7 +33,7 @@ func TestStructuredOutputRequestKeepsFrozenSchemaAndInstructions(t *testing.T) { schema := json.RawMessage(`{"type":"object","properties":{"number":{"const":9007199254740992}}}`) instructions := "Keep these original instructions." snapshot := Snapshot{Agent: v1.Agent{Model: "model", Instructions: &instructions, Text: v1.TextConfig{Format: v1.TextFormat{Type: "json_schema", Schema: schema}}}} - request, err := (&Dispatcher{}).executionRequest(context.Background(), sessions.Session{}, snapshot, runtimedevice.KindCapabilities{MessageItems: true}, sessions.ExecutionBinding{}) + request, err := (&Dispatcher{}).executionRequest(context.Background(), sessions.Session{}, snapshot, proto.AgentKindCapabilities{MessageItems: proto.CapabilitySupported}, sessions.ExecutionBinding{}) if err != nil || request.ExecutionControls.OutputFormat == nil { t.Fatal(err) } diff --git a/services/core/internal/execution/support.go b/services/core/internal/execution/support.go index 00fe48b5f..887a689a6 100644 --- a/services/core/internal/execution/support.go +++ b/services/core/internal/execution/support.go @@ -6,7 +6,6 @@ import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -67,59 +66,59 @@ func (p Policy) validateEngineInputs(engine string, configuration json.RawMessag // engineCapabilities is shared by device selection and the final preclaim check. // Capability bits describe the connected adapter; the declaration states what it may be asked to do. -func (p Policy) engineCapabilities(peer *runtimegateway.Session, engine string, snapshot Snapshot) (runtimedevice.KindCapabilities, error) { - fail := func(message string) (runtimedevice.KindCapabilities, error) { - return runtimedevice.KindCapabilities{}, errors.New(message) +func (p Policy) engineCapabilities(peer *runtimegateway.Session, engine string, snapshot Snapshot) (proto.AgentKindCapabilities, error) { + fail := func(message string) (proto.AgentKindCapabilities, error) { + return proto.AgentKindCapabilities{}, errors.New(message) } declaration, ok := p.Harnesses().Lookup(engine) if !ok || !declaration.Accepts(snapshot.Environment.Type) { return fail("execution engine placement is not supported") } if err := validateDeclaredConfiguration(declaration, snapshot); err != nil { - return runtimedevice.KindCapabilities{}, err + return proto.AgentKindCapabilities{}, err } info, found, known := peer.AgentKindStatus(engine) caps := info.Capabilities - if !known || !found || !info.Available || !caps.Streaming || !caps.DurableTurns || !caps.DurableInputReceipts { + if !known || !found || !info.Available || !caps.Streaming.IsSupported() || !caps.DurableTurns.IsSupported() || !caps.DurableInputReceipts.IsSupported() { return fail("device must advertise streaming, durable input receipts and durable turns for this engine") } - if !caps.Preparation { + if !caps.Preparation.IsSupported() { return fail("device must advertise executor preparation") } - if !caps.ExecutionControls { + if !caps.ExecutionControls.IsSupported() { return fail("device must advertise execution_controls") } - if snapshot.Agent.Text.Format.Type == "json_schema" && (!caps.StructuredOutput || !caps.MessageItems) { + if snapshot.Agent.Text.Format.Type == "json_schema" && (!caps.StructuredOutput.IsSupported() || !caps.MessageItems.IsSupported()) { return fail("device must support structured output and message observations") } - if !caps.ToolObservations { + if !caps.ToolObservations.IsSupported() { return fail("device must advertise tool_observations") } - if snapshot.Agent.MultiAgent.Enabled && !caps.SubagentObservations { + if snapshot.Agent.MultiAgent.Enabled && !caps.SubagentObservations.IsSupported() { return fail("device must support durable subagent observations") } - if !snapshot.Agent.MultiAgent.Enabled && !caps.SubagentControl { + if !snapshot.Agent.MultiAgent.Enabled && !caps.SubagentControl.IsSupported() { return fail("device must advertise subagent_control") } tools, err := executionTools(snapshot.Agent.Tools) if err != nil { return fail("invalid execution tool configuration") } - if err := (proto.PromptRequestPayload{ToolSearch: tools.Search, FunctionTools: tools.Functions}).ValidateToolSearch(caps.ToolSearch); err != nil { + if err := (proto.PromptRequestPayload{ToolSearch: tools.Search, FunctionTools: tools.Functions}).ValidateToolSearch(caps.ToolSearch.IsSupported()); err != nil { return fail(err.Error()) } - if len(tools.Functions) > 0 && !caps.FunctionTools { + if len(tools.Functions) > 0 && !caps.FunctionTools.IsSupported() { return fail("device must advertise function_tools") } if _, err := p.mcpExecutionCredentials(engine, snapshot, tools.MCP, caps); err != nil { - return runtimedevice.KindCapabilities{}, err + return proto.AgentKindCapabilities{}, err } if snapshot.Environment.Type == "openai_hosted" || snapshot.Environment.Type == "self_hosted" { - if !caps.Preparation || !caps.LocalEnvironment || !caps.WorkspaceReadPreparation || !caps.WorkspaceOutputExport { + if !caps.Preparation.IsSupported() || !caps.LocalEnvironment.IsSupported() || !caps.WorkspaceReadPreparation.IsSupported() || !caps.WorkspaceOutputExport.IsSupported() { return fail("device must advertise local preparation, workspace reads and output export") } } - if environmentNone(snapshot) && !caps.EnvironmentNone { + if environmentNone(snapshot) && !caps.EnvironmentNone.IsSupported() { return fail("device must advertise environment_none") } return caps, nil @@ -128,7 +127,7 @@ func (p Policy) engineCapabilities(peer *runtimegateway.Session, engine string, // sessionCapabilities shares storage-specific validation between device selection // and the final preparation gate. The immutable binding, not today's deployment, // determines whether this Session requires native history outside compute. -func (d *Dispatcher) sessionCapabilities(ctx context.Context, peer *runtimegateway.Session, session sessions.Session, snapshot Snapshot) (runtimedevice.KindCapabilities, error) { +func (d *Dispatcher) sessionCapabilities(ctx context.Context, peer *runtimegateway.Session, session sessions.Session, snapshot Snapshot) (proto.AgentKindCapabilities, error) { caps, err := d.engineCapabilities(peer, session.Engine, snapshot) if err != nil { return caps, err @@ -138,10 +137,10 @@ func (d *Dispatcher) sessionCapabilities(ctx context.Context, peer *runtimegatew } environment, err := d.SessionsReader.GetSessionEnvironment(ctx, session.TenantID, session.ID) if err != nil { - return runtimedevice.KindCapabilities{}, err + return proto.AgentKindCapabilities{}, err } - if err := d.validateEnvironmentHistory(session.Engine, environment, caps.RetainedNativeHistory); err != nil { - return runtimedevice.KindCapabilities{}, err + if err := d.validateEnvironmentHistory(session.Engine, environment, caps.RetainedNativeHistory.IsSupported()); err != nil { + return proto.AgentKindCapabilities{}, err } return caps, nil } diff --git a/services/core/internal/persistence/postgres/deploymentpg/specification_test.go b/services/core/internal/persistence/postgres/deploymentpg/specification_test.go index d6f62cbc7..628fd63f9 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/specification_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/specification_test.go @@ -33,7 +33,7 @@ func TestSandboxSpecificationRoundTripAndClaimStays(t *testing.T) { t.Fatal("saved deployment lost its resources or Runtime provenance", err) } preview, err := f.service.SetupForSelection(view.InstallationID, input) - if err != nil || preview.Mode != setup.Mode || preview.BackendFingerprint != setup.BackendFingerprint || !reflect.DeepEqual(preview.Suspension, setup.Suspension) || !reflect.DeepEqual(preview.Configuration, setup.Configuration) { + if err != nil || preview.Mode != setup.Mode || preview.BackendFingerprint != setup.BackendFingerprint || (preview.Suspension == nil) != (setup.Suspension == nil) || preview.Suspension != nil && preview.Suspension.RetentionSeconds != setup.Suspension.RetentionSeconds || !reflect.DeepEqual(preview.Configuration, setup.Configuration) { t.Fatal("preview and persisted normalized deployment disagree", err) } input.ExpectedGeneration = view.Generation diff --git a/services/core/internal/persistence/postgres/sessionpg/device_capabilities_test.go b/services/core/internal/persistence/postgres/sessionpg/device_capabilities_test.go index c468bc51b..c75d972cc 100644 --- a/services/core/internal/persistence/postgres/sessionpg/device_capabilities_test.go +++ b/services/core/internal/persistence/postgres/sessionpg/device_capabilities_test.go @@ -4,9 +4,10 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/jackc/pgx/v5/pgtype" ) @@ -15,7 +16,7 @@ func TestAuthenticatedCapabilitiesKeepRevokedReceipt(t *testing.T) { tenant, _, _ := newEnvironment(t, pool, "self_hosted", "pending") device := newDevice(t, pool, tenant, pgtype.UUID{}) store := New(pgunit.NewPool(pool), nil) - kinds := []runtimedevice.SupportedAgentKind{{Kind: "fixture", Available: true, Capabilities: runtimedevice.KindCapabilities{RetainedNativeHistory: true}}} + kinds := []proto.SupportedAgentKind{{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{RetainedNativeHistory: proto.CapabilitySupported})}} correct := strings.Repeat("a", 64) if ok, err := store.TouchAuthenticatedDevice(t.Context(), device, correct, kinds); err != nil || !ok { t.Fatal(ok, err) diff --git a/services/core/internal/persistence/postgres/sessionpg/devices.go b/services/core/internal/persistence/postgres/sessionpg/devices.go index e8c915fd1..976014d2b 100644 --- a/services/core/internal/persistence/postgres/sessionpg/devices.go +++ b/services/core/internal/persistence/postgres/sessionpg/devices.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/google/uuid" "github.com/jackc/pgx/v5" @@ -196,13 +197,13 @@ func (s *Store) TouchDevice(ctx context.Context, device string) (bool, error) { return n > 0, err } -func (s *Store) TouchAuthenticatedDevice(ctx context.Context, device, credentialHash string, kinds []runtimedevice.SupportedAgentKind) (bool, error) { +func (s *Store) TouchAuthenticatedDevice(ctx context.Context, device, credentialHash string, kinds []proto.SupportedAgentKind) (bool, error) { id, err := parseID(device) if err != nil { return false, err } if kinds == nil { - kinds = []runtimedevice.SupportedAgentKind{} + kinds = []proto.SupportedAgentKind{} } raw, err := json.Marshal(kinds) if err != nil { diff --git a/services/core/internal/runtimedevice/state.go b/services/core/internal/runtimedevice/state.go index 57de85485..d6338d3a6 100644 --- a/services/core/internal/runtimedevice/state.go +++ b/services/core/internal/runtimedevice/state.go @@ -1,6 +1,8 @@ // Package device defines persistence data shared by daemon gateways and their stores. package runtimedevice +import "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + // HeartbeatStatus is the post-heartbeat liveness the runner uses to // detect state changes. type HeartbeatStatus struct { @@ -11,52 +13,11 @@ type HeartbeatStatus struct { Deleted bool } -// KindCapabilities mirrors the daemon heartbeat capability -// shape after gateway-level normalization. Persistence stays separate from wire protocol structs. -type KindCapabilities struct { - SubagentObservations bool `json:"subagent_observations,omitempty"` - Streaming bool `json:"streaming,omitempty"` - Usage bool `json:"usage,omitempty"` - Resume bool `json:"resume,omitempty"` - NativeSessionRecovery bool `json:"native_session_recovery,omitempty"` - RetainedNativeHistory bool `json:"retained_native_history,omitempty"` - MessageItems bool `json:"message_items,omitempty"` - - ToolObservations bool `json:"tool_observations,omitempty"` - EnvironmentNone bool `json:"environment_none,omitempty"` - LocalEnvironment bool `json:"local_environment,omitempty"` - Preparation bool `json:"preparation,omitempty"` - WorkspaceReadPreparation bool `json:"workspace_read_preparation,omitempty"` - WorkspaceOutputExport bool `json:"workspace_output_export,omitempty"` - // ExecutionControls accepts the typed execution controls. - ExecutionControls bool `json:"execution_controls,omitempty"` - StructuredOutput bool `json:"structured_output,omitempty"` - ToolSearch bool `json:"tool_search,omitempty"` - MessageImages bool `json:"message_images,omitempty"` - FunctionResultImages bool `json:"function_result_images,omitempty"` - SubagentControl bool `json:"subagent_control,omitempty"` - FunctionTools bool `json:"function_tools,omitempty"` - MCPHTTPTools bool `json:"mcp_http_tools,omitempty"` - MCPHTTPRequired bool `json:"mcp_http_required,omitempty"` - MCPHTTPBearerAuth bool `json:"mcp_http_bearer_auth,omitempty"` - DurableInputReceipts bool `json:"durable_input_receipts,omitempty"` - DurableTurns bool `json:"durable_turns,omitempty"` -} - -// SupportedAgentKind is the sanitized runtime.config view -// of one daemon-side agent_kind. -type SupportedAgentKind struct { - Kind string `json:"kind"` - Available bool `json:"available"` - Version string `json:"version,omitempty"` - Capabilities KindCapabilities `json:"capabilities,omitempty"` -} - // Heartbeat is the WebSocket daemon heartbeat // payload after gateway normalization. type Heartbeat struct { RuntimeID string // CredentialHash comes from gateway authentication, never a daemon frame. CredentialHash string - SupportedAgentKinds []SupportedAgentKind + SupportedAgentKinds []proto.SupportedAgentKind } diff --git a/services/core/internal/runtimegateway/capabilities_test.go b/services/core/internal/runtimegateway/capabilities_test.go index 05e8cac2a..771107864 100644 --- a/services/core/internal/runtimegateway/capabilities_test.go +++ b/services/core/internal/runtimegateway/capabilities_test.go @@ -3,33 +3,9 @@ package runtimegateway import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "reflect" "testing" ) -func TestCapabilityProjectionCoversEveryField(t *testing.T) { - declaration := prototest.Capabilities(proto.AgentKindCapabilities{}) - wireType, deviceType := reflect.TypeOf(declaration), reflect.TypeOf(runtimedevice.KindCapabilities{}) - if wireType.NumField() != deviceType.NumField() { - t.Fatal("wire and device capability inventories differ") - } - for i := 0; i < wireType.NumField(); i++ { - field := wireType.Field(i) - t.Run(field.Name, func(t *testing.T) { - caps := declaration - reflect.ValueOf(&caps).Elem().Field(i).Set(reflect.ValueOf(proto.CapabilitySupported)) - kinds := deviceKindsFromHeartbeat(proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "fixture", Available: true, Capabilities: caps}}}) - actual := reflect.ValueOf(kinds[0].Capabilities) - for j := 0; j < deviceType.NumField(); j++ { - if actual.Field(j).Bool() != (deviceType.Field(j).Name == field.Name) { - t.Fatal("projection lost or crossed a capability field") - } - } - }) - } -} - func TestInvalidHeartbeatDiscardsPreviousDeclarationAndClosesTransport(t *testing.T) { session := newTestSession(newFakeConn(), "runtime", proto.Version, NewRegistry(), nil) valid, err := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}}}) diff --git a/services/core/internal/runtimegateway/functions_test.go b/services/core/internal/runtimegateway/functions_test.go deleted file mode 100644 index e39a7fa83..000000000 --- a/services/core/internal/runtimegateway/functions_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package runtimegateway - -import ( - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" - "testing" -) - -func TestFunctionCapabilitySurvivesHeartbeatMapping(t *testing.T) { - for _, supported := range []bool{false, true} { - kinds := deviceKindsFromHeartbeat(proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilityFromBool(supported), FunctionResultImages: proto.CapabilityFromBool(supported)})}}}) - s := &Session{} - s.setSupportedAgentKinds(kinds) - info, found, known := s.AgentKindStatus("codex") - if !found || !known || info.Capabilities.FunctionTools != supported || info.Capabilities.FunctionResultImages != supported { - t.Fatal(info, found, known) - } - } -} diff --git a/services/core/internal/runtimegateway/mcp_test.go b/services/core/internal/runtimegateway/mcp_test.go index c78c9d6ee..674ec7866 100644 --- a/services/core/internal/runtimegateway/mcp_test.go +++ b/services/core/internal/runtimegateway/mcp_test.go @@ -22,9 +22,9 @@ func TestMCPHTTPBearerCapabilitySurvivesHeartbeatMapping(t *testing.T) { t.Fatal(err) } s := &Session{} - s.setSupportedAgentKinds(deviceKindsFromHeartbeat(decoded)) + s.setSupportedAgentKinds(decoded.SupportedAgentKinds) info, found, known := s.AgentKindStatus("codex") - if !found || !known || !info.Capabilities.MCPHTTPTools || info.Capabilities.MCPHTTPBearerAuth != supported { + if !found || !known || !info.Capabilities.MCPHTTPTools.IsSupported() || info.Capabilities.MCPHTTPBearerAuth.IsSupported() != supported { t.Fatal("bearer capability was lost or inferred from credential-free MCP") } } @@ -42,9 +42,9 @@ func TestMCPRequiredCapabilityIsExplicit(t *testing.T) { t.Fatal(err) } s := &Session{} - s.setSupportedAgentKinds(deviceKindsFromHeartbeat(decoded)) + s.setSupportedAgentKinds(decoded.SupportedAgentKinds) info, found, known := s.AgentKindStatus("codex") - if !found || !known || info.Capabilities.MCPHTTPRequired != supported { + if !found || !known || info.Capabilities.MCPHTTPRequired.IsSupported() != supported { t.Fatal("combination capability lost or inferred") } } diff --git a/services/core/internal/runtimegateway/preparation_test.go b/services/core/internal/runtimegateway/preparation_test.go index aaea2352d..eecb90019 100644 --- a/services/core/internal/runtimegateway/preparation_test.go +++ b/services/core/internal/runtimegateway/preparation_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" ) func TestPreparationSubscriptionHasNoRunIdentityAndOrdersRevisions(t *testing.T) { @@ -76,10 +75,3 @@ func TestPreparationCloseAndOverflowDoNotInventRunEvents(t *testing.T) { s.Close("test") } } - -func TestPreparationCapabilitySurvivesHeartbeatMapping(t *testing.T) { - kinds := deviceKindsFromHeartbeat(proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{Preparation: proto.CapabilitySupported, LocalEnvironment: proto.CapabilitySupported, WorkspaceReadPreparation: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilitySupported})}}}) - if len(kinds) != 1 || (!kinds[0].Capabilities.Preparation || !kinds[0].Capabilities.LocalEnvironment || !kinds[0].Capabilities.WorkspaceReadPreparation || !kinds[0].Capabilities.NativeSessionRecovery) { - t.Fatal("preparation capability lost") - } -} diff --git a/services/core/internal/runtimegateway/session.go b/services/core/internal/runtimegateway/session.go index a1004b4bf..6a34d1830 100644 --- a/services/core/internal/runtimegateway/session.go +++ b/services/core/internal/runtimegateway/session.go @@ -95,7 +95,7 @@ type Session struct { // sending execution_prepare so unsupported engines fail on the server. kindsMu sync.RWMutex kindsSeen bool - supportedKinds []runtimedevice.SupportedAgentKind + supportedKinds []proto.SupportedAgentKind // Subscribers keyed by runID. The read loop only sends on these // channels; Unsubscribe is the only place that closes them. @@ -186,30 +186,30 @@ func (s *Session) LastSeen() time.Time { // AgentKindStatus returns the latest advertised descriptor for kind. // found=false means the daemon has not advertised that kind; snapshotKnown // distinguishes "no heartbeat yet" from "heartbeat arrived and omitted it". -func (s *Session) AgentKindStatus(kind string) (info runtimedevice.SupportedAgentKind, found bool, snapshotKnown bool) { +func (s *Session) AgentKindStatus(kind string) (info proto.SupportedAgentKind, found bool, snapshotKnown bool) { kind = strings.TrimSpace(kind) if kind == "" { - return runtimedevice.SupportedAgentKind{}, false, false + return proto.SupportedAgentKind{}, false, false } s.kindsMu.RLock() seen := s.kindsSeen - kinds := make([]runtimedevice.SupportedAgentKind, len(s.supportedKinds)) + kinds := make([]proto.SupportedAgentKind, len(s.supportedKinds)) copy(kinds, s.supportedKinds) s.kindsMu.RUnlock() if !seen { - return runtimedevice.SupportedAgentKind{}, false, false + return proto.SupportedAgentKind{}, false, false } for _, candidate := range kinds { if candidate.Kind == kind { return candidate, true, true } } - return runtimedevice.SupportedAgentKind{}, false, true + return proto.SupportedAgentKind{}, false, true } // setSupportedAgentKinds reports whether these are the session's first kinds. -func (s *Session) setSupportedAgentKinds(kinds []runtimedevice.SupportedAgentKind) (first bool) { - copyKinds := make([]runtimedevice.SupportedAgentKind, len(kinds)) +func (s *Session) setSupportedAgentKinds(kinds []proto.SupportedAgentKind) (first bool) { + copyKinds := make([]proto.SupportedAgentKind, len(kinds)) copy(copyKinds, kinds) s.kindsMu.Lock() first = !s.kindsSeen @@ -396,7 +396,7 @@ func (s *Session) handleHeartbeat(env proto.Envelope) { s.Close("invalid heartbeat declaration") return } - kinds := deviceKindsFromHeartbeat(p) + kinds := p.SupportedAgentKinds if s.setSupportedAgentKinds(kinds) { s.reg.announce() } @@ -429,46 +429,6 @@ func (s *Session) handleHeartbeat(env proto.Envelope) { } } -func deviceKindsFromHeartbeat(p proto.HeartbeatPayload) []runtimedevice.SupportedAgentKind { - out := make([]runtimedevice.SupportedAgentKind, 0, len(p.SupportedAgentKinds)) - for _, info := range p.SupportedAgentKinds { - out = append(out, runtimedevice.SupportedAgentKind{ - Kind: info.Kind, - Available: info.Available, - Version: info.Version, - Capabilities: runtimedevice.KindCapabilities{ - Streaming: info.Capabilities.Streaming.IsSupported(), - Usage: info.Capabilities.Usage.IsSupported(), - Resume: info.Capabilities.Resume.IsSupported(), - DurableTurns: info.Capabilities.DurableTurns.IsSupported(), - DurableInputReceipts: info.Capabilities.DurableInputReceipts.IsSupported(), - NativeSessionRecovery: info.Capabilities.NativeSessionRecovery.IsSupported(), - RetainedNativeHistory: info.Capabilities.RetainedNativeHistory.IsSupported(), - MessageItems: info.Capabilities.MessageItems.IsSupported(), - - ToolObservations: info.Capabilities.ToolObservations.IsSupported(), - EnvironmentNone: info.Capabilities.EnvironmentNone.IsSupported(), - LocalEnvironment: info.Capabilities.LocalEnvironment.IsSupported(), - Preparation: info.Capabilities.Preparation.IsSupported(), - WorkspaceReadPreparation: info.Capabilities.WorkspaceReadPreparation.IsSupported(), - WorkspaceOutputExport: info.Capabilities.WorkspaceOutputExport.IsSupported(), - StructuredOutput: info.Capabilities.StructuredOutput.IsSupported(), - ToolSearch: info.Capabilities.ToolSearch.IsSupported(), - MessageImages: info.Capabilities.MessageImages.IsSupported(), - FunctionResultImages: info.Capabilities.FunctionResultImages.IsSupported(), - ExecutionControls: info.Capabilities.ExecutionControls.IsSupported(), - SubagentControl: info.Capabilities.SubagentControl.IsSupported(), - SubagentObservations: info.Capabilities.SubagentObservations.IsSupported(), - FunctionTools: info.Capabilities.FunctionTools.IsSupported(), - MCPHTTPTools: info.Capabilities.MCPHTTPTools.IsSupported(), - MCPHTTPRequired: info.Capabilities.MCPHTTPRequired.IsSupported(), - MCPHTTPBearerAuth: info.Capabilities.MCPHTTPBearerAuth.IsSupported(), - }, - }) - } - return out -} - func (s *Session) dispatch(env proto.Envelope) { if !s.allowsReceiptFrame(env, false) { return diff --git a/services/core/internal/runtimegateway/session_test.go b/services/core/internal/runtimegateway/session_test.go index 15cb6f889..e7fcc6530 100644 --- a/services/core/internal/runtimegateway/session_test.go +++ b/services/core/internal/runtimegateway/session_test.go @@ -374,26 +374,26 @@ func TestSession_HeartbeatPersistsSupportedAgentKinds(t *testing.T) { if len(got.SupportedAgentKinds) != 3 { t.Fatalf("SupportedAgentKinds len = %d, want 3: %#v", len(got.SupportedAgentKinds), got.SupportedAgentKinds) } - byKind := map[string]runtimedevice.SupportedAgentKind{} + byKind := map[string]proto.SupportedAgentKind{} for _, info := range got.SupportedAgentKinds { byKind[info.Kind] = info } claude := byKind["fake_alpha"] - if !claude.Available || claude.Version != "1.2.3" || !claude.Capabilities.Streaming || !claude.Capabilities.Usage || !claude.Capabilities.Resume { + if !claude.Available || claude.Version != "1.2.3" || !claude.Capabilities.Streaming.IsSupported() || !claude.Capabilities.Usage.IsSupported() || !claude.Capabilities.Resume.IsSupported() { t.Fatalf("fake_alpha descriptor not converted: %#v", claude) } fake_beta := byKind["fake_beta"] - if fake_beta.Available || fake_beta.Version != "missing" || !fake_beta.Capabilities.Streaming { + if fake_beta.Available || fake_beta.Version != "missing" || !fake_beta.Capabilities.Streaming.IsSupported() { t.Fatalf("fake_beta descriptor not converted: %#v", fake_beta) } - if !byKind["codex"].Capabilities.ExecutionControls || claude.Capabilities.ExecutionControls || fake_beta.Capabilities.ExecutionControls || !byKind["codex"].Capabilities.ToolObservations || claude.Capabilities.ToolObservations || fake_beta.Capabilities.ToolObservations || !byKind["codex"].Capabilities.SubagentControl || claude.Capabilities.SubagentControl || fake_beta.Capabilities.SubagentControl || !byKind["codex"].Capabilities.EnvironmentNone || claude.Capabilities.EnvironmentNone || fake_beta.Capabilities.EnvironmentNone || !byKind["codex"].Capabilities.MessageItems { + if !byKind["codex"].Capabilities.ExecutionControls.IsSupported() || claude.Capabilities.ExecutionControls.IsSupported() || fake_beta.Capabilities.ExecutionControls.IsSupported() || !byKind["codex"].Capabilities.ToolObservations.IsSupported() || claude.Capabilities.ToolObservations.IsSupported() || fake_beta.Capabilities.ToolObservations.IsSupported() || !byKind["codex"].Capabilities.SubagentControl.IsSupported() || claude.Capabilities.SubagentControl.IsSupported() || fake_beta.Capabilities.SubagentControl.IsSupported() || !byKind["codex"].Capabilities.EnvironmentNone.IsSupported() || claude.Capabilities.EnvironmentNone.IsSupported() || fake_beta.Capabilities.EnvironmentNone.IsSupported() || !byKind["codex"].Capabilities.MessageItems.IsSupported() { t.Fatalf("capabilities not preserved: %#v", byKind) } - if !byKind["codex"].Capabilities.MCPHTTPTools || claude.Capabilities.MCPHTTPTools || fake_beta.Capabilities.MCPHTTPTools { + if !byKind["codex"].Capabilities.MCPHTTPTools.IsSupported() || claude.Capabilities.MCPHTTPTools.IsSupported() || fake_beta.Capabilities.MCPHTTPTools.IsSupported() { t.Fatalf("HTTP MCP capability not preserved: %#v", byKind) } codex, found, known := sess.AgentKindStatus("codex") - if !found || !known || !codex.Capabilities.MCPHTTPTools { + if !found || !known || !codex.Capabilities.MCPHTTPTools.IsSupported() { t.Fatalf("capabilities absent from live session: %#v", codex) } } diff --git a/services/core/internal/sessions/artifacts_test.go b/services/core/internal/sessions/artifacts_test.go index c4213ee9d..68bd1174a 100644 --- a/services/core/internal/sessions/artifacts_test.go +++ b/services/core/internal/sessions/artifacts_test.go @@ -11,8 +11,8 @@ import ( "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" ) // exportFile is one entry of a test export. @@ -213,7 +213,7 @@ func (s *fakeArtifactStorage) TouchDevice(context.Context, string) (bool, error) return false, nil } -func (s *fakeArtifactStorage) TouchAuthenticatedDevice(context.Context, string, string, []runtimedevice.SupportedAgentKind) (bool, error) { +func (s *fakeArtifactStorage) TouchAuthenticatedDevice(context.Context, string, string, []proto.SupportedAgentKind) (bool, error) { s.t.Fatal("unexpected call to TouchAuthenticatedDevice") return false, nil } diff --git a/services/core/internal/sessions/devices.go b/services/core/internal/sessions/devices.go index 85a102e37..ff9e06dc6 100644 --- a/services/core/internal/sessions/devices.go +++ b/services/core/internal/sessions/devices.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" ) @@ -90,7 +91,7 @@ type DeviceStorage interface { TouchDevice(ctx context.Context, device string) (bool, error) // TouchAuthenticatedDevice records that the device was seen with the // credential and reports whether that credential still has authority. - TouchAuthenticatedDevice(ctx context.Context, device, credentialHash string, kinds []runtimedevice.SupportedAgentKind) (bool, error) + TouchAuthenticatedDevice(ctx context.Context, device, credentialHash string, kinds []proto.SupportedAgentKind) (bool, error) // WithEnrollment authenticates the executor credential for the // Environment, then runs apply in the transaction of the Environment's // Session, with the Environment and what the Session lock shows. A diff --git a/services/core/internal/sessions/devices_test.go b/services/core/internal/sessions/devices_test.go index 0194ccda9..c7cfbe77d 100644 --- a/services/core/internal/sessions/devices_test.go +++ b/services/core/internal/sessions/devices_test.go @@ -3,6 +3,8 @@ package sessions import ( "context" "errors" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" "strings" "testing" @@ -16,7 +18,7 @@ import ( type fakeStorage struct { t *testing.T calls []string - heartbeatKinds []runtimedevice.SupportedAgentKind + heartbeatKinds []proto.SupportedAgentKind createDevice func(DeviceRegistration) (ExecutionDevice, error) revokeDevice func() error @@ -77,7 +79,7 @@ func (s *fakeStorage) TouchDevice(_ context.Context, device string) (bool, error return s.touchDevice() } -func (s *fakeStorage) TouchAuthenticatedDevice(_ context.Context, device, credentialHash string, kinds []runtimedevice.SupportedAgentKind) (bool, error) { +func (s *fakeStorage) TouchAuthenticatedDevice(_ context.Context, device, credentialHash string, kinds []proto.SupportedAgentKind) (bool, error) { s.record("TouchAuthenticatedDevice", s.touchAuthenticatedDevice != nil, device, credentialHash) s.heartbeatKinds = kinds return s.touchAuthenticatedDevice() @@ -238,9 +240,9 @@ func TestEnrollRuntimeBindsUnderTheSessionLock(t *testing.T) { func TestHeartbeatPassesRetainedHistoryDeclarationToAuthenticatedStorage(t *testing.T) { storage := &fakeStorage{t: t, touchAuthenticatedDevice: returns(true)} - kinds := []runtimedevice.SupportedAgentKind{{Kind: "fixture", Available: true, Capabilities: runtimedevice.KindCapabilities{RetainedNativeHistory: true}}} + kinds := []proto.SupportedAgentKind{{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{RetainedNativeHistory: proto.CapabilitySupported})}} _, err := deviceService(t, storage).TouchAgentDaemonHeartbeat(t.Context(), runtimedevice.Heartbeat{RuntimeID: "device", CredentialHash: credentialDigest, SupportedAgentKinds: kinds}) - if err != nil || len(storage.heartbeatKinds) != 1 || !storage.heartbeatKinds[0].Capabilities.RetainedNativeHistory { + if err != nil || len(storage.heartbeatKinds) != 1 || !storage.heartbeatKinds[0].Capabilities.RetainedNativeHistory.IsSupported() { t.Fatalf("declaration lost: %v", err) } _, err = deviceService(t, storage).TouchAgentDaemonHeartbeat(t.Context(), runtimedevice.Heartbeat{RuntimeID: "device", CredentialHash: credentialDigest}) diff --git a/services/core/migrations/000008_runtime_deployment_idle_default.sql b/services/core/migrations/000008_runtime_deployment_idle_default.sql new file mode 100644 index 000000000..6aa3f0489 --- /dev/null +++ b/services/core/migrations/000008_runtime_deployment_idle_default.sql @@ -0,0 +1,3 @@ +-- +goose Up +-- The column default is the one authored definition of the initial idle policy. +ALTER TABLE runtime_deployment ALTER COLUMN idle_seconds SET DEFAULT 300; diff --git a/services/core/tests/integration/archive_cancellation_test.go b/services/core/tests/integration/archive_cancellation_test.go index ddb4c7ac3..6f3bb7a73 100644 --- a/services/core/tests/integration/archive_cancellation_test.go +++ b/services/core/tests/integration/archive_cancellation_test.go @@ -114,7 +114,7 @@ func TestArchiveWaitingCancellationReceipts(t *testing.T) { peer, err = registry.LookupDevice(owner.DeviceID) if err == nil { info, _, known := peer.AgentKindStatus("codex") - if known && info.Capabilities.FunctionTools { + if known && info.Capabilities.FunctionTools.IsSupported() { break } } diff --git a/services/core/tests/integration/claude_execution_test.go b/services/core/tests/integration/claude_execution_test.go index e0d8bb086..585e77354 100644 --- a/services/core/tests/integration/claude_execution_test.go +++ b/services/core/tests/integration/claude_execution_test.go @@ -36,7 +36,7 @@ func claudeHeartbeat(t *testing.T, h *dispatchHarness, ready bool) { for { peer, _ := h.registry.LookupDevice(h.device.ID) info, found, known := peer.AgentKindStatus("claude_sdk") - if known && found && info.Capabilities.DurableInputReceipts == ready { + if known && found && info.Capabilities.DurableInputReceipts.IsSupported() == ready { return } if time.Now().After(deadline) { diff --git a/services/core/tests/integration/claude_mcp_test.go b/services/core/tests/integration/claude_mcp_test.go index 9956f5700..a3504ac15 100644 --- a/services/core/tests/integration/claude_mcp_test.go +++ b/services/core/tests/integration/claude_mcp_test.go @@ -33,7 +33,7 @@ func TestClaudeMCPWaitsForCapableRuntime(t *testing.T) { awaitDaemonRemoteCondition(t, t.Context(), 3*time.Second, "Claude MCP heartbeat", func() bool { peer, _ := h.registry.LookupDevice(h.device.ID) info, found, known := peer.AgentKindStatus("claude_sdk") - return known && found && info.Capabilities.MCPHTTPTools == (authenticated || required) && !info.Capabilities.MCPHTTPBearerAuth && !info.Capabilities.MCPHTTPRequired + return known && found && info.Capabilities.MCPHTTPTools.IsSupported() == (authenticated || required) && !info.Capabilities.MCPHTTPBearerAuth.IsSupported() && !info.Capabilities.MCPHTTPRequired.IsSupported() }) input := h.message("start", "Use declared tools only") if prebound { @@ -109,7 +109,7 @@ func TestClaudeMCPUnsupportedSnapshotRejectedBeforeClaim(t *testing.T) { deadline := time.Now().Add(3 * time.Second) for { peer, _ := h.registry.LookupDevice(h.device.ID) - if info, _, _ := peer.AgentKindStatus("claude_sdk"); info.Capabilities.MCPHTTPBearerAuth { + if info, _, _ := peer.AgentKindStatus("claude_sdk"); info.Capabilities.MCPHTTPBearerAuth.IsSupported() { break } if time.Now().After(deadline) { diff --git a/services/core/tests/integration/dispatch_test.go b/services/core/tests/integration/dispatch_test.go index 00104f28b..2e5ac4dae 100644 --- a/services/core/tests/integration/dispatch_test.go +++ b/services/core/tests/integration/dispatch_test.go @@ -435,7 +435,7 @@ func TestExecutionRejectsRuntimeMissingCapabilityBeforeClaim(t *testing.T) { for { peer, _ := h.registry.LookupDevice(h.device.ID) info, _, _ := peer.AgentKindStatus("codex") - if info.Capabilities.DurableInputReceipts == (missing != "durable_input_receipts") && info.Capabilities.Preparation == (missing != "preparation") && info.Capabilities.ExecutionControls == (missing != "execution_controls") && info.Capabilities.DurableTurns == (missing != "durable_turns") && info.Capabilities.SubagentControl == (missing != "subagent_control") && info.Capabilities.ToolObservations == (missing != "tool_observations") && info.Capabilities.EnvironmentNone == (missing != "environment_none") { + if info.Capabilities.DurableInputReceipts.IsSupported() == (missing != "durable_input_receipts") && info.Capabilities.Preparation.IsSupported() == (missing != "preparation") && info.Capabilities.ExecutionControls.IsSupported() == (missing != "execution_controls") && info.Capabilities.DurableTurns.IsSupported() == (missing != "durable_turns") && info.Capabilities.SubagentControl.IsSupported() == (missing != "subagent_control") && info.Capabilities.ToolObservations.IsSupported() == (missing != "tool_observations") && info.Capabilities.EnvironmentNone.IsSupported() == (missing != "environment_none") { break } if time.Now().After(deadline) { diff --git a/services/core/tests/integration/environment_directory_test.go b/services/core/tests/integration/environment_directory_test.go index 5b02797ef..a06ca7d1b 100644 --- a/services/core/tests/integration/environment_directory_test.go +++ b/services/core/tests/integration/environment_directory_test.go @@ -37,7 +37,7 @@ func directoryWorkerBeforeStart(t *testing.T, before func(*dispatchHarness, sess } awaitDaemonRemoteCondition(t, t.Context(), 3*time.Second, "read preparation capability", func() bool { info, _, _ := peer.AgentKindStatus("codex") - return info.Capabilities.WorkspaceReadPreparation + return info.Capabilities.WorkspaceReadPreparation.IsSupported() }) var w *execution.Worker if before != nil { diff --git a/services/core/tests/integration/environment_runtime_fixture_test.go b/services/core/tests/integration/environment_runtime_fixture_test.go index 4a047b13a..f34073627 100644 --- a/services/core/tests/integration/environment_runtime_fixture_test.go +++ b/services/core/tests/integration/environment_runtime_fixture_test.go @@ -118,9 +118,9 @@ func awaitFixtureCapabilities(t *testing.T, h *dispatchHarness, caps proto.Agent return false } info, _, known := peer.AgentKindStatus("codex") - return known && info.Capabilities.Preparation == caps.Preparation.IsSupported() && - info.Capabilities.LocalEnvironment == caps.LocalEnvironment.IsSupported() && - info.Capabilities.DurableInputReceipts == caps.DurableInputReceipts.IsSupported() && - info.Capabilities.WorkspaceOutputExport == caps.WorkspaceOutputExport.IsSupported() + return known && info.Capabilities.Preparation.IsSupported() == caps.Preparation.IsSupported() && + info.Capabilities.LocalEnvironment.IsSupported() == caps.LocalEnvironment.IsSupported() && + info.Capabilities.DurableInputReceipts.IsSupported() == caps.DurableInputReceipts.IsSupported() && + info.Capabilities.WorkspaceOutputExport.IsSupported() == caps.WorkspaceOutputExport.IsSupported() }) } diff --git a/services/core/tests/integration/environment_worker_helpers_test.go b/services/core/tests/integration/environment_worker_helpers_test.go index c69afb4e7..6420aca56 100644 --- a/services/core/tests/integration/environment_worker_helpers_test.go +++ b/services/core/tests/integration/environment_worker_helpers_test.go @@ -22,7 +22,7 @@ func enableWorkerEnvironment(t *testing.T, h *dispatchHarness) { return false } info, _, _ := peer.AgentKindStatus("codex") - return info.Capabilities.Preparation && info.Capabilities.EnvironmentNone + return info.Capabilities.Preparation.IsSupported() && info.Capabilities.EnvironmentNone.IsSupported() }) } diff --git a/services/core/tests/integration/execution_messages_test.go b/services/core/tests/integration/execution_messages_test.go index b84787a72..32ae94ccb 100644 --- a/services/core/tests/integration/execution_messages_test.go +++ b/services/core/tests/integration/execution_messages_test.go @@ -22,7 +22,7 @@ func TestExecutionPersistsMessageObservations(t *testing.T) { t.Fatal(err) } info, _, _ := peer.AgentKindStatus("codex") - if info.Capabilities.MessageItems { + if info.Capabilities.MessageItems.IsSupported() { break } if time.Now().After(deadline) { diff --git a/services/core/tests/integration/function_execution_test.go b/services/core/tests/integration/function_execution_test.go index 0e8c78d98..f81acf33c 100644 --- a/services/core/tests/integration/function_execution_test.go +++ b/services/core/tests/integration/function_execution_test.go @@ -37,7 +37,7 @@ func newFunctionHarnessWith(t *testing.T, configuration string) *dispatchHarness for { peer, _ := h.registry.LookupDevice(h.device.ID) info, _, _ := peer.AgentKindStatus("codex") - if info.Capabilities.FunctionTools { + if info.Capabilities.FunctionTools.IsSupported() { return h } if time.Now().After(deadline) { diff --git a/services/core/tests/integration/function_worker_test.go b/services/core/tests/integration/function_worker_test.go index 035c1f51f..b79736081 100644 --- a/services/core/tests/integration/function_worker_test.go +++ b/services/core/tests/integration/function_worker_test.go @@ -54,7 +54,7 @@ func TestWorkerWaitsForToolCapabilities(t *testing.T) { for { peer, _ := h.registry.LookupDevice(h.device.ID) info, _, _ := peer.AgentKindStatus("codex") - if info.Capabilities.Preparation == caps.Preparation.IsSupported() && info.Capabilities.DurableInputReceipts == caps.DurableInputReceipts.IsSupported() && info.Capabilities.ExecutionControls == caps.ExecutionControls.IsSupported() && info.Capabilities.FunctionTools == caps.FunctionTools.IsSupported() && info.Capabilities.ToolObservations == caps.ToolObservations.IsSupported() && info.Capabilities.MCPHTTPTools == caps.MCPHTTPTools.IsSupported() && info.Capabilities.MCPHTTPBearerAuth == caps.MCPHTTPBearerAuth.IsSupported() && info.Capabilities.MCPHTTPRequired == caps.MCPHTTPRequired.IsSupported() { + if info.Capabilities.Preparation.IsSupported() == caps.Preparation.IsSupported() && info.Capabilities.DurableInputReceipts.IsSupported() == caps.DurableInputReceipts.IsSupported() && info.Capabilities.ExecutionControls.IsSupported() == caps.ExecutionControls.IsSupported() && info.Capabilities.FunctionTools.IsSupported() == caps.FunctionTools.IsSupported() && info.Capabilities.ToolObservations.IsSupported() == caps.ToolObservations.IsSupported() && info.Capabilities.MCPHTTPTools.IsSupported() == caps.MCPHTTPTools.IsSupported() && info.Capabilities.MCPHTTPBearerAuth.IsSupported() == caps.MCPHTTPBearerAuth.IsSupported() && info.Capabilities.MCPHTTPRequired.IsSupported() == caps.MCPHTTPRequired.IsSupported() { break } if time.Now().After(deadline) { diff --git a/services/core/tests/integration/harness_onboarding_test.go b/services/core/tests/integration/harness_onboarding_test.go index b83de3901..63f020c00 100644 --- a/services/core/tests/integration/harness_onboarding_test.go +++ b/services/core/tests/integration/harness_onboarding_test.go @@ -132,7 +132,7 @@ func TestThirdHarnessPublicOnboarding(t *testing.T) { } for deadline := time.Now().Add(3 * time.Second); ; { current, _, _ := peer.AgentKindStatus("fixture_harness") - if !current.Capabilities.DurableInputReceipts { + if !current.Capabilities.DurableInputReceipts.IsSupported() { break } if time.Now().After(deadline) { diff --git a/services/core/tests/integration/local_environment_worker_test.go b/services/core/tests/integration/local_environment_worker_test.go index 24af7917e..a772b904c 100644 --- a/services/core/tests/integration/local_environment_worker_test.go +++ b/services/core/tests/integration/local_environment_worker_test.go @@ -37,7 +37,7 @@ func localWorker(t *testing.T, scoped, execute bool) (*dispatchHarness, *executi return false } info, _, _ := peer.AgentKindStatus("codex") - return info.Capabilities.LocalEnvironment + return info.Capabilities.LocalEnvironment.IsSupported() }) w := startWorker(t, t.Context(), h.s, h.d) ctx, cancel := context.WithCancel(t.Context()) diff --git a/services/core/tests/integration/mcode_public_native_test.go b/services/core/tests/integration/mcode_public_native_test.go index f592fc60e..84c6f6ab0 100644 --- a/services/core/tests/integration/mcode_public_native_test.go +++ b/services/core/tests/integration/mcode_public_native_test.go @@ -159,7 +159,7 @@ func startNativeEngineDaemon(t *testing.T, h *dispatchHarness, home, binary, eng deadline := time.Now().Add(45 * time.Second) for time.Now().Before(deadline) { if peer, err := h.registry.LookupDevice(h.device.ID); err == nil && peer != old { - if info, found, known := peer.AgentKindStatus(engine); found && known && info.Available && info.Capabilities.EnvironmentNone { + if info, found, known := peer.AgentKindStatus(engine); found && known && info.Available && info.Capabilities.EnvironmentNone.IsSupported() { return stop } } diff --git a/services/core/tests/integration/native_daemon_test.go b/services/core/tests/integration/native_daemon_test.go index 908f769a9..ac6cb9dee 100644 --- a/services/core/tests/integration/native_daemon_test.go +++ b/services/core/tests/integration/native_daemon_test.go @@ -92,7 +92,7 @@ func startNativeDispatchDaemon(t *testing.T, h *dispatchHarness, home, binary st for { peer, e := h.registry.LookupDevice(h.device.ID) if e == nil && peer != oldPeer { - if info, found, known := peer.AgentKindStatus("codex"); known && found && info.Available && info.Capabilities.EnvironmentNone { + if info, found, known := peer.AgentKindStatus("codex"); known && found && info.Available && info.Capabilities.EnvironmentNone.IsSupported() { break } } diff --git a/services/core/tests/integration/structured_output_dispatch_test.go b/services/core/tests/integration/structured_output_dispatch_test.go index 06ad11e44..a9af87c3f 100644 --- a/services/core/tests/integration/structured_output_dispatch_test.go +++ b/services/core/tests/integration/structured_output_dispatch_test.go @@ -35,7 +35,7 @@ func TestStructuredOutputDispatchRechecksOperationQualification(t *testing.T) { peer, _ := h.registry.LookupDevice(h.device.ID) for deadline := time.Now().Add(3 * time.Second); ; { info, found, known := peer.AgentKindStatus("fixture_harness") - if known && found && info.Capabilities.StructuredOutput { + if known && found && info.Capabilities.StructuredOutput.IsSupported() { break } if time.Now().After(deadline) {