diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index b5af01e8c..d90a069dc 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -13,7 +13,6 @@ on: - docs.json - package.json - pnpm-workspace.yaml - - .npmrc - Makefile - .github/actions/node/** - .github/workflows/website.yml diff --git a/.gitignore b/.gitignore index 3d22625f0..40355526c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,11 +11,6 @@ coverage/ go.work.sum __pycache__/ *.pyc -/config/ -/logs/ -/state/ -/cache/ -/target/ # `go build ./services/web` in the repository root writes this binary. /web playwright-report/ diff --git a/Makefile b/Makefile index 2e73eade1..4294afe9c 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SQLC_VERSION ?= v1.29.0 SQLC ?= go run github.com/sqlc-dev/sqlc/cmd/sqlc@$(SQLC_VERSION) SWAG_VERSION ?= v1.16.4 -.PHONY: help check check-database check-go check-sqlc sqlc-generate node-deps check-claude-sdk check-web check-mcode-harness build-daemon build-core check-core check-core-packages check-core-integration docker-build-core check-core-container build-codex-runtime build-claude-runtime build-claude-sdk-runtime build-mcode-harness build-mcode-runtime +.PHONY: help check check-database check-go check-sqlc sqlc-generate node-deps check-claude-sdk check-web check-mcode-harness build-daemon build-core check-core check-core-packages check-core-integration docker-build-core check-core-container build-codex-runtime build-claude-runtime build-claude-sdk-runtime help: @printf '%s\n' 'make build-core Build standalone Core commands' 'make build-daemon Build the execution daemon' 'make check Run Core, persistence and runtime checks' 'See README.md for runtime prerequisites and deployment.' @@ -154,12 +154,6 @@ build-codex-runtime: build-claude-runtime: ./scripts/build-claude-runtime.sh -build-mcode-harness: - ./scripts/build-mcode-harness.sh - -build-mcode-runtime: - ./scripts/build-mcode-runtime.sh - .PHONY: build-microsandbox-provider check-microsandbox-provider build-microsandbox-provider: python3 scripts/build-microsandbox-provider.py build "$${OAC_DEV_HOME:-$$HOME/.oac}/build/microsandbox-provider/oac-microsandbox-provider" @@ -180,7 +174,6 @@ check-distribution: go test ./services/web ./services/core/cmd/oac -count=1 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s deploy/node -p 'test_*.py' PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s deploy/compose -p 'test_*.py' - PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s scripts/acceptance -p 'test_*.py' PYTHONDONTWRITEBYTECODE=1 python3 deploy/test_install.py PYTHONDONTWRITEBYTECODE=1 python3 deploy/test_install_source.py PYTHONDONTWRITEBYTECODE=1 python3 scripts/core-distribution-manifest.test.py diff --git a/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go deleted file mode 100644 index 157133232..000000000 --- a/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go +++ /dev/null @@ -1,161 +0,0 @@ -//go:build linux - -package claudesdk - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/google/uuid" -) - -func TestLiveClaudeSDKCancelResume(t *testing.T) { - entrypoint := os.Getenv("OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT") - keyFile := os.Getenv("OAC_TEST_CLAUDE_SDK_MINIMAX_KEY_FILE") - if entrypoint == "" || keyFile == "" { - t.Skip("real cancellation acceptance requires explicit SDK entrypoint and private key file") - } - proofRoot := os.Getenv("OAC_TEST_CLAUDE_SDK_PROOF_DIR") - if !filepath.IsAbs(proofRoot) { - t.Fatal("OAC_TEST_CLAUDE_SDK_PROOF_DIR must be an absolute managed directory") - } - root, err := os.MkdirTemp(proofRoot, "claude-cancel-") - if err != nil { - t.Fatal(err) - } - t.Logf("real cancellation evidence: %s", root) - t.Setenv("OAC_RUNTIME_HOME", root) - key, err := os.ReadFile(keyFile) - if err != nil { - t.Fatal(err) - } - config := Config{Entrypoint: entrypoint, StateDir: filepath.Join(root, "state"), Env: []string{ - "ANTHROPIC_BASE_URL=https://api.minimax.cn/anthropic", "ANTHROPIC_AUTH_TOKEN=" + strings.TrimSpace(string(key)), - "ANTHROPIC_API_KEY=", "CLAUDE_CODE_OAUTH_TOKEN=", "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", - "ANTHROPIC_DEFAULT_SONNET_MODEL=MiniMax-M3", "ANTHROPIC_DEFAULT_OPUS_MODEL=MiniMax-M3", "ANTHROPIC_DEFAULT_HAIKU_MODEL=MiniMax-M3", - }} - readiness, err := CheckRuntime(context.Background(), config) - if err != nil { - t.Fatal("real runtime readiness failed", err) - } - readinessJSON, _ := json.MarshalIndent(readiness, "", " ") - if err := os.WriteFile(filepath.Join(root, "readiness.json"), readinessJSON, 0o600); err != nil { - t.Fatal(err) - } - type evidence struct { - NodePID int `json:"node_pid"` - NativePIDs []int `json:"native_pids"` - CancelMS int64 `json:"cancel_milliseconds,omitempty"` - Cancelled bool `json:"cancelled"` - Failure string `json:"failure,omitempty"` - Outcome proto.DonePayload `json:"outcome"` - Events []proto.Envelope `json:"events"` - } - run := func(prompt, resume string, cancelOnText bool) evidence { - t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) - defer cancel() - out := make(chan proto.Envelope, 64) - request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": "MiniMax-M3", "system_prompt": "Follow the user's requested format. Preserve the exact verification value in conversation history. Use no tools."}} - running, err := startSingleTurn(ctx, config, request, out) - if err != nil { - t.Fatal(err) - } - s := running.(*session) - defer s.Cancel(ctx) - proof := evidence{NodePID: s.process.Cmd.Process.Pid} - done := false - for event := range out { - proof.Events = append(proof.Events, event) - if event.Type == proto.TypeDelta && len(proof.NativePIDs) == 0 { - raw, _ := os.ReadFile(fmt.Sprintf("/proc/%d/task/%d/children", proof.NodePID, proof.NodePID)) - for _, value := range strings.Fields(string(raw)) { - pid, _ := strconv.Atoi(value) - args, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) - if bytes.Contains(args, []byte("\x00--input-format\x00stream-json\x00")) && bytes.Contains(args, []byte("\x00--output-format\x00stream-json\x00")) { - proof.NativePIDs = append(proof.NativePIDs, pid) - } - } - } - switch event.Type { - case proto.TypeDelta: - if cancelOnText && !proof.Cancelled { - select { - case <-s.process.Done(): - t.Fatal("native execution ended before cancellation") - default: - } - started := time.Now() - if err := s.Cancel(ctx); err != nil { - t.Fatal("live cancellation failed", err) - } - proof.CancelMS = time.Since(started).Milliseconds() - proof.Cancelled = true - proof.Outcome = s.CancellationOutcome() - } - case proto.TypeError: - var payload proto.ErrorPayload - _ = event.DecodePayload(&payload) - proof.Failure = payload.Error - case proto.TypeDone: - done = true - var payload proto.DonePayload - if err := event.DecodePayload(&payload); err != nil { - t.Fatal(err) - } - if proof.Cancelled { - expected, _ := json.Marshal(proof.Outcome) - actual, _ := json.Marshal(payload) - if !bytes.Equal(expected, actual) { - t.Fatal("live cancellation outcome differs from Done") - } - } - proof.Outcome = payload - } - } - data, _ := json.MarshalIndent(proof, "", " ") - if err := os.WriteFile(filepath.Join(root, fmt.Sprintf("execution-%d.json", proof.NodePID)), data, 0o600); err != nil { - t.Fatal(err) - } - if !done || len(proof.NativePIDs) == 0 || proof.Cancelled != cancelOnText { - t.Fatal("missing real execution/completion/cancellation evidence") - } - select { - case <-s.process.Done(): - default: - t.Fatal("completion preceded owned process release") - } - for _, pid := range append([]int{proof.NodePID}, proof.NativePIDs...) { - if value, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)); err == nil { - fields := strings.Fields(string(value)[strings.LastIndex(string(value), ")")+1:]) - if len(fields) == 0 || fields[0] != "Z" { - t.Fatalf("execution process %d remains alive", pid) - } - } - } - return proof - } - nonce := "cancel-history-" + uuid.NewString() - first := run("Remember this exact verification value: "+nonce+". First repeat it, then write two hundred numbered sentences about trees. Do not use tools.", "", true) - id, _ := first.Outcome.Metadata[proto.DoneMetaAgentSessionID].(string) - if id == "" || first.Outcome.Content == "" || first.Failure == "" { - t.Fatal("live cancellation lost identity, partial output or interruption evidence") - } - second := run("Return only the exact cancel-history verification value in the earlier user request. Ignore the earlier request for numbered sentences.", id, false) - if second.Failure != "" || second.Outcome.Metadata[proto.DoneMetaAgentSessionID] != id || !strings.Contains(second.Outcome.Content, nonce) || first.NodePID == second.NodePID { - t.Fatalf("cold continuation did not preserve identity/history; evidence %s", root) - } - data, _ := json.MarshalIndent(map[string]any{"scope": "private Go factory -> maintained SDK/native -> real MiniMax cancellation and cold continuation; public admission remains separate", "verification_value": nonce, "executions": []evidence{first, second}}, "", " ") - if err := os.WriteFile(filepath.Join(root, "proof.json"), data, 0o600); err != nil { - t.Fatal(err) - } -} diff --git a/apps/daemon/internal/agent/claudesdk/combination_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/combination_live_linux_test.go deleted file mode 100644 index 776fe8f17..000000000 --- a/apps/daemon/internal/agent/claudesdk/combination_live_linux_test.go +++ /dev/null @@ -1,140 +0,0 @@ -//go:build linux - -package claudesdk - -import ( - "context" - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/google/uuid" -) - -// TestLiveClaudeSDKNativeCombinations proves structured output beside multi_agent, -// a function tool and deferred tool search on the real SDK and provider. -func TestLiveClaudeSDKNativeCombinations(t *testing.T) { - entrypoint, keyFile, proofRoot := os.Getenv("OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT"), os.Getenv("OAC_TEST_CLAUDE_SDK_MINIMAX_KEY_FILE"), os.Getenv("OAC_TEST_CLAUDE_SDK_PROOF_DIR") - endpoint, model := os.Getenv("OAC_TEST_CLAUDE_SDK_BASE_URL"), os.Getenv("OAC_TEST_CLAUDE_SDK_MODEL") - if entrypoint == "" || keyFile == "" || endpoint == "" || model == "" || !filepath.IsAbs(proofRoot) { - t.Skip("real SDK/provider acceptance requires an entrypoint, a private key file, an endpoint, a model and an absolute proof directory") - } - key, err := os.ReadFile(keyFile) - if err != nil { - t.Fatal(err) - } - if err := os.MkdirAll(proofRoot, 0o700); err != nil { - t.Fatal(err) - } - root, err := os.MkdirTemp(proofRoot, "claude-combination-") - if err != nil { - t.Fatal(err) - } - t.Setenv("OAC_RUNTIME_HOME", root) - config := Config{Entrypoint: entrypoint, StateDir: filepath.Join(root, "state"), Env: []string{ - "ANTHROPIC_BASE_URL=" + endpoint, "ANTHROPIC_AUTH_TOKEN=" + strings.TrimSpace(string(key)), - "ANTHROPIC_API_KEY=", "CLAUDE_CODE_OAUTH_TOKEN=", - "ANTHROPIC_DEFAULT_SONNET_MODEL=" + model, "ANTHROPIC_DEFAULT_OPUS_MODEL=" + model, "ANTHROPIC_DEFAULT_HAIKU_MODEL=" + model, - }} - schema := json.RawMessage(`{"type":"object","properties":{"answer":{"type":"string"}},"required":["answer"],"additionalProperties":false}`) - lookup := func(deferred bool) []proto.FunctionTool { - return []proto.FunctionTool{{Name: "lookup", Description: "Return the synthetic verification value.", Parameters: json.RawMessage(`{"type":"object","properties":{},"additionalProperties":false}`), DeferLoading: deferred}} - } - // A stateless JSON-RPC MCP server with one tool. - mcpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, _ := io.ReadAll(r.Body) - var rpc struct { - ID json.RawMessage `json:"id"` - Method string `json:"method"` - } - _ = json.Unmarshal(body, &rpc) - var result string - switch rpc.Method { - case "initialize": - result = `{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"proof","version":"0"}}` - case "tools/list": - result = `{"tools":[{"name":"echo","description":"Return the synthetic MCP value.","inputSchema":{"type":"object","properties":{}}}]}` - case "tools/call": - result = `{"content":[{"type":"text","text":"synthetic-mcp-value"}]}` - default: - w.WriteHeader(http.StatusAccepted) - return - } - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":` + string(rpc.ID) + `,"result":` + result + `}`)) - })) - defer mcpServer.Close() - mcp := func(req *proto.PromptRequestPayload) { - req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "proof", ServerURL: mcpServer.URL}} - } - cases := map[string]func(*proto.PromptRequestPayload){ - "multi_agent": func(req *proto.PromptRequestPayload) { - req.DisableSubagents, req.ObserveSubagentIdentities = false, true - }, - "mcp": mcp, - "mcp_tool_search": func(req *proto.PromptRequestPayload) { - mcp(req) - req.FunctionTools, req.ToolSearch = lookup(true), true - }, - "function": func(req *proto.PromptRequestPayload) { req.FunctionTools = lookup(false) }, - "tool_search": func(req *proto.PromptRequestPayload) { - req.FunctionTools, req.ToolSearch = lookup(true), true - }, - } - for name, configure := range cases { - t.Run(name, func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) - defer cancel() - req := proto.PromptRequestPayload{RunID: uuid.NewString(), DisableExecutionEnvironment: true, DisableSubagents: true, - Input: proto.TextInput("Call the lookup tool if one is available and put its value in answer. Otherwise answer with the word ready."), - ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: schema}}, - AgentOptions: map[string]any{"model": model, "system_prompt": "Reply only through the structured output."}} - configure(&req) - out := make(chan proto.Envelope, 64) - running, err := startSingleTurn(ctx, config, req, out) - if err != nil { - t.Fatal(err) - } - defer running.Cancel(context.Background()) - s := running.(*session) - var done, called bool - var content, failure string - for event := range out { - switch event.Type { - case proto.TypeFunctionCall: - var call proto.FunctionCallPayload - if err := event.DecodePayload(&call); err != nil { - t.Fatal(err) - } - called = true - text := "synthetic-verification-value" - if err := s.SubmitFunctionResult(ctx, proto.FunctionResultPayload{CallID: call.CallID, DeliveryID: uuid.NewString(), Success: true, Content: []proto.InputContent{{Type: "input_text", Text: &text}}}); err != nil { - t.Fatal(err) - } - case proto.TypeError: - var payload proto.ErrorPayload - _ = json.Unmarshal(event.Payload, &payload) - failure = payload.Error - case proto.TypeDone: - var payload proto.DonePayload - _ = json.Unmarshal(event.Payload, &payload) - done, content = true, payload.Content - } - } - var answer struct { - Answer string `json:"answer"` - } - if !done || failure != "" || json.Unmarshal([]byte(content), &answer) != nil || answer.Answer == "" { - t.Fatalf("structured output failed: done=%v failure=%q content=%q", done, failure, content) - } - t.Logf("%s: function_called=%v answer=%q", name, called, answer.Answer) - }) - } -} diff --git a/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go deleted file mode 100644 index 16059494d..000000000 --- a/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go +++ /dev/null @@ -1,203 +0,0 @@ -//go:build linux - -package claudesdk - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "os" - "path/filepath" - "slices" - "strconv" - "strings" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/google/uuid" -) - -// Explicit opt-in acceptance; normal test runs never contact a provider. -func TestLiveClaudeExecutorReuseAndCancel(t *testing.T) { - entry, keyPath, proof := os.Getenv("OAC_TEST_CLAUDE_EXECUTOR_ENTRYPOINT"), os.Getenv("OAC_TEST_CLAUDE_EXECUTOR_KEY_FILE"), os.Getenv("OAC_TEST_CLAUDE_EXECUTOR_PROOF_DIR") - if entry == "" || keyPath == "" || proof == "" { - t.Skip("explicit installed runtime, private key file and proof directory required") - } - endpoint, model := os.Getenv("OAC_TEST_CLAUDE_EXECUTOR_BASE_URL"), os.Getenv("OAC_TEST_CLAUDE_EXECUTOR_MODEL") - if endpoint == "" || model == "" { - t.Fatal("explicit provider endpoint and model required") - } - if !filepath.IsAbs(proof) { - t.Fatal("absolute proof directory required") - } - if err := os.MkdirAll(proof, 0700); err != nil { - t.Fatal(err) - } - key, err := os.ReadFile(keyPath) - if err != nil { - t.Fatal("cannot read selected credential file") - } - t.Setenv("OAC_RUNTIME_HOME", proof) - config := Config{Node: os.Getenv("OAC_TEST_CLAUDE_EXECUTOR_NODE"), Entrypoint: entry, StateDir: filepath.Join(proof, "state"), Env: []string{ - "ANTHROPIC_BASE_URL=" + endpoint, "ANTHROPIC_AUTH_TOKEN=" + strings.TrimSpace(string(key)), "ANTHROPIC_API_KEY=", "CLAUDE_CODE_OAUTH_TOKEN=", "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", - "ANTHROPIC_DEFAULT_SONNET_MODEL=" + model, "ANTHROPIC_DEFAULT_OPUS_MODEL=" + model, "ANTHROPIC_DEFAULT_HAIKU_MODEL=" + model, - }} - if proxy := os.Getenv("OAC_TEST_CLAUDE_EXECUTOR_HTTP_PROXY"); proxy != "" { - config.Env = append(config.Env, "HTTP_PROXY="+proxy, "HTTPS_PROXY="+proxy, "http_proxy="+proxy, "https_proxy="+proxy) - } - ctx, cancel := context.WithTimeout(t.Context(), 4*time.Minute) - defer cancel() - readiness, err := CheckRuntime(ctx, config) - if err != nil { - t.Fatal("installed runtime readiness failed", err) - } - type turnEvidence struct { - Run string `json:"run"` - NodePID int `json:"node_pid"` - NativePIDs []int `json:"native_pids"` - FirstTextMS int64 `json:"first_text_ms"` - ElapsedMS int64 `json:"elapsed_ms"` - CancelMS int64 `json:"cancel_ms,omitempty"` - Cancelled bool `json:"cancelled"` - CancellationError string `json:"cancellation_error,omitempty"` - Settlement agent.TurnSettlement `json:"settlement"` - SettlementError string `json:"settlement_error,omitempty"` - Done proto.DonePayload `json:"done"` - Errors []string `json:"errors,omitempty"` - } - evidence := struct { - Runtime RuntimeInfo `json:"runtime"` - PrepareMS int64 `json:"prepare_ms"` - Turns []turnEvidence `json:"turns"` - Recovered bool `json:"recovered_after_cancel"` - Closed bool `json:"closed"` - }{Runtime: readiness} - persist := func() { - raw, _ := json.MarshalIndent(evidence, "", " ") - _ = os.WriteFile(filepath.Join(proof, "executor-evidence.json"), raw, 0600) - } - defer persist() - request := proto.PromptRequestPayload{DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": model, "system_prompt": "Follow requested formats briefly. Remember the exact verification marker across the conversation. Use no tools."}} - factory := NewExecutorFactory(config) - prepared := time.Now() - owner, err := factory(ctx, request) - if err != nil { - t.Fatal("executor preparation failed", err) - } - evidence.PrepareMS = time.Since(prepared).Milliseconds() - defer func() { - closeCtx, stop := context.WithTimeout(context.Background(), 10*time.Second) - defer stop() - evidence.Closed = owner.Close(closeCtx) == nil - persist() - }() - nativeChildren := func(pid int) []int { - raw, _ := os.ReadFile(fmt.Sprintf("/proc/%d/task/%d/children", pid, pid)) - var ids []int - for _, value := range strings.Fields(string(raw)) { - child, _ := strconv.Atoi(value) - args, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", child)) - if bytes.Contains(args, []byte("\x00--input-format\x00stream-json\x00")) && bytes.Contains(args, []byte("\x00--output-format\x00stream-json\x00")) { - ids = append(ids, child) - } - } - slices.Sort(ids) - return ids - } - run := func(id, prompt string, cancelOnText bool) turnEvidence { - t.Helper() - started := time.Now() - record := turnEvidence{Run: id, NodePID: owner.(*executor).base.process.Cmd.Process.Pid} - output := make(chan proto.Envelope, 128) - turn, err := owner.StartTurn(ctx, id, proto.TextInput(prompt), output) - if err != nil || turn == nil { - t.Fatal("Turn start failed", err) - } - cancellation := make(chan error, 1) - var cancelAt time.Time - for event := range output { - if event.ID != id { - t.Fatal("output crossed Turn identity") - } - if event.Type == proto.TypeDelta && record.FirstTextMS == 0 { - record.FirstTextMS = time.Since(started).Milliseconds() - record.NativePIDs = nativeChildren(record.NodePID) - if cancelOnText { - record.Cancelled = true - cancelAt = time.Now() - go func() { - cancelCtx, stop := context.WithTimeout(ctx, 30*time.Second) - defer stop() - cancellation <- turn.Cancel(cancelCtx) - }() - } - } - if event.Type == proto.TypeError { - var failure proto.ErrorPayload - _ = event.DecodePayload(&failure) - record.Errors = append(record.Errors, failure.Error) - } - if event.Type == proto.TypeDone { - _ = event.DecodePayload(&record.Done) - } - } - if record.Cancelled { - err := <-cancellation - record.CancelMS = time.Since(cancelAt).Milliseconds() - if err != nil { - record.CancellationError = err.Error() - } - } - record.Settlement, err = turn.AwaitSettlement(ctx) - if err != nil { - record.SettlementError = err.Error() - } - record.ElapsedMS = time.Since(started).Milliseconds() - evidence.Turns = append(evidence.Turns, record) - persist() - t.Logf("turn=%s bridge_pid=%d native_pids=%v reusable=%t first_text_ms=%d total_ms=%d", id, record.NodePID, record.NativePIDs, record.Settlement.Reusable, record.FirstTextMS, record.ElapsedMS) - return record - } - marker := "REUSE-" + uuid.NewString() - first := run("first", "Remember this marker: "+marker+". Reply with exactly the marker.", false) - if first.SettlementError != "" || !first.Settlement.Reusable || !strings.Contains(first.Done.Content, marker) { - t.Fatal("first Turn failed or was not reusable") - } - second := run("second", "What exact marker did I give you? Reply with only that marker.", false) - if second.SettlementError != "" || !second.Settlement.Reusable || !strings.Contains(second.Done.Content, marker) || first.NodePID != second.NodePID || len(first.NativePIDs) == 0 || !slices.Equal(first.NativePIDs, second.NativePIDs) { - t.Fatal("ordinary Turns did not retain native execution and history") - } - interrupted := run("cancel", "List the numbers 1 through 10000, one number per line, without stopping early.", true) - if !interrupted.Cancelled { - t.Fatal("native output ended before cancellation boundary") - } - if interrupted.SettlementError != "" || !interrupted.Settlement.Reusable { - if interrupted.Settlement.Reason == "" && interrupted.SettlementError == "" { - t.Fatal("invalid executor omitted its reason") - } - if err := owner.Close(ctx); err != nil { - t.Fatal("invalid owner cleanup was not confirmed", err) - } - native, _ := interrupted.Done.Metadata[proto.DoneMetaAgentSessionID].(string) - if native == "" { - native, _ = second.Done.Metadata[proto.DoneMetaAgentSessionID].(string) - } - request.AgentSessionID = native - request.RequireExistingNativeSession = true - owner, err = factory(ctx, request) - if err != nil { - t.Fatal("history recovery failed", err) - } - evidence.Recovered = true - } - continued := run("continued", "What exact marker did I originally give you? Reply with only the marker.", false) - if continued.SettlementError != "" || !continued.Settlement.Reusable || !strings.Contains(continued.Done.Content, marker) { - t.Fatal("history did not continue after cancellation") - } - if !evidence.Recovered && (continued.NodePID != second.NodePID || !slices.Equal(continued.NativePIDs, second.NativePIDs)) { - t.Fatal("reusable cancellation replaced native execution") - } -} diff --git a/apps/daemon/internal/agent/claudesdk/live_linux_test.go b/apps/daemon/internal/agent/claudesdk/live_linux_test.go deleted file mode 100644 index c3da9e8ef..000000000 --- a/apps/daemon/internal/agent/claudesdk/live_linux_test.go +++ /dev/null @@ -1,403 +0,0 @@ -//go:build linux - -package claudesdk - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/http/httputil" - "net/url" - "os" - "path/filepath" - "strconv" - "strings" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/google/uuid" -) - -func TestLiveClaudeSDKTextResume(t *testing.T) { - entrypoint := os.Getenv("OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT") - keyFile := os.Getenv("OAC_TEST_CLAUDE_SDK_MINIMAX_KEY_FILE") - if entrypoint == "" || keyFile == "" { - t.Skip("real SDK/provider acceptance requires explicit entrypoint and private key file") - } - key, err := os.ReadFile(keyFile) - if err != nil { - t.Fatal(err) - } - proofRoot := os.Getenv("OAC_TEST_CLAUDE_SDK_PROOF_DIR") - if !filepath.IsAbs(proofRoot) { - t.Fatal("OAC_TEST_CLAUDE_SDK_PROOF_DIR must be an absolute managed proof directory") - } - if err := os.MkdirAll(proofRoot, 0o700); err != nil { - t.Fatal(err) - } - root, err := os.MkdirTemp(proofRoot, "claude-adapter-") - if err != nil { - t.Fatal(err) - } - t.Setenv("OAC_RUNTIME_HOME", root) - target, _ := url.Parse("https://api.minimax.cn/anthropic") - proxy := httputil.NewSingleHostReverseProxy(target) - director := proxy.Director - proxy.Director = func(req *http.Request) { director(req); req.Host = target.Host } - proxy.ErrorHandler = func(w http.ResponseWriter, _ *http.Request, _ error) { - http.Error(w, "provider transport failed", http.StatusBadGateway) - } - var mu sync.Mutex - type providerRequest struct { - Model string `json:"model"` - Tools []struct { - Name string `json:"name"` - } `json:"tools"` - } - var requests []providerRequest - var delayNext atomic.Bool - var delayedRequests atomic.Int32 - forwarder := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if req.Method == "POST" && strings.HasSuffix(req.URL.Path, "/messages") { - body, err := io.ReadAll(req.Body) - if err != nil { - http.Error(w, "request read failed", 400) - return - } - _ = req.Body.Close() - req.Body = io.NopCloser(bytes.NewReader(body)) - var value providerRequest - _ = json.Unmarshal(body, &value) - mu.Lock() - requests = append(requests, value) - mu.Unlock() - } - if req.Method == "POST" && strings.HasSuffix(req.URL.Path, "/messages") && delayNext.Swap(false) { - delayedRequests.Add(1) - select { - case <-time.After(31 * time.Second): - case <-req.Context().Done(): - return - } - } - proxy.ServeHTTP(w, req) - })) - defer forwarder.Close() - config := Config{Entrypoint: entrypoint, StateDir: filepath.Join(root, "state"), Env: []string{ - "ANTHROPIC_BASE_URL=" + forwarder.URL, "ANTHROPIC_AUTH_TOKEN=" + strings.TrimSpace(string(key)), - "ANTHROPIC_API_KEY=", "CLAUDE_CODE_OAUTH_TOKEN=", "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", - "ANTHROPIC_DEFAULT_SONNET_MODEL=MiniMax-M3", "ANTHROPIC_DEFAULT_OPUS_MODEL=MiniMax-M3", "ANTHROPIC_DEFAULT_HAIKU_MODEL=MiniMax-M3", - }} - - // Ambient project configuration must not add a model tool or start a server. - work := filepath.Join(config.StateDir, "work") - if err := os.MkdirAll(filepath.Join(work, ".claude"), 0o700); err != nil { - t.Fatal(err) - } - canary := filepath.Join(root, "ambient-mcp-started") - ambient, _ := json.Marshal(map[string]any{"mcpServers": map[string]any{"ambient": map[string]any{ - "command": "node", "args": []string{"-e", "require('node:fs').writeFileSync(process.argv[1], 'unexpected')", canary}, - }}}) - if err := os.WriteFile(filepath.Join(work, ".mcp.json"), ambient, 0o600); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(work, ".claude", "settings.json"), []byte(`{"enableAllProjectMcpServers":true,"permissions":{"allow":["Bash","Read","Agent","WebSearch"]}}`), 0o600); err != nil { - t.Fatal(err) - } - type evidence struct { - ExecutionControls *proto.ExecutionControls `json:"execution_controls"` - SteeringText string `json:"steering_text,omitempty"` - SteeringWritten bool `json:"steering_written,omitempty"` - SteeringConfirmed bool `json:"steering_confirmed,omitempty"` - SteeringMilliseconds int64 `json:"steering_milliseconds,omitempty"` - SessionID string `json:"session_id"` - NodePID int `json:"node_pid"` - NativePIDs []int `json:"native_pids"` - ChildPIDs []int `json:"child_pids"` - Text string `json:"text"` - Failure string `json:"failure,omitempty"` - Events []proto.Envelope `json:"events"` - FunctionCalls int `json:"function_calls"` - AppliedResults int `json:"applied_results"` - ProviderRequests []providerRequest `json:"provider_requests"` - } - functionNonce := "function-" + uuid.NewString() - run := func(prompt, resume string, success *bool, steering ...string) evidence { - t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) - defer cancel() - mu.Lock() - requestStart := len(requests) - mu.Unlock() - out := make(chan proto.Envelope, 64) - request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": "MiniMax-M3", "system_prompt": "Answer briefly and preserve the exact verification value in the conversation. Use no tools."}} - if success != nil { - request.AgentOptions["system_prompt"] = "Call lookup exactly once as requested, then report both result parts and any prior verification value. Never retry a failed tool." - request.FunctionTools = []proto.FunctionTool{{Name: "lookup", Description: "Return a synthetic verification value.", Parameters: json.RawMessage(`{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}`)}} - } - running, err := startSingleTurn(ctx, config, request, out) - if err != nil { - t.Fatal(err) - } - s := running.(*session) - defer running.Cancel(context.Background()) - proof := evidence{NodePID: s.process.Cmd.Process.Pid, ExecutionControls: request.ExecutionControls} - if len(steering) > 0 { - proof.SteeringText = steering[0] - } - type steeringResult struct { - err error - elapsed int64 - written bool - } - steeringReply := make(chan steeringResult, 1) - var steeringAt time.Time - beginSteering := func() { - if proof.SteeringText == "" || !steeringAt.IsZero() { - return - } - steeringAt = time.Now() - if success != nil { - delayNext.Store(true) - } - go func() { - callCtx, cancel := context.WithCancel(ctx) - defer cancel() - timer := time.AfterFunc(10*time.Second, cancel) - defer timer.Stop() - written := false - err := s.SteerWithReceipt(callCtx, proto.PromptSteerPayload{InputID: uuid.NewString(), Input: proto.TextInput(proof.SteeringText)}, func() { written = timer.Stop() }) - steeringReply <- steeringResult{err: err, elapsed: time.Since(steeringAt).Milliseconds(), written: written} - }() - } - type children struct{ all, native []int } - observed := make(chan children, 1) - go func() { - pids := map[int]bool{} - native := map[int]bool{} - ticker := time.NewTicker(10 * time.Millisecond) - defer ticker.Stop() - for { - raw, _ := os.ReadFile(fmt.Sprintf("/proc/%d/task/%d/children", proof.NodePID, proof.NodePID)) - for _, value := range strings.Fields(string(raw)) { - if pid, err := strconv.Atoi(value); err == nil { - pids[pid] = true - // SDK history lookup may also spawn Git helpers; identify the execution transport. - args, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) - if bytes.Contains(args, []byte("\x00--input-format\x00stream-json\x00")) && - bytes.Contains(args, []byte("\x00--output-format\x00stream-json\x00")) { - native[pid] = true - } - } - } - select { - case <-s.process.Done(): - var result children - for pid := range pids { - result.all = append(result.all, pid) - } - for pid := range native { - result.native = append(result.native, pid) - } - observed <- result - return - case <-ticker.C: - } - } - }() - done := false - for event := range out { - proof.Events = append(proof.Events, event) - switch event.Type { - case proto.TypeDelta: - if success == nil { - beginSteering() - } - case proto.TypeToolCall: - var tool proto.ToolCallPayload - if err := event.DecodePayload(&tool); err != nil { - t.Fatal(err) - } - if tool.Observation == nil || tool.Observation.Kind != "function" || (tool.Stage != "before" && tool.Stage != "after") { - t.Fatal("invalid live neutral function observation") - } - if tool.Stage == "after" { - expectedStatus := "completed" - if success != nil && !*success { - expectedStatus = "failed" - } - if tool.Observation.Status != expectedStatus || tool.Observation.Content == nil || len(*tool.Observation.Content) != 2 { - t.Fatal("live result observation lost status or content") - } - } - case proto.TypeFunctionCall: - var call proto.FunctionCallPayload - if err := event.DecodePayload(&call); err != nil { - t.Fatal(err) - } - proof.FunctionCalls++ - beginSteering() - if success == nil || proof.FunctionCalls != 1 || call.Name != "lookup" { - t.Fatal("unexpected live function call") - } - first, second := functionNonce, "ordered-second-part" - if !*success { - first, second = "synthetic-current-failure", "do-not-retry" - } - value := proto.FunctionResultPayload{CallID: call.CallID, DeliveryID: uuid.NewString(), Success: *success, Content: []proto.InputContent{{Type: "input_text", Text: &first}, {Type: "input_text", Text: &second}}} - if err := s.SubmitFunctionResult(ctx, value); err != nil { - t.Fatalf("live native result receipt failed: %v; proof root %s", err, root) - } - proof.AppliedResults++ - case proto.TypeError: - var payload proto.ErrorPayload - _ = json.Unmarshal(event.Payload, &payload) - proof.Failure = payload.Error - case proto.TypeDone: - done = true - var payload proto.DonePayload - _ = json.Unmarshal(event.Payload, &payload) - proof.Text = payload.Content - proof.SessionID, _ = payload.Metadata[proto.DoneMetaAgentSessionID].(string) - } - } - // Done reports the Turn outcome; startSingleTurn closes its Executor - // after output settlement. Verify release at that boundary. - if _, err := s.AwaitSettlement(ctx); err != nil { - t.Fatal(err) - } - select { - case <-s.process.Done(): - case <-time.After(5 * time.Second): - t.Fatal("single-Turn Executor retained its process after settlement") - } - if !steeringAt.IsZero() { - receipt := <-steeringReply - if receipt.err != nil { - proof.Failure = "steering receipt: " + receipt.err.Error() - } else { - proof.SteeringConfirmed = true - } - proof.SteeringMilliseconds = receipt.elapsed - proof.SteeringWritten = receipt.written - } - released := <-observed - proof.NativePIDs, proof.ChildPIDs = released.native, released.all - if !done { - t.Fatal("no daemon completion before timeout") - } - for _, pid := range proof.ChildPIDs { - if value, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)); err == nil { - fields := strings.Fields(string(value)[strings.LastIndex(string(value), ")")+1:]) - if len(fields) == 0 || fields[0] != "Z" { - t.Fatalf("SDK child %d remains alive after Done", pid) - } - } - } - - mu.Lock() - proof.ProviderRequests = append([]providerRequest{}, requests[requestStart:]...) - mu.Unlock() - for _, sent := range proof.ProviderRequests { - if sent.Model != "MiniMax-M3" { - t.Fatal("unexpected provider model", sent.Model) - } - want := 0 - if success != nil { - want = 1 - } - if len(sent.Tools) != want { - t.Fatal("native tool inventory widened", sent.Tools) - } - for _, tool := range sent.Tools { - if tool.Name != "mcp__functions__lookup" { - t.Fatal("undeclared native tool", tool.Name) - } - } - } - if _, err := os.Stat(canary); !os.IsNotExist(err) { - t.Fatal("ambient MCP configuration was not excluded", err) - } - data, err := json.MarshalIndent(proof, "", " ") - if err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(root, fmt.Sprintf("execution-%d.json", proof.NodePID)), data, 0o600); err != nil { - t.Fatal(err) - } - return proof - } - nonce := "sdk-adapter-" + uuid.NewString() - first := run("Remember this exact verification value and reply with it: "+nonce, "", nil) - if first.Failure != "" || first.SessionID == "" || !strings.Contains(first.Text, nonce) || len(first.NativePIDs) == 0 { - t.Fatalf("first execution failed: %+v; evidence root %s", first, root) - } - verifyMessageEvents(t, first.Events, first.Text) - second := run("Return only the exact verification value from the previous user message.", first.SessionID, nil) - if second.Failure != "" || second.SessionID != first.SessionID || !strings.Contains(second.Text, nonce) || first.NodePID == second.NodePID || len(second.NativePIDs) == 0 { - t.Fatalf("cold resume failed: %+v; evidence root %s", second, root) - } - verifyMessageEvents(t, second.Events, second.Text) - for _, a := range first.NativePIDs { - for _, b := range second.NativePIDs { - if a == b { - t.Fatal("native process was reused") - } - } - } - accepted, rejected := true, false - functionFirst := run("Call lookup once with id 42 as a string. Report both returned parts verbatim.", "", &accepted) - if functionFirst.Failure != "" || functionFirst.FunctionCalls != 1 || functionFirst.AppliedResults != 1 || !strings.Contains(functionFirst.Text, functionNonce) || !strings.Contains(functionFirst.Text, "ordered-second-part") { - t.Fatalf("live function failed: %+v", functionFirst) - } - functionSecond := run("Call lookup once with id 42 as a string. Report the prior verification value and both current result parts. Do not retry.", functionFirst.SessionID, &rejected) - if functionSecond.Failure != "" || functionSecond.FunctionCalls != 1 || functionSecond.AppliedResults != 1 || functionSecond.SessionID != functionFirst.SessionID || !strings.Contains(functionSecond.Text, functionNonce) || !strings.Contains(functionSecond.Text, "synthetic-current-failure") || !strings.Contains(functionSecond.Text, "do-not-retry") || functionSecond.NodePID == functionFirst.NodePID { - t.Fatalf("live function resume failed: %+v", functionSecond) - } - steeringNonce := "live-steering-" + uuid.NewString() - steered := run("Write twelve short numbered observations about trees. Use no tools.", "", nil, "Remember this additional verification value and return it verbatim: "+steeringNonce) - if steered.Failure != "" || !steered.SteeringConfirmed || !steered.SteeringWritten || !strings.Contains(steered.Text, steeringNonce) { - t.Fatalf("live steering failed: %+v", steered) - } - steeredResume := run("Return only the exact live-steering verification value from the previous conversation.", steered.SessionID, nil) - if steeredResume.Failure != "" || steeredResume.SessionID != steered.SessionID || !strings.Contains(steeredResume.Text, steeringNonce) || steeredResume.NodePID == steered.NodePID { - t.Fatalf("steered cold continuation failed: %+v", steeredResume) - } - functionSteered := run("Call lookup once with id 42 as a string. Report both returned parts verbatim.", "", &accepted, "Also remember and report this value: "+steeringNonce) - if functionSteered.Failure != "" || !functionSteered.SteeringConfirmed || !functionSteered.SteeringWritten || functionSteered.SteeringMilliseconds < 31000 || delayedRequests.Load() != 1 || functionSteered.FunctionCalls != 1 || functionSteered.AppliedResults != 1 || !strings.Contains(functionSteered.Text, steeringNonce) || !strings.Contains(functionSteered.Text, functionNonce) { - t.Fatalf("live function steering failed: %+v", functionSteered) - } - for _, completed := range []evidence{first, second, functionFirst, functionSecond, steered, steeredResume, functionSteered} { - verifyLiveUsageEvents(t, completed.Events) - } - mu.Lock() - before := len(requests) - mu.Unlock() - missing := run("Say hello.", uuid.NewString(), nil) - mu.Lock() - measured := append([]providerRequest{}, requests...) - mu.Unlock() - if !strings.Contains(missing.Failure, "history_unavailable") || missing.SessionID != "" || len(missing.NativePIDs) != 0 || len(measured) != before { - t.Fatalf("missing history did not fail before native/model start: %+v", missing) - } - if len(measured) < 2 { - t.Fatal("expected real model requests") - } - for _, request := range measured { - if request.Model != "MiniMax-M3" { - t.Fatalf("unexpected requested model %q", request.Model) - } - } - data, _ := json.MarshalIndent(map[string]any{"scope": "private Go factory -> official SDK -> real MiniMax with default typed execution controls, active input, native continuation and disabled environment/subagent tools; public API not enabled; no filesystem isolation claim", "turns": []evidence{first, second, functionFirst, functionSecond, steered, steeredResume, functionSteered}, "missing_history": missing, "model_requests": measured, "controlled_provider_delay_seconds": 31, "delayed_requests": delayedRequests.Load()}, "", " ") - if err := os.WriteFile(filepath.Join(root, "proof.json"), data, 0o600); err != nil { - t.Fatal(err) - } - t.Logf("real adapter proof: %s", filepath.Join(root, "proof.json")) -} diff --git a/apps/daemon/internal/agent/claudesdk/native_controls_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/native_controls_live_linux_test.go deleted file mode 100644 index d3170948f..000000000 --- a/apps/daemon/internal/agent/claudesdk/native_controls_live_linux_test.go +++ /dev/null @@ -1,87 +0,0 @@ -//go:build linux - -package claudesdk - -import ( - "context" - "encoding/json" - "os" - "path/filepath" - "slices" - "strings" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/google/uuid" -) - -// TestLiveClaudeSDKNativeControls proves, on the real SDK and provider, a whitespace-only message -// and live web search on environment none. -func TestLiveClaudeSDKNativeControls(t *testing.T) { - entrypoint, keyFile, proofRoot := os.Getenv("OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT"), os.Getenv("OAC_TEST_CLAUDE_SDK_MINIMAX_KEY_FILE"), os.Getenv("OAC_TEST_CLAUDE_SDK_PROOF_DIR") - endpoint, model := os.Getenv("OAC_TEST_CLAUDE_SDK_BASE_URL"), os.Getenv("OAC_TEST_CLAUDE_SDK_MODEL") - if entrypoint == "" || keyFile == "" || endpoint == "" || model == "" || !filepath.IsAbs(proofRoot) { - t.Skip("real SDK/provider acceptance requires an entrypoint, a private key file, an endpoint, a model and an absolute proof directory") - } - key, err := os.ReadFile(keyFile) - if err != nil { - t.Fatal(err) - } - if err := os.MkdirAll(proofRoot, 0o700); err != nil { - t.Fatal(err) - } - root, err := os.MkdirTemp(proofRoot, "claude-controls-") - if err != nil { - t.Fatal(err) - } - t.Setenv("OAC_RUNTIME_HOME", root) - config := Config{Entrypoint: entrypoint, StateDir: filepath.Join(root, "state"), Env: []string{ - "ANTHROPIC_BASE_URL=" + endpoint, "ANTHROPIC_AUTH_TOKEN=" + strings.TrimSpace(string(key)), - "ANTHROPIC_API_KEY=", "CLAUDE_CODE_OAUTH_TOKEN=", - "ANTHROPIC_DEFAULT_SONNET_MODEL=" + model, "ANTHROPIC_DEFAULT_OPUS_MODEL=" + model, "ANTHROPIC_DEFAULT_HAIKU_MODEL=" + model, - }} - run := func(t *testing.T, prompt proto.MessageInput, configure func(*proto.PromptRequestPayload)) (items []string, content, failure string) { - t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) - defer cancel() - req := proto.PromptRequestPayload{RunID: uuid.NewString(), DisableExecutionEnvironment: true, DisableSubagents: true, Input: prompt, - ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, - AgentOptions: map[string]any{"model": model, "system_prompt": "Answer briefly."}} - configure(&req) - out := make(chan proto.Envelope, 64) - running, err := startSingleTurn(ctx, config, req, out) - if err != nil { - t.Fatal(err) - } - defer running.Cancel(context.Background()) - for event := range out { - items = append(items, event.Type) - switch event.Type { - case proto.TypeError: - var payload proto.ErrorPayload - _ = json.Unmarshal(event.Payload, &payload) - failure = payload.Error - case proto.TypeDone: - var payload proto.DonePayload - _ = json.Unmarshal(event.Payload, &payload) - content = payload.Content - } - } - return - } - t.Run("whitespace_only_message", func(t *testing.T) { - items, content, failure := run(t, proto.TextInput(" \n\t"), func(*proto.PromptRequestPayload) {}) - if failure != "" || content == "" { - t.Fatalf("whitespace-only message failed: items=%v failure=%q", items, failure) - } - }) - t.Run("live_search_on_none", func(t *testing.T) { - items, content, failure := run(t, proto.TextInput("Search the web for the current stable version of Go and report it in one sentence."), func(req *proto.PromptRequestPayload) { - req.ExecutionControls.WebSearch = &proto.WebSearchControl{Mode: "live", ContextSize: "medium"} - }) - if failure != "" || content == "" || !slices.Contains(items, proto.TypeToolCall) { - t.Fatalf("live search failed: items=%v failure=%q", items, failure) - } - }) -} diff --git a/apps/daemon/internal/agent/claudesdk/subagents_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/subagents_live_linux_test.go deleted file mode 100644 index 586c4c0b6..000000000 --- a/apps/daemon/internal/agent/claudesdk/subagents_live_linux_test.go +++ /dev/null @@ -1,197 +0,0 @@ -//go:build linux - -package claudesdk - -import ( - "bytes" - "context" - "encoding/json" - "os" - "path/filepath" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/google/uuid" -) - -// liveSubagentConfig reads the private provider settings of the real-provider Subagent tests. -func liveSubagentConfig(t *testing.T) (config Config, key []byte, root string) { - entrypoint, keyFile, proofRoot := os.Getenv("OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT"), os.Getenv("OAC_TEST_CLAUDE_SDK_MINIMAX_KEY_FILE"), os.Getenv("OAC_TEST_CLAUDE_SDK_PROOF_DIR") - endpoint, model := os.Getenv("OAC_TEST_CLAUDE_SDK_BASE_URL"), os.Getenv("OAC_TEST_CLAUDE_SDK_MODEL") - if entrypoint == "" || keyFile == "" || proofRoot == "" || endpoint == "" || model == "" { - t.Skip("real SDK/provider acceptance requires explicit entrypoint, key file, proof directory, base URL and model") - } - key, err := os.ReadFile(keyFile) - if err != nil { - t.Fatal(err) - } - key = bytes.TrimSpace(key) - root, err = os.MkdirTemp(proofRoot, "claude-subagent-") - if err != nil { - t.Fatal(err) - } - t.Setenv("OAC_RUNTIME_HOME", root) - return Config{Entrypoint: entrypoint, StateDir: filepath.Join(root, "state"), Env: []string{ - "ANTHROPIC_BASE_URL=" + endpoint, "ANTHROPIC_AUTH_TOKEN=" + string(key), "ANTHROPIC_API_KEY=", "CLAUDE_CODE_OAUTH_TOKEN=", - "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", "ANTHROPIC_DEFAULT_SONNET_MODEL=" + model, "ANTHROPIC_DEFAULT_OPUS_MODEL=" + model, "ANTHROPIC_DEFAULT_HAIKU_MODEL=" + model, - }}, key, root -} - -// saveEvidence stores the observed events of a live run, unless they would contain the provider key. -func saveEvidence(t *testing.T, root string, key []byte, evidence any) { - encoded, _ := json.MarshalIndent(evidence, "", " ") - if bytes.Contains(encoded, key) { - t.Error("evidence contains the provider key") - return - } - _ = os.WriteFile(filepath.Join(root, "evidence.json"), encoded, 0o600) - t.Logf("evidence: %s/evidence.json", root) -} - -// TestLiveClaudeSDKNativeSubagent runs one Turn in which the model spawns a native general-purpose Subagent with a per-call model. -func TestLiveClaudeSDKNativeSubagent(t *testing.T) { - config, key, root := liveSubagentConfig(t) - model := os.Getenv("OAC_TEST_CLAUDE_SDK_MODEL") - ctx, cancel := context.WithTimeout(context.Background(), 150*time.Second) - defer cancel() - out := make(chan proto.Envelope, 256) - request := proto.PromptRequestPayload{RunID: uuid.NewString(), ObserveSubagentIdentities: true, - Input: proto.TextInput("Call the Agent tool exactly once with subagent_type \"general-purpose\", model \"sonnet\", name \"helper\" and a prompt asking it to reply with the single word PONG. Then report what it replied."), - AgentOptions: map[string]any{"model": model}} - turn, err := startSingleTurn(ctx, config, request, out) - if err != nil { - t.Fatal(err) - } - defer turn.Cancel(context.Background()) - counts := map[string]int{} - var coordination []map[string]any - var events []proto.Envelope - defer func() { - saveEvidence(t, root, key, map[string]any{"counts": counts, "coordination": coordination, "events": events}) - }() - for event := range out { - events = append(events, event) - counts[event.Type]++ - switch event.Type { - case proto.TypeError: - t.Fatalf("native failure: %s", event.Payload) - case proto.TypeSubagentCoordination: - var fact map[string]any - _ = json.Unmarshal(event.Payload, &fact) - coordination = append(coordination, fact) - } - } - if _, err := turn.AwaitSettlement(ctx); err != nil { - t.Fatal(err) - } - if counts[proto.TypeSubagentIdentity] != 1 || counts[proto.TypeSubagentTurn] < 2 || counts[proto.TypeSubagentItem] == 0 { - t.Fatal("native Subagent was not observed") - } - var spawn map[string]any - for _, fact := range coordination { - if fact["kind"] == "create_subagent_call" { - spawn = fact - } - } - if spawn == nil || spawn["status"] != "completed" || spawn["model"] != "sonnet" { - t.Fatalf("spawn coordination did not report the requested model: %v", spawn) - } -} - -// TestLiveClaudeSDKSubagentFunctionCall runs one Turn in which a native Subagent calls the Agent's function. The call reaches Core with the child's -// identity after the child's facts, the submitted result is confirmed by the child's own native tool result, and the Turn reports no root tool call for it. -func TestLiveClaudeSDKSubagentFunctionCall(t *testing.T) { - config, key, root := liveSubagentConfig(t) - ctx, cancel := context.WithTimeout(context.Background(), 240*time.Second) - defer cancel() - out := make(chan proto.Envelope, 256) - request := proto.PromptRequestPayload{RunID: uuid.NewString(), ObserveSubagentIdentities: true, - FunctionTools: []proto.FunctionTool{{Name: "lookup", Description: "Returns the secret code of a topic.", Parameters: json.RawMessage(`{"type":"object","properties":{"topic":{"type":"string"}},"required":["topic"]}`)}}, - Input: proto.TextInput("Call the Agent tool exactly once with subagent_type \"general-purpose\", name \"helper\" and a prompt telling it to call the mcp__functions__lookup tool with topic \"sky\" and reply with exactly the code it returned. Then report that code."), - AgentOptions: map[string]any{"model": os.Getenv("OAC_TEST_CLAUDE_SDK_MODEL")}} - turn, err := startSingleTurn(ctx, config, request, out) - if err != nil { - t.Fatal(err) - } - defer turn.Cancel(context.Background()) - submitter := turn.(agent.FunctionResultSubmitter) - var events []proto.Envelope - var call proto.FunctionCallPayload - var tools, subagentTools, factsBefore int - submitted := make(chan error, 1) - defer func() { saveEvidence(t, root, key, events) }() - for event := range out { - events = append(events, event) - switch event.Type { - case proto.TypeError: - t.Fatalf("native failure: %s", event.Payload) - case proto.TypeSubagentIdentity, proto.TypeSubagentTurn: - if call.CallID == "" { - factsBefore++ - } - case proto.TypeToolCall: - tools++ - case proto.TypeSubagentItem: - if bytes.Contains(event.Payload, []byte(`"kind":"function"`)) { - subagentTools++ - } - case proto.TypeFunctionCall: - if err := event.DecodePayload(&call); err != nil { - t.Fatal(err) - } - text := "CODE-4821" - result := proto.FunctionResultPayload{DeliveryID: "delivery", CallID: call.CallID, Success: true, Content: []proto.InputContent{{Type: "input_text", Text: &text}}} - go func() { submitted <- submitter.SubmitFunctionResult(ctx, result) }() - } - } - if _, err := turn.AwaitSettlement(ctx); err != nil { - t.Fatal(err) - } - if call.NativeID == "" || call.TurnID == "" || factsBefore < 2 || tools != 0 || subagentTools == 0 { - t.Fatalf("child call %+v, facts before it %d, root tool calls %d, child function items %d", call, factsBefore, tools, subagentTools) - } - if err := <-submitted; err != nil { - t.Fatalf("child result was not confirmed: %v", err) - } -} - -// TestLiveClaudeSDKSubagentFunctionCallWithdrawn cancels the Turn while a Subagent's function call waits. Native stops the child, and the call is withdrawn. -func TestLiveClaudeSDKSubagentFunctionCallWithdrawn(t *testing.T) { - config, key, root := liveSubagentConfig(t) - ctx, cancel := context.WithTimeout(context.Background(), 240*time.Second) - defer cancel() - out := make(chan proto.Envelope, 256) - request := proto.PromptRequestPayload{RunID: uuid.NewString(), ObserveSubagentIdentities: true, - FunctionTools: []proto.FunctionTool{{Name: "lookup", Description: "Returns the secret code of a topic.", Parameters: json.RawMessage(`{"type":"object","properties":{"topic":{"type":"string"}},"required":["topic"]}`)}}, - Input: proto.TextInput("Call the Agent tool exactly once with subagent_type \"general-purpose\" and a prompt telling it to call the mcp__functions__lookup tool with topic \"sky\" and reply with the code it returned."), - AgentOptions: map[string]any{"model": os.Getenv("OAC_TEST_CLAUDE_SDK_MODEL")}} - turn, err := startSingleTurn(ctx, config, request, out) - if err != nil { - t.Fatal(err) - } - defer turn.Cancel(context.Background()) - var events []proto.Envelope - var call, withdrawn proto.FunctionCallWithdrawnPayload - var pending proto.FunctionCallPayload - defer func() { saveEvidence(t, root, key, events) }() - for event := range out { - events = append(events, event) - switch event.Type { - case proto.TypeFunctionCall: - if err := event.DecodePayload(&pending); err != nil { - t.Fatal(err) - } - go turn.Cancel(ctx) - case proto.TypeFunctionCallWithdrawn: - if err := event.DecodePayload(&withdrawn); err != nil { - t.Fatal(err) - } - } - } - call = proto.FunctionCallWithdrawnPayload{CallID: pending.CallID, NativeID: pending.NativeID, TurnID: pending.TurnID} - if pending.NativeID == "" || withdrawn != call { - t.Fatalf("call %+v was not withdrawn: %+v", pending, withdrawn) - } -} diff --git a/apps/daemon/internal/agent/claudesdk/usage_test.go b/apps/daemon/internal/agent/claudesdk/usage_test.go index d2ad262d5..d3c49bf3c 100644 --- a/apps/daemon/internal/agent/claudesdk/usage_test.go +++ b/apps/daemon/internal/agent/claudesdk/usage_test.go @@ -113,53 +113,3 @@ func runUsageHelper(request startRequest, mode string, encode func(bridgeEvent)) } encode(bridgeEvent{Type: "result", SessionID: id, Text: "final"}) } - -func verifyLiveUsageEvents(t *testing.T, events []proto.Envelope) { - t.Helper() - var observed proto.Usage - count := 0 - for _, event := range events { - switch event.Type { - case proto.TypeUsage: - var payload proto.UsagePayload - if err := event.DecodePayload(&payload); err != nil { - t.Fatal(err) - } - observed = payload.Usage - count++ - case proto.TypeDone: - var payload proto.DonePayload - if err := event.DecodePayload(&payload); err != nil { - t.Fatal(err) - } - if count < 1 || !reflect.DeepEqual(payload.Usage, observed) { - t.Fatal("missing, repeated or changed live usage") - } - } - } - if count < 1 || observed.Tokens != nil || observed.Model != "" { - t.Fatal("live native evidence became unsupported public accounting") - } - if count > 1 { - snapshots, ok := observed.Raw["claude_sdk_results"].([]any) - if !ok || len(snapshots) != count || !reflect.DeepEqual(snapshots[count-1], observed.Raw["claude_sdk_result"]) { - t.Fatal("lost native-turn snapshots") - } - } - snapshot, ok := observed.Raw["claude_sdk_result"].(map[string]any) - if !ok || snapshot["subtype"] != "success" || snapshot["is_error"] != false { - t.Fatal("missing native result provenance") - } - main, ok := snapshot["usage"].(map[string]any) - if !ok { - t.Fatal("missing main-loop usage") - } - output, ok := main["output_tokens"].(float64) - if !ok || output <= 0 { - t.Fatal("missing real output count") - } - models, ok := snapshot["modelUsage"].(map[string]any) - if !ok || len(models) == 0 { - t.Fatal("missing per-model native usage") - } -} diff --git a/apps/daemon/internal/agent/claudesdk/workspace_commands_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/workspace_commands_live_linux_test.go deleted file mode 100644 index b2ff0fabe..000000000 --- a/apps/daemon/internal/agent/claudesdk/workspace_commands_live_linux_test.go +++ /dev/null @@ -1,52 +0,0 @@ -//go:build linux - -package claudesdk - -import ( - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -func liveWorkspaceCommands(t *testing.T, runID string, events []proto.Envelope, commands []string) []proto.ToolCallPayload { - t.Helper() - started := map[string]string{} - finished := map[string]bool{} - var complete []proto.ToolCallPayload - terminal := false - for _, event := range events { - if event.Type == proto.TypeDone { - terminal = true - } - if event.Type != proto.TypeToolCall { - continue - } - var call proto.ToolCallPayload - if terminal || event.ID != runID || event.DecodePayload(&call) != nil || call.ID == "" || call.Observation == nil || call.Observation.Kind != "command" { - t.Fatal("invalid command frame or execution identity") - } - o := call.Observation - if o.ExitCode != nil || o.Cwd != nil || o.DurationMS != nil { - t.Fatal("command observation invented unavailable native metadata") - } - switch call.Stage { - case "before": - if len(started) >= len(commands) || started[call.ID] != "" || o.Command != commands[len(started)] || o.Status != "in_progress" || len(o.Output) != 0 { - t.Fatal("unexpected, duplicate or fabricated command start") - } - started[call.ID] = o.Command - case "after": - if started[call.ID] != o.Command || finished[call.ID] || o.Status == "in_progress" { - t.Fatal("command completion lacks a unique matching start") - } - finished[call.ID] = true - complete = append(complete, call) - default: - t.Fatal("invalid command stage") - } - } - if len(complete) != len(commands) || len(started) != len(commands) { - t.Fatal("command observations missing or replayed from an earlier query") - } - return complete -} diff --git a/apps/daemon/internal/agent/claudesdk/workspace_directory_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/workspace_directory_live_linux_test.go deleted file mode 100644 index d6afad379..000000000 --- a/apps/daemon/internal/agent/claudesdk/workspace_directory_live_linux_test.go +++ /dev/null @@ -1,103 +0,0 @@ -//go:build linux - -package claudesdk - -import ( - "context" - "encoding/json" - "errors" - "io/fs" - "os" - "path/filepath" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" -) - -func liveWorkspaceDirectoryFixtures(t *testing.T, root string) { - t.Helper() - for _, name := range []string{"directory-empty", "directory-denied"} { - if err := os.Mkdir(filepath.Join(root, name), 0700); err != nil { - t.Fatal(err) - } - } - if err := os.Chmod(filepath.Join(root, "directory-denied"), 0); err != nil { - t.Fatal(err) - } - for name, target := range map[string]string{"directory-inside-link": "directory-empty", "directory-outside-link": filepath.Dir(root)} { - if err := os.Symlink(target, filepath.Join(root, name)); err != nil { - t.Fatal(err) - } - } -} - -func liveWorkspaceDirectories(t *testing.T, ctx context.Context, reader agent.WorkspaceReader, root, stage string) { - t.Helper() - lister, ok := reader.(agent.WorkspaceDirectoryLister) - if !ok { - t.Fatal("workspace directory owner missing") - } - result, err := lister.ListWorkspaceDirectory(ctx, "", 1000) - if err != nil || result.Truncated { - t.Fatal("workspace directory failed", stage, err) - } - expected, err := os.ReadDir(root) - if err != nil || len(expected) != len(result.Entries) { - t.Fatal("directory length differs", stage, err) - } - found := make(map[string]agent.WorkspaceDirectoryEntry) - for _, entry := range result.Entries { - found[entry.Name] = entry - } - for _, entry := range expected { - actual, ok := found[entry.Name()] - if !ok { - t.Fatal("missing directory entry", stage, entry.Name()) - } - stat, err := os.Lstat(filepath.Join(root, entry.Name())) - if err != nil { - t.Fatal(err) - } - switch { - case stat.Mode().IsRegular(): - // The command heartbeat is intentionally changing during live execution. - if actual.Kind != "file" || actual.SizeBytes == nil || (entry.Name() != "heartbeat.txt" && *actual.SizeBytes != stat.Size()) { - t.Fatal("file metadata differs", stage, actual) - } - case stat.IsDir(): - if actual.Kind != "directory" || actual.SizeBytes != nil { - t.Fatal(actual) - } - case stat.Mode()&os.ModeSymlink != 0: - if actual.Kind != "symlink" || actual.SizeBytes != nil { - t.Fatal(actual) - } - } - } - empty, err := lister.ListWorkspaceDirectory(ctx, "directory-empty", 2) - if err != nil || empty.Truncated || len(empty.Entries) != 0 { - t.Fatal(empty, err) - } - bounded, err := lister.ListWorkspaceDirectory(ctx, "", 1) - if err != nil || !bounded.Truncated || len(bounded.Entries) != 1 { - t.Fatal(bounded, err) - } - for _, path := range []string{"directory-inside-link", "directory-outside-link"} { - if _, err := lister.ListWorkspaceDirectory(ctx, path, 2); !errors.Is(err, fs.ErrPermission) && !errors.Is(err, agent.ErrWorkspaceReadInvalid) { - t.Fatal("directory denial missing", path, err) - } - } - if _, err := lister.ListWorkspaceDirectory(ctx, "directory-denied", 2); !errors.Is(err, fs.ErrPermission) { - t.Fatal("permission denial missing", err) - } - if _, err := lister.ListWorkspaceDirectory(ctx, "directory-missing", 2); !errors.Is(err, fs.ErrNotExist) { - t.Fatal(err) - } - raw, err := json.MarshalIndent(result, "", " ") - if err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(filepath.Dir(root), "directory-"+stage+".json"), raw, 0600); err != nil { - t.Fatal(err) - } -} diff --git a/apps/daemon/internal/agent/claudesdk/workspace_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/workspace_live_linux_test.go deleted file mode 100644 index df08c4bb2..000000000 --- a/apps/daemon/internal/agent/claudesdk/workspace_live_linux_test.go +++ /dev/null @@ -1,233 +0,0 @@ -//go:build linux - -package claudesdk - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/google/uuid" -) - -// Run only inside a separately qualified outer placement, with its pinned native -// dependencies. This fixture does not create isolation or public admission. -func TestLiveClaudeWorkspaceFactory(t *testing.T) { - configFile := os.Getenv("OAC_TEST_CLAUDE_WORKSPACE_LIVE_CONFIG") - if configFile == "" { - t.Skip("requires explicit qualified placement and real provider configuration") - } - var placement struct { - Node, Entrypoint, Proof, Scratch, KeyFile, DependencyPath, Proxy string - } - raw, err := os.ReadFile(configFile) - if err != nil || json.Unmarshal(raw, &placement) != nil { - t.Fatal("invalid private live configuration") - } - root, err := os.MkdirTemp(placement.Proof, "factory-") - if err != nil { - t.Fatal(err) - } - t.Logf("workspace factory proof: %s", root) - t.Setenv("OAC_RUNTIME_HOME", root) - t.Setenv("OAC_TEST_PARENT_SECRET", "parent-must-not-enter-workspace") - key, err := os.ReadFile(placement.KeyFile) - if err != nil || len(bytes.TrimSpace(key)) == 0 { - t.Fatal("private real-provider key unavailable") - } - scratch, err := os.MkdirTemp(placement.Scratch, "f-") - if err != nil { - t.Fatal(err) - } - config := Config{Node: placement.Node, Entrypoint: placement.Entrypoint, StateDir: filepath.Join(root, "state"), - Workspace: &WorkspaceConfig{Directory: filepath.Join(root, "workspace"), HomeDir: filepath.Join(root, "home"), - ScratchDir: scratch}, - Env: []string{"ANTHROPIC_BASE_URL=https://api.minimax.cn/anthropic", "ANTHROPIC_API_KEY=", "ANTHROPIC_AUTH_TOKEN=" + strings.TrimSpace(string(key)), - "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", "ANTHROPIC_DEFAULT_SONNET_MODEL=MiniMax-M3", "ANTHROPIC_DEFAULT_OPUS_MODEL=MiniMax-M3", "ANTHROPIC_DEFAULT_HAIKU_MODEL=MiniMax-M3"}} - if placement.Proxy != "" { - config.Env = append(config.Env, "HTTP_PROXY="+placement.Proxy, "HTTPS_PROXY="+placement.Proxy, "NO_PROXY=127.0.0.1,localhost") - } - for _, dir := range []string{config.StateDir, config.Workspace.Directory, config.Workspace.HomeDir} { - if err := os.Mkdir(dir, 0o700); err != nil { - t.Fatal(err) - } - } - liveWorkspaceReadFixtures(t, config.Workspace.Directory) - heartbeat := filepath.Join(config.Workspace.Directory, "heartbeat.txt") - artifact := filepath.Join(config.Workspace.Directory, "value.txt") - type evidence struct { - Reads []liveWorkspaceRead `json:"reads,omitempty"` - RunID string `json:"run_id"` - Events []proto.Envelope `json:"events"` - Done proto.DonePayload `json:"done"` - Failure string `json:"failure,omitempty"` - Cancelled bool `json:"cancelled"` - CancelMS int64 `json:"cancel_ms,omitempty"` - BridgePID int `json:"bridge_pid"` - Terminals int `json:"terminals"` - Heartbeats []string `json:"heartbeats,omitempty"` - } - writeEvidence := func(name string, proof evidence) { - t.Helper() - encoded, err := json.MarshalIndent(proof, "", " ") - if err != nil || bytes.Contains(encoded, bytes.TrimSpace(key)) { - t.Fatal("cannot safely encode factory observations") - } - if err := os.WriteFile(filepath.Join(root, name+".json"), encoded, 0o600); err != nil { - t.Fatal(err) - } - } - run := func(name, prompt, resume string, cancelOnEffect bool) evidence { - t.Helper() - ctx, cancel := context.WithTimeout(t.Context(), 180*time.Second) - defer cancel() - out := make(chan proto.Envelope, 64) - req := workspaceRequest() - req.RunID, req.Input, req.AgentSessionID = uuid.NewString(), proto.TextInput(prompt), resume - req.AgentOptions = map[string]any{"model": "MiniMax-M3", "system_prompt": "Follow the exact verification instructions using the requested native tools. Preserve conversation facts. No other files, network operations or background work."} - proof := evidence{RunID: req.RunID} - running, err := startSingleTurn(ctx, config, req, out) - if err != nil { - if name == "missing-history" && running == nil && strings.Contains(err.Error(), "history_unavailable") { - proof.Failure = err.Error() - writeEvidence(name, proof) - return proof - } - t.Fatal(err) - } - s := running.(*session) - defer s.Cancel(context.Background()) - proof.BridgePID = s.process.Cmd.Process.Pid - proof.Reads = append(proof.Reads, liveWorkspaceReads(t, ctx, s, config.Workspace.Directory, "active", "read-binary.bin", "read-empty.bin", "read-large.bin")...) - ticker := time.NewTicker(80 * time.Millisecond) - defer ticker.Stop() - for out != nil { - select { - case <-ctx.Done(): - t.Fatal("real factory deadline expired") - case <-ticker.C: - value, _ := os.ReadFile(heartbeat) - if cancelOnEffect && !proof.Cancelled && len(value) > 0 && string(value) != "0" && string(value) != "1" { - proof.Reads = append(proof.Reads, liveWorkspaceReads(t, ctx, s, config.Workspace.Directory, "effect", "value.txt")...) - started := time.Now() - if err := s.Cancel(ctx); err != nil { - t.Fatal("factory cancellation failed", err) - } - proof.CancelMS = time.Since(started).Milliseconds() - proof.Cancelled = true - } - case event, ok := <-out: - if !ok { - out = nil - continue - } - proof.Events = append(proof.Events, event) - switch event.Type { - case proto.TypeError: - var failure proto.ErrorPayload - _ = event.DecodePayload(&failure) - proof.Failure = failure.Error - case proto.TypeDone: - proof.Terminals++ - _ = event.DecodePayload(&proof.Done) - } - } - } - if proof.Cancelled { - a, _ := os.ReadFile(heartbeat) - time.Sleep(1500 * time.Millisecond) - b, _ := os.ReadFile(heartbeat) - proof.Heartbeats = []string{string(a), string(b)} - if len(a) == 0 || !bytes.Equal(a, b) { - t.Fatal("native command effects continued after Cancel") - } - settled, _ := json.Marshal(s.CancellationOutcome()) - done, _ := json.Marshal(proof.Done) - if !bytes.Equal(settled, done) { - t.Fatal("cancellation outcome differs from terminal Done") - } - } - // Native history settles when the executor closes after Done; the next step must not race that close. - select { - case <-s.process.Done(): - case <-ctx.Done(): - t.Fatal("native process did not exit after the Turn") - } - writeEvidence(name, proof) - if proof.Terminals != 1 || (cancelOnEffect && !proof.Cancelled) { - t.Fatal("missing actual cancellation or unique completion") - } - return proof - } - commands := []string{ - `python3 -c "import sys; print('OBS_SUCCESS_STDOUT'); print('OBS_SUCCESS_STDERR', file=sys.stderr)"`, - `python3 -c "import sys; print('OBS_FAILURE_STDOUT'); print('OBS_FAILURE_STDERR', file=sys.stderr); sys.exit(7)"`, - } - observed := run("commands", fmt.Sprintf("Execute exactly these two foreground Bash calls, sequentially, with timeout 10000. Preserve each command exactly. The second intentionally fails; do not retry or repair it. Use no other tools.\n1. %s\n2. %s", commands[0], commands[1]), "", false) - observedID, _ := observed.Done.Metadata[proto.DoneMetaAgentSessionID].(string) - if observedID == "" || observed.Failure != "" { - t.Fatal("real command observation query failed") - } - completed := liveWorkspaceCommands(t, observed.RunID, observed.Events, commands) - for i, expected := range []struct{ status, output string }{ - {"completed", "OBS_SUCCESS_STDERR\nOBS_SUCCESS_STDOUT"}, - {"failed", "Exit code 7\nOBS_FAILURE_STDERR\nOBS_FAILURE_STDOUT"}, - } { - var output string - if completed[i].Observation.Status != expected.status || json.Unmarshal(completed[i].Observation.Output, &output) != nil || output != expected.output { - t.Fatal("native command result text/status was not preserved") - } - } - nonce := "conversation-" + uuid.NewString() - command := "python3 -u - <<'VERIFY_PY'\nimport secrets,time\nfrom pathlib import Path\nPath('value.txt').write_text(secrets.token_hex(16)+'\\n')\nfor n in range(180):\n Path('heartbeat.txt').write_text(str(n))\n time.sleep(1)\nVERIFY_PY" - first := run("first", fmt.Sprintf("Remember the conversation-only value %s; do not write it into any file. Execute exactly one foreground Bash call with timeout 120000 and this exact command. Wait for it; use no other tools.\n%s", nonce, command), observedID, true) - id, _ := first.Done.Metadata[proto.DoneMetaAgentSessionID].(string) - if id == "" || id != observedID { - t.Fatal("cancelled native Session identity unavailable") - } - interrupted := liveWorkspaceCommands(t, first.RunID, first.Events, []string{command}) - if interrupted[0].ID == completed[0].ID || interrupted[0].ID == completed[1].ID || - (interrupted[0].Observation.Status != "incomplete" && interrupted[0].Observation.Status != "failed") || - (interrupted[0].Observation.Status == "failed" && len(interrupted[0].Observation.Output) == 0) { - t.Fatal("cancelled command lost its native failure or incomplete observation") - } - original, err := os.ReadFile(artifact) - if err != nil || len(bytes.TrimSpace(original)) != 32 { - t.Fatal("actual workspace artifact missing") - } - second := run("resumed", "Use native Read to read value.txt. Then use native Edit to append the literal suffix -resumed to its value, retaining a trailing newline. Do not use Bash. Reply with the original file value and the conversation-only value remembered earlier.", id, false) - liveWorkspaceCommands(t, second.RunID, second.Events, nil) - final, err := os.ReadFile(artifact) - if err != nil || string(final) != strings.TrimSpace(string(original))+"-resumed\n" || second.Failure != "" || - second.Done.Metadata[proto.DoneMetaAgentSessionID] != id || !strings.Contains(second.Done.Content, nonce) || - !strings.Contains(second.Done.Content, strings.TrimSpace(string(original))) || first.BridgePID == second.BridgePID { - t.Fatal("fresh-process native workspace/history continuation failed") - } - retained := config.StateDir + "-retained" - if err := os.Rename(config.StateDir, retained); err != nil { - t.Fatal(err) - } - defer func() { - _ = os.Remove(config.StateDir) - _ = os.Rename(retained, config.StateDir) - }() - if err := os.Mkdir(config.StateDir, 0o700); err != nil { - t.Fatal(err) - } - missing := run("missing-history", "Continue the existing Session only; do not start another Session.", id, false) - entries, err := os.ReadDir(config.StateDir) - after, _ := os.ReadFile(artifact) - if missing.Failure != "claudesdk: history_unavailable" || missing.Terminals != 0 || err != nil || len(entries) != 0 || !bytes.Equal(final, after) || missing.Done.Metadata[proto.DoneMetaAgentSessionID] != nil { - t.Fatal("missing native history did not fail before new execution") - } - if err := os.RemoveAll(scratch); err != nil { - t.Fatal(err) - } -} diff --git a/apps/daemon/internal/agent/claudesdk/workspace_read_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/workspace_read_live_linux_test.go deleted file mode 100644 index e721126e7..000000000 --- a/apps/daemon/internal/agent/claudesdk/workspace_read_live_linux_test.go +++ /dev/null @@ -1,58 +0,0 @@ -//go:build linux - -package claudesdk - -import ( - "bytes" - "context" - "crypto/sha256" - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" -) - -type liveWorkspaceRead struct { - Stage string `json:"stage"` - Path string `json:"path"` - Bytes int `json:"bytes"` - SHA256 string `json:"sha256"` - Truncated bool `json:"truncated"` -} - -func liveWorkspaceReadFixtures(t *testing.T, root string) { - t.Helper() - liveWorkspaceDirectoryFixtures(t, root) - for path, data := range map[string][]byte{"read-binary.bin": bytes.Repeat([]byte{0, 255, 128, 1}, 64), "read-empty.bin": {}, "read-large.bin": bytes.Repeat([]byte{0, 255, 128, 1}, (workspaceReadMaxBytes+40)/4)} { - if err := os.WriteFile(filepath.Join(root, path), data, 0600); err != nil { - t.Fatal(err) - } - } -} - -func liveWorkspaceReads(t *testing.T, ctx context.Context, reader agent.WorkspaceReader, root, stage string, paths ...string) []liveWorkspaceRead { - t.Helper() - liveWorkspaceDirectories(t, ctx, reader, root, stage) - var proof []liveWorkspaceRead - for _, path := range paths { - expected, err := os.ReadFile(filepath.Join(root, path)) - if err != nil { - t.Fatal(err) - } - result, err := reader.ReadWorkspaceFile(ctx, path, workspaceReadMaxBytes) - if err != nil { - t.Fatal("actual native workspace read failed", stage, path, err) - } - truncated := len(expected) > workspaceReadMaxBytes - if truncated { - expected = expected[:workspaceReadMaxBytes] - } - if !bytes.Equal(result.Data, expected) || result.Truncated != truncated { - t.Fatal("native bytes differ", stage, path, len(result.Data), result.Truncated) - } - proof = append(proof, liveWorkspaceRead{Stage: stage, Path: path, Bytes: len(result.Data), SHA256: fmt.Sprintf("%x", sha256.Sum256(result.Data)), Truncated: result.Truncated}) - } - return proof -} diff --git a/apps/daemon/internal/agent/codex/environment_local.go b/apps/daemon/internal/agent/codex/environment_local.go index 9fa3f3c2e..9f59aada0 100644 --- a/apps/daemon/internal/agent/codex/environment_local.go +++ b/apps/daemon/internal/agent/codex/environment_local.go @@ -1,7 +1,6 @@ package codex import ( - "os" "runtime" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" @@ -9,7 +8,7 @@ import ( // SupportsLocalEnvironment checks deployment prerequisites, not public admission. func SupportsLocalEnvironment(version string) bool { - if !SupportsNativeSessionRecovery(version) || os.Getenv("OAC_RUNTIME_CODEX_HARNESS_BIN") != "" || (runtime.GOOS != "linux" && runtime.GOOS != "darwin" && runtime.GOOS != "windows") { + if !SupportsNativeSessionRecovery(version) || (runtime.GOOS != "linux" && runtime.GOOS != "darwin" && runtime.GOOS != "windows") { return false } binding, err := localworkspace.Load() diff --git a/apps/daemon/internal/agent/codex/executor_native_test.go b/apps/daemon/internal/agent/codex/executor_native_test.go deleted file mode 100644 index 2f6150630..000000000 --- a/apps/daemon/internal/agent/codex/executor_native_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package codex - -import ( - "context" - "encoding/json" - "log/slog" - "os" - "os/exec" - "strings" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -// nativeAcceptanceConfig returns the pinned binary and the explicitly -// configured Responses provider of an opt-in native test, and gives the test an -// isolated Runtime home. Credentials are read from a private file, never -// arguments. -func nativeAcceptanceConfig(t *testing.T) (binary string, options map[string]any) { - t.Helper() - if os.Getenv("OAC_TEST_CODEX_EXECUTOR_NATIVE") != "1" { - t.Skip("requires an authorized native model acceptance environment") - } - binary, root := os.Getenv("OAC_TEST_CODEX_BINARY"), os.Getenv("OAC_TEST_CODEX_LIVE_ROOT") - model, endpoint := os.Getenv("OAC_TEST_CODEX_MODEL"), os.Getenv("OAC_TEST_CODEX_BASE_URL") - if binary == "" || root == "" || model == "" || endpoint == "" { - t.Fatal("missing explicit native acceptance configuration") - } - version, err := exec.Command(binary, "--version").Output() - if err != nil || strings.TrimSpace(string(version)) != "codex-cli 0.153.4" { - t.Fatal("native artifact is not Codex 0.153.4") - } - key, err := os.ReadFile(os.Getenv("OAC_TEST_CODEX_KEY_FILE")) - if err != nil || strings.TrimSpace(string(key)) == "" { - t.Fatal("private provider credential unavailable") - } - if err = os.MkdirAll(root, 0700); err != nil { - t.Fatal("cannot create isolated acceptance root") - } - isolated, err := os.MkdirTemp(root, "run-") - if err != nil { - t.Fatal("cannot create isolated acceptance workspace") - } - t.Cleanup(func() { _ = os.RemoveAll(isolated) }) - t.Setenv("OAC_RUNTIME_HOME", isolated) - for _, name := range []string{"CODEX_EXEC_SERVER_URL", "CODEX_EXEC_SERVER_NOISE_REGISTRY_URL", "CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID", "CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN"} { - t.Setenv(name, "") - } - return binary, map[string]any{"model": model, "model_provider": map[string]any{"base_url": endpoint, "protocol": "responses", "api_key": strings.TrimSpace(string(key))}} -} - -func TestExecutorNativeReuse(t *testing.T) { - binary, options := nativeAcceptanceConfig(t) - cfg := defaultSessionConfig(testStateRoot(t)) - cfg.codexBinary = binary - cfg.logger = slog.New(slog.DiscardHandler) - req := proto.PromptRequestPayload{ - AgentKind: "codex", AgentStateKey: "executor-native", - DisableExecutionEnvironment: true, DisableSubagents: true, - AgentOptions: options, - FunctionTools: []proto.FunctionTool{{Name: "hold", Description: "Wait until the host supplies a result.", Parameters: json.RawMessage("{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}")}}, - } - ctx, cancel := context.WithTimeout(t.Context(), 4*time.Minute) - defer cancel() - began := time.Now() - e, err := newExecutor(ctx, req, cfg) - if err != nil { - t.Fatalf("native prepare failed (%T)", err) - } - t.Cleanup(func() { - c, stop := context.WithTimeout(context.Background(), 20*time.Second) - defer stop() - if err := e.Close(c); err != nil { - t.Errorf("native owner cleanup failed (%T)", err) - } - }) - t.Logf("native_version=0.153.4 prepare_ms=%d", time.Since(began).Milliseconds()) - pid := e.prepared.session.rpc.cmd.Process.Pid - var thread string - run := func(id, prompt string, old agent.Turn, interrupt bool) (agent.Turn, string) { - t.Helper() - output := make(chan proto.Envelope, 512) - started := time.Now() - turn, err := e.StartTurn(ctx, id, proto.TextInput(prompt), output) - if err != nil { - t.Fatalf("%s start failed (%T)", id, err) - } - if old != nil { - if err := old.Cancel(ctx); err != nil { - t.Fatalf("%s stale cancellation failed", id) - } - } - done := make(chan struct{}) - called := make(chan struct{}, 1) - var text string - var terminal int - var first time.Duration - go func() { - defer close(done) - for event := range output { - if first == 0 { - first = time.Since(started) - } - if event.Type == proto.TypeFunctionCall { - select { - case called <- struct{}{}: - default: - } - } - if event.Type == proto.TypeDone { - var result proto.DonePayload - _ = event.DecodePayload(&result) - text = result.Content - terminal++ - } - } - }() - if interrupt { - select { - case <-called: - case <-done: - t.Fatal("native cancellation did not reach its function") - case <-ctx.Done(): - t.Fatal("native function admission timed out") - } - cancelStarted := time.Now() - if err := turn.Cancel(ctx); err != nil { - t.Fatalf("native interrupt failed (%T)", err) - } - t.Logf("turn=%s cancel_settled_ms=%d", id, time.Since(cancelStarted).Milliseconds()) - } - settlement, err := turn.AwaitSettlement(ctx) - if err != nil || !settlement.Reusable { - t.Fatalf("%s native settlement not reusable (error=%t reason=%s)", id, err != nil, settlement.Reason) - } - <-done - if terminal != 1 { - t.Fatalf("%s emitted %d terminals", id, terminal) - } - s := turn.(*Session) - if thread == "" { - thread = s.currentThreadID() - } - if thread == "" || s.currentThreadID() != thread || !s.rpc.Alive() || s.rpc.cmd.Process.Pid != pid { - t.Fatal("native owner/thread changed") - } - t.Logf("turn=%s first_event_ms=%d settled_ms=%d same_process=true same_thread=true", id, first.Milliseconds(), time.Since(started).Milliseconds()) - return turn, text - } - first, _ := run("cold", "Remember the exact code CEDAR-4729 for this conversation. Reply only SAVED. Do not call tools.", nil, false) - _, answer := run("warm", "What is the exact code I asked you to remember? Reply only the code. Do not call tools.", nil, false) - if strings.TrimSpace(answer) != "CEDAR-4729" { - t.Fatal("warm native Turn did not retain conversation memory") - } - cancelled, _ := run("cancel", "Call the hold function now with an empty object. Wait for its result before responding.", nil, true) - _, answer = run("followup", "What is the exact code I asked you to remember? Reply only the code. Do not call tools.", cancelled, false) - if strings.TrimSpace(answer) != "CEDAR-4729" { - t.Fatal("post-cancellation native Turn lost conversation memory") - } - if err := first.Cancel(ctx); err != nil { - t.Fatal("retired first Turn cancellation failed") - } - if err := e.Close(ctx); err != nil { - t.Fatal("native final cleanup failed") - } - if e.prepared.session.rpc.Alive() { - t.Fatal("native child remained alive after owner close") - } -} diff --git a/apps/daemon/internal/agent/codex/subagent_tools_native_test.go b/apps/daemon/internal/agent/codex/subagent_tools_native_test.go deleted file mode 100644 index 4d0840434..000000000 --- a/apps/daemon/internal/agent/codex/subagent_tools_native_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package codex - -import ( - "context" - "encoding/json" - "io" - "log/slog" - "net/http" - "net/http/httptest" - "strings" - "sync" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -// This opt-in test runs the real pinned harness with a model that spawns a -// Subagent, which calls a function or an MCP tool of the Agent. -func TestSubagentToolsNative(t *testing.T) { - binary, options := nativeAcceptanceConfig(t) - mcpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, _ := io.ReadAll(r.Body) - var rpc struct { - ID json.RawMessage `json:"id"` - Method string `json:"method"` - } - _ = json.Unmarshal(body, &rpc) - var result string - switch rpc.Method { - case "initialize": - result = `{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"proof","version":"0"}}` - case "tools/list": - result = `{"tools":[{"name":"echo","description":"Return the synthetic MCP value.","inputSchema":{"type":"object","properties":{}}}]}` - case "tools/call": - result = `{"content":[{"type":"text","text":"MCP-5521"}]}` - default: - w.WriteHeader(http.StatusAccepted) - return - } - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":` + string(rpc.ID) + `,"result":` + result + `}`)) - })) - defer mcpServer.Close() - for name, prompt := range map[string]string{ - "function": "Call spawn_agent with fork_context true and the task: call the lookup function once and report the value it returns. Then wait for that Subagent and reply with its value. Do not call lookup yourself.", - "mcp": "Call spawn_agent with a task: call the echo tool of the proof MCP server once and report the value it returns. Then wait for that Subagent and reply with its value. Do not call echo yourself.", - } { - t.Run(name, func(t *testing.T) { - cfg := defaultSessionConfig(testStateRoot(t)) - cfg.codexBinary = binary - cfg.logger = slog.New(slog.DiscardHandler) - servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "proof", ServerURL: mcpServer.URL}} - req := proto.PromptRequestPayload{ - AgentKind: "codex", AgentStateKey: "subagent-tools-" + name, - DisableExecutionEnvironment: true, ObserveSubagentIdentities: true, - AgentOptions: options, - FunctionTools: []proto.FunctionTool{{Name: "lookup", Description: "Return the synthetic verification value.", Parameters: json.RawMessage(`{"type":"object","properties":{},"additionalProperties":false}`)}}, - MCPHTTPServers: &servers, - } - ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute) - defer cancel() - e, err := newExecutor(ctx, req, cfg) - if err != nil { - t.Fatalf("native prepare failed (%T)", err) - } - defer e.Close(context.Background()) - output := make(chan proto.Envelope, 512) - turn, err := e.StartTurn(ctx, name, proto.TextInput(prompt), output) - if err != nil { - t.Fatal(err) - } - var calls []proto.FunctionCallPayload - var childTools []string - var answer string - var answers sync.WaitGroup - for event := range output { - switch event.Type { - case proto.TypeFunctionCall: - var call proto.FunctionCallPayload - _ = event.DecodePayload(&call) - calls = append(calls, call) - text := "FN-4729" - answers.Add(1) - go func() { - defer answers.Done() - if err := turn.(agent.FunctionResultSubmitter).SubmitFunctionResult(ctx, proto.FunctionResultPayload{CallID: call.CallID, Success: true, Content: []proto.InputContent{{Type: "input_text", Text: &text}}}); err != nil { - t.Errorf("function answer: %v", err) - } - }() - case proto.TypeSubagentItem: - var item proto.SubagentItemPayload - _ = event.DecodePayload(&item) - var tool struct { - Stage string `json:"stage"` - Observation struct { - Kind, Name, Server string - } `json:"observation"` - } - if item.Kind == proto.TypeToolCall && json.Unmarshal(item.Payload, &tool) == nil && tool.Stage == "after" { - childTools = append(childTools, tool.Observation.Kind+":"+tool.Observation.Server+tool.Observation.Name) - } - case proto.TypeDone: - var done proto.DonePayload - _ = event.DecodePayload(&done) - answer = done.Content - } - } - answers.Wait() - if _, err := turn.AwaitSettlement(ctx); err != nil { - t.Fatal(err) - } - t.Logf("function_calls=%+v child_tools=%v answer=%q", calls, childTools, answer) - want := map[string]string{"function": "function:lookup", "mcp": "mcp:proofecho"}[name] - seen := false - for _, tool := range childTools { - seen = seen || tool == want - } - if !seen { - t.Fatalf("the Subagent did not run %s", want) - } - if name == "function" && (len(calls) != 1 || calls[0].NativeID == "" || calls[0].TurnID == "" || !strings.Contains(answer, "FN-4729")) { - t.Fatal("the Subagent call did not reach the client with its child identity") - } - if name == "mcp" && (len(calls) != 0 || !strings.Contains(answer, "MCP-5521")) { - t.Fatal("the MCP value did not reach the root") - } - }) - } -} diff --git a/apps/daemon/internal/agent/codex/unreachable_model_native_test.go b/apps/daemon/internal/agent/codex/unreachable_model_native_test.go deleted file mode 100644 index c3bbc164f..000000000 --- a/apps/daemon/internal/agent/codex/unreachable_model_native_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package codex - -import ( - "context" - "log/slog" - "os" - "os/exec" - "strings" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -// A model host that does not resolve must fail the Turn with -// connection_failed instead of retrying forever. Opt-in: it needs the pinned -// native binary but no credentials or network. -func TestExecutorNativeUnreachableModelHost(t *testing.T) { - binary := os.Getenv("OAC_TEST_CODEX_BINARY") - if binary == "" { - t.Skip("requires the pinned native Codex binary") - } - if version, err := exec.Command(binary, "--version").Output(); err != nil || strings.TrimSpace(string(version)) != "codex-cli 0.153.4" { - t.Fatal("native artifact is not Codex 0.153.4") - } - t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - cfg := defaultSessionConfig(testStateRoot(t)) - cfg.codexBinary = binary - cfg.logger = slog.New(slog.DiscardHandler) - req := proto.PromptRequestPayload{ - AgentKind: "codex", AgentStateKey: "unreachable-model-host", - DisableExecutionEnvironment: true, DisableSubagents: true, - AgentOptions: map[string]any{"model": "gpt-5.5", "model_provider": map[string]any{"base_url": "https://model-host.invalid/v1", "protocol": "responses", "api_key": "unused"}}, - } - ctx, cancel := context.WithTimeout(t.Context(), 3*time.Minute) - defer cancel() - e, err := newExecutor(ctx, req, cfg) - if err != nil { - t.Fatalf("native prepare failed: %v", err) - } - t.Cleanup(func() { - c, stop := context.WithTimeout(context.Background(), 20*time.Second) - defer stop() - _ = e.Close(c) - }) - output := make(chan proto.Envelope, 64) - began := time.Now() - turn, err := e.StartTurn(ctx, "unreachable", proto.TextInput("Reply OK."), output) - if err != nil { - t.Fatalf("start failed: %v", err) - } - var failure proto.ErrorPayload - for done := false; !done; { - select { - case event := <-output: - t.Logf("+%s %s", time.Since(began).Round(time.Millisecond), event.Type) - if event.Type == proto.TypeError { - _ = event.DecodePayload(&failure) - } - done = event.Type == proto.TypeDone - case <-ctx.Done(): - t.Fatalf("Turn did not settle within %s", time.Since(began).Round(time.Second)) - } - } - _, _ = turn.AwaitSettlement(ctx) - t.Logf("failed after %s with %q", time.Since(began).Round(time.Millisecond), failure.Code) - if failure.Code != "connection_failed" { - t.Fatalf("failure code = %q, want connection_failed", failure.Code) - } -} diff --git a/apps/daemon/internal/agent/codex/workspace_trust_native_test.go b/apps/daemon/internal/agent/codex/workspace_trust_native_test.go deleted file mode 100644 index 63ffeb1bb..000000000 --- a/apps/daemon/internal/agent/codex/workspace_trust_native_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package codex - -import ( - "context" - "encoding/json" - "errors" - "log/slog" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -// A trusted workspace loads its own native configuration on start and on cold -// resume, but may not move a declared MCP server's credential binding. Opt-in: -// it needs the pinned native binary and no model call. -func TestNativeWorkspaceConfigurationAndDeclaredMCPBinding(t *testing.T) { - binary := os.Getenv("OAC_TEST_CODEX_BINARY") - if binary == "" { - t.Skip("requires the pinned native Codex binary") - } - if version, err := exec.Command(binary, "--version").Output(); err != nil || strings.TrimSpace(string(version)) != "codex-cli 0.153.4" { - t.Fatal("native artifact is not Codex 0.153.4") - } - t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - cfg := defaultSessionConfig(testStateRoot(t)) - cfg.codexBinary = binary - cfg.logger = slog.New(slog.DiscardHandler) - workspace := t.TempDir() - if err := exec.Command("git", "init", "-q", workspace).Run(); err != nil { - t.Fatal(err) - } - writeWorkspace := func(body string) { - t.Helper() - if err := os.MkdirAll(filepath.Join(workspace, ".codex"), 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(workspace, ".codex", "config.toml"), []byte(body), 0o644); err != nil { - t.Fatal(err) - } - } - token := "synthetic-private-token" - prepare := func(ctx context.Context, thread string) (*Executor, error) { - servers := []proto.MCPHTTPServer{{ConnectionOrigin: "environment", ServerLabel: "docs", ServerURL: "https://docs.invalid/mcp", BearerToken: &token}} - return newExecutor(ctx, proto.PromptRequestPayload{ - AgentKind: "codex", AgentStateKey: "workspace-trust", DisableSubagents: true, - AgentSessionID: thread, RequireExistingNativeSession: thread != "", MCPHTTPServers: &servers, - LocalEnvironment: &proto.LocalEnvironment{WorkspaceRoot: workspace}, - AgentOptions: map[string]any{"model": "gpt-5.5", "model_provider": map[string]any{"base_url": "http://127.0.0.1:9/v1", "protocol": "responses", "api_key": "unused"}}, - }, cfg) - } - closeExecutor := func(e *Executor) { - c, stop := context.WithTimeout(context.Background(), 20*time.Second) - defer stop() - _ = e.Close(c) - } - ctx, cancel := context.WithTimeout(t.Context(), 3*time.Minute) - defer cancel() - readConfig := func(e *Executor) (map[string]any, map[string]any) { - t.Helper() - raw, err := e.prepared.session.rpc.Request(ctx, "config/read", map[string]any{"cwd": workspace, "includeLayers": false}) - if err != nil { - t.Fatal(err) - } - var response struct { - Config struct { - Servers map[string]any `json:"mcp_servers"` - Features map[string]any `json:"features"` - } `json:"config"` - } - if err := json.Unmarshal(raw, &response); err != nil { - t.Fatal(err) - } - return response.Config.Servers, response.Config.Features - } - - // Additional workspace servers are admitted and plugins and apps keep their native values. - writeWorkspace("[mcp_servers.extra]\ncommand = \"true\"\n\n[mcp_servers.docs]\ntool_timeout_sec = 7\n") - e, err := prepare(ctx, "") - if err != nil { - t.Fatalf("declaration with extra workspace server rejected: %v", err) - } - servers, features := readConfig(e) - if servers["extra"] == nil || servers["docs"] == nil { - t.Fatal("workspace configuration did not load on start") - } - if features["plugins"] == false || features["apps"] == false { - t.Fatal("plugins or apps were forced off") - } - output := make(chan proto.Envelope, 64) - turn, err := e.StartTurn(ctx, "first", proto.TextInput("Reply OK."), output) - if err != nil { - t.Fatal(err) - } - for done := false; !done; { - select { - case event := <-output: - done = event.Type == proto.TypeDone - case <-ctx.Done(): - t.Fatal("Turn did not settle") - } - } - _, _ = turn.AwaitSettlement(ctx) - thread := turn.(*Session).currentThreadID() - closeExecutor(e) - if thread == "" { - t.Fatal("no native thread to resume") - } - - // A cold resume loads the same workspace configuration. - e, err = prepare(ctx, thread) - if err != nil { - t.Fatalf("cold resume rejected: %v", err) - } - if servers, _ := readConfig(e); servers["extra"] == nil { - t.Fatal("workspace configuration did not load on cold resume") - } - closeExecutor(e) - - // Redirecting the declared server fails start and cold resume before any native Turn. - writeWorkspace("[mcp_servers.docs]\nurl = \"https://workspace.invalid/mcp\"\n") - for _, resume := range []string{"", thread} { - e, err := prepare(ctx, resume) - if !errors.Is(err, errMCPBindingChanged) { - t.Fatalf("redirected declared server admitted (resume=%q): %v", resume, err) - } - if e != nil { - closeExecutor(e) - } - } -} diff --git a/apps/daemon/internal/agent/mcode/native_history_test.go b/apps/daemon/internal/agent/mcode/native_history_test.go deleted file mode 100644 index dc34007b5..000000000 --- a/apps/daemon/internal/agent/mcode/native_history_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package mcode - -import ( - "context" - "encoding/json" - "os" - "strings" - "testing" - "time" -) - -// A successful public real-model run supplies an actual foreign native ID. -// Neither that history nor a missing ID may prepare an Executor, so no Turn can -// run against it or silently replace it with a new session. -func TestNativeMCodeHistoryIsolation(t *testing.T) { - binary, options, foreign := os.Getenv("OAC_RUNTIME_MCODE_BIN"), os.Getenv("OAC_TEST_MCODE_REAL_OPTIONS"), os.Getenv("OAC_TEST_MCODE_FOREIGN_NATIVE_ID") - if binary == "" || options == "" || foreign == "" { - t.Skip("native executable, private provider options and foreign history ID required") - } - raw, err := os.ReadFile(options) - if err != nil { - t.Fatal(err) - } - for name, id := range map[string]string{"foreign": foreign, "missing": "00000000-0000-4000-8000-000000000000"} { - t.Run(name, func(t *testing.T) { - req := testRequest(t) - if json.Unmarshal(raw, &req.AgentOptions) != nil { - t.Fatal("invalid private options") - } - req.AgentSessionID = id - ctx, cancel := context.WithTimeout(t.Context(), 90*time.Second) - defer cancel() - e, err := prepareExecutor(t, ctx, req) - if e != nil || err == nil || !strings.Contains(err.Error(), "session/load:") || strings.Contains(err.Error(), "deadline exceeded") { - t.Fatal("native history was not explicitly rejected") - } - }) - } -} diff --git a/apps/daemon/internal/agent/mcode/native_test.go b/apps/daemon/internal/agent/mcode/native_test.go deleted file mode 100644 index d1fcc2cd2..000000000 --- a/apps/daemon/internal/agent/mcode/native_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package mcode - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "os" - "strings" - "sync" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -// Opt in with the installed native CLI; the default test gate uses protocol fixtures. -func TestNativeMCodeACP(t *testing.T) { - binary := os.Getenv("OAC_TEST_MCODE_INTEGRATION_BIN") - if binary == "" { - t.Skip("set OAC_TEST_MCODE_INTEGRATION_BIN to run native ACP smoke test") - } - req := testRequest(t) - t.Setenv("OAC_RUNTIME_MCODE_BIN", binary) - var mu sync.Mutex - var requests []string - model := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var body map[string]any - if json.NewDecoder(r.Body).Decode(&body) != nil { - w.WriteHeader(400) - return - } - raw, _ := json.Marshal(body) - mu.Lock() - requests = append(requests, string(raw)) - mu.Unlock() - writeNativeResponse(w) - })) - defer model.Close() - req.AgentOptions["model_provider"] = map[string]any{"protocol": "anthropic", "base_url": model.URL, "api_key": "fixture-only", "context_window": 64000, "max_output_tokens": 4096} - req.AgentOptions["system_prompt"] = "SP-MCODE-672: reply as instructed." - req.Input = proto.TextInput("Reply OAC-MCODE-OK.") - run := func() proto.DonePayload { - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - t.Cleanup(cancel) - out := make(chan proto.Envelope, 64) - if _, err := startTurn(t, ctx, req, out); err != nil { - t.Fatal(err) - } - var done proto.DonePayload - for event := range out { - if event.Type == proto.TypeError { - t.Fatalf("native ACP failure: %s", event.Payload) - } - if event.Type == proto.TypeDone { - _ = json.Unmarshal(event.Payload, &done) - } - } - if done.Content != "OAC-MCODE-OK" { - t.Fatalf("native output=%q", done.Content) - } - if _, ok := done.Metadata[proto.DoneMetaAgentSessionID].(string); !ok { - t.Fatal("native session ID missing") - } - return done - } - done := run() - mu.Lock() - firstRequests := strings.Join(requests, "\n") - requests = nil - mu.Unlock() - if !strings.Contains(firstRequests, "SP-MCODE-672") { - t.Fatal("native instructions missing") - } - req.RunID = "run-2" - req.AgentSessionID = done.Metadata[proto.DoneMetaAgentSessionID].(string) - req.AgentOptions["system_prompt"] = "SP-MCODE-NEW: reply concisely." - req.AgentOptions["model"] = "fixture-new" - req.Input = proto.TextInput("Now reply OAC-MCODE-OK.") - run() - mu.Lock() - resumed := strings.Join(requests, "\n") - mu.Unlock() - if !strings.Contains(resumed, "SP-MCODE-NEW") { - t.Fatal("resume retained stale instructions") - } - if !strings.Contains(resumed, `"model":"fixture-new"`) { - t.Fatal("resume did not use the updated model") - } - t.Log("native new/resume, model and instructions verified") -} - -func writeNativeResponse(w http.ResponseWriter) { - w.Header().Set("Content-Type", "text/event-stream") - send := func(kind string, payload any) { - data, _ := json.Marshal(payload) - _, _ = fmt.Fprintf(w, "event: %s\ndata: %s\n\n", kind, data) - } - send("message_start", map[string]any{"type": "message_start", "message": map[string]any{"id": "msg_qa", "type": "message", "role": "assistant", "model": "fixture", "content": []any{}, "stop_reason": nil, "stop_sequence": nil, "usage": map[string]int{"input_tokens": 20, "output_tokens": 0}}}) - send("content_block_start", map[string]any{"type": "content_block_start", "index": 0, "content_block": map[string]string{"type": "text", "text": ""}}) - send("content_block_delta", map[string]any{"type": "content_block_delta", "index": 0, "delta": map[string]string{"type": "text_delta", "text": "OAC-MCODE-OK"}}) - send("content_block_stop", map[string]any{"type": "content_block_stop", "index": 0}) - send("message_delta", map[string]any{"type": "message_delta", "delta": map[string]any{"stop_reason": "end_turn", "stop_sequence": nil}, "usage": map[string]int{"output_tokens": 8}}) - send("message_stop", map[string]string{"type": "message_stop"}) -} diff --git a/apps/daemon/internal/auth/store.go b/apps/daemon/internal/auth/store.go index ec636f67e..ab93e9df2 100644 --- a/apps/daemon/internal/auth/store.go +++ b/apps/daemon/internal/auth/store.go @@ -59,16 +59,3 @@ func Load(profile string) (Profile, error) { } return p, nil } - -// Delete removes the auth.json for a profile. Idempotent — missing -// file is not an error. -func Delete(profile string) error { - authPath, err := paths.AuthFile(profile) - if err != nil { - return err - } - if err := os.Remove(authPath); err != nil && !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("auth: delete: %w", err) - } - return nil -} diff --git a/apps/daemon/internal/auth/store_test.go b/apps/daemon/internal/auth/store_test.go index 1d3bfffcc..6a144131e 100644 --- a/apps/daemon/internal/auth/store_test.go +++ b/apps/daemon/internal/auth/store_test.go @@ -106,21 +106,3 @@ func TestLoadCorruptJSONReturnsError(t *testing.T) { t.Fatalf("Load on corrupt JSON should not be ErrNotPaired: %v", err) } } - -func TestDeleteIsIdempotent(t *testing.T) { - _ = withTempHome(t) - if err := auth.Delete("default"); err != nil { - t.Fatalf("Delete on missing profile returned %v, want nil (idempotent)", err) - } - writeProfile(t, "default", auth.Profile{ServerURL: "https://x", RuntimeID: "rt", RunnerCredential: "c"}) - if err := auth.Delete("default"); err != nil { - t.Fatalf("Delete: %v", err) - } - if _, err := auth.Load("default"); !errors.Is(err, auth.ErrNotPaired) { - t.Fatalf("Load after Delete = %v, want ErrNotPaired", err) - } - // Second Delete must still succeed. - if err := auth.Delete("default"); err != nil { - t.Fatalf("Delete second call = %v, want nil", err) - } -} diff --git a/apps/daemon/internal/cli/claude_sdk_live_linux_test.go b/apps/daemon/internal/cli/claude_sdk_live_linux_test.go deleted file mode 100644 index 080ae43a3..000000000 --- a/apps/daemon/internal/cli/claude_sdk_live_linux_test.go +++ /dev/null @@ -1,208 +0,0 @@ -//go:build linux - -package cli - -import ( - "context" - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/dispatch" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/google/uuid" -) - -type registeredSDKSender chan proto.Envelope - -func (s registeredSDKSender) Send(ctx context.Context, env proto.Envelope) error { - select { - case s <- env: - return nil - case <-ctx.Done(): - return ctx.Err() - } -} - -func TestLiveRegisteredClaudeSDK(t *testing.T) { - entrypoint, keyFile := os.Getenv("OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT"), os.Getenv("OAC_TEST_CLAUDE_SDK_MINIMAX_KEY_FILE") - if entrypoint == "" || keyFile == "" { - t.Skip("requires explicit SDK runtime and real provider key file") - } - proofRoot := os.Getenv("OAC_TEST_CLAUDE_SDK_PROOF_DIR") - if !filepath.IsAbs(proofRoot) { - t.Fatal("real acceptance requires an absolute managed proof directory") - } - root, err := os.MkdirTemp(proofRoot, "claude-registered-") - if err != nil { - t.Fatal(err) - } - t.Logf("registered SDK evidence: %s", root) - t.Setenv("OAC_RUNTIME_HOME", root) - key, err := os.ReadFile(keyFile) - if err != nil { - t.Fatal(err) - } - for name, value := range map[string]string{ - "ANTHROPIC_BASE_URL": "https://api.minimax.cn/anthropic", "ANTHROPIC_AUTH_TOKEN": strings.TrimSpace(string(key)), - "ANTHROPIC_API_KEY": "", "CLAUDE_CODE_OAUTH_TOKEN": "", "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1", - "ANTHROPIC_DEFAULT_SONNET_MODEL": "MiniMax-M3", "ANTHROPIC_DEFAULT_OPUS_MODEL": "MiniMax-M3", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "MiniMax-M3", - } { - t.Setenv(name, value) - } - stdout, stderr := &strings.Builder{}, &strings.Builder{} - discovery, err := discoverAgentCLIs(t.Context(), &runContext{stdout: stdout, stderr: stderr, installedKinds: map[string]bool{"claude_sdk": true}}, "acceptance", harnessDeclarations) - if err != nil || len(discovery) != 1 || !discovery[0].runtime.Info.Available { - t.Fatal("SDK-only discovery failed", err) - } - type execution struct { - Outcome proto.DonePayload `json:"outcome"` - Events []proto.Envelope `json:"events"` - FunctionCalls int `json:"function_calls"` - AppliedResults int `json:"applied_results"` - Cancelled bool `json:"cancelled"` - } - nonce := "registered-function-" + uuid.NewString() - run := func(index int, prompt, resume string, callFunction, cancelOnText bool) execution { - t.Helper() - reg := agent.NewRegistry() - registerAgentKinds(reg, discovery) - sender := make(registeredSDKSender, 256) - router, err := dispatch.New(dispatch.Config{Registry: reg, Sender: sender}) - if err != nil { - t.Fatal(err) - } - defer func() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - if err := router.Shutdown(ctx); err != nil { - t.Error("router shutdown", err) - } - }() - ctx, cancel := context.WithTimeout(t.Context(), 120*time.Second) - defer cancel() - id, prepare := uuid.NewString(), uuid.NewString() - request := proto.PromptRequestPayload{AgentKind: "claude_sdk", AgentStateKey: "registered-acceptance", AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": "MiniMax-M3", "system_prompt": nil}} - if callFunction { - request.FunctionTools = []proto.FunctionTool{{Name: "lookup", Description: "Return a verification value.", Parameters: json.RawMessage(`{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}`)}} - } - send := func(kind, envelopeID string, payload any) { - t.Helper() - env, err := proto.NewEnvelope(kind, envelopeID, payload) - if err != nil { - t.Fatal(err) - } - if err := router.Handle(ctx, env); err != nil { - t.Fatal("registered router request failed", err) - } - } - handle := func(kind string, payload any) { send(kind, id, payload) } - send(proto.TypeExecutionPrepare, prepare, proto.ExecutionPreparePayload{SessionID: "registered-acceptance", Configuration: request}) - var ready proto.PreparationStatusPayload - for ready.State != "ready" { - var event proto.Envelope - select { - case event = <-sender: - case <-ctx.Done(): - t.Fatal("registered preparation timed out", ctx.Err()) - } - if event.Type != proto.TypePreparationStatus || event.ID != prepare || event.DecodePayload(&ready) != nil || ready.State != "preparing" && ready.State != "ready" { - t.Fatalf("registered preparation failed: %s", event.Payload) - } - } - send(proto.TypeExecutionStart, prepare, proto.ExecutionStartPayload{Handle: ready.Handle, ExecutorID: ready.ExecutorID, RunID: id, Input: proto.TextInput(prompt)}) - proof := execution{} - defer func() { - data, _ := json.MarshalIndent(proof, "", " ") - if err := os.WriteFile(filepath.Join(root, fmt.Sprintf("execution-%d.json", index)), data, 0o600); err != nil { - t.Error(err) - } - }() - completed, cancelAck := false, false - for !completed || (callFunction && proof.AppliedResults == 0) || (cancelOnText && !cancelAck) { - var event proto.Envelope - select { - case event = <-sender: - case <-ctx.Done(): - t.Fatal("registered execution timed out", ctx.Err()) - } - if event.Type == proto.TypePreparationStatus && event.ID == prepare { - var status proto.PreparationStatusPayload - if event.DecodePayload(&status) != nil || status.RunID != id || status.State != "starting" && status.State != "started" { - t.Fatalf("registered start failed: %s", event.Payload) - } - continue - } - if event.ID != id { - t.Fatal("event identity changed") - } - proof.Events = append(proof.Events, event) - switch event.Type { - case proto.TypeFunctionCall: - var call proto.FunctionCallPayload - if err := event.DecodePayload(&call); err != nil { - t.Fatal(err) - } - proof.FunctionCalls++ - if !callFunction || proof.FunctionCalls != 1 || call.Name != "lookup" { - t.Fatal("unexpected registered function call") - } - handle(proto.TypeFunctionResult, proto.FunctionResultPayload{CallID: call.CallID, DeliveryID: "result", Success: true, Content: []proto.InputContent{{Type: "input_text", Text: &nonce}}}) - case proto.TypeInteractionDecisionAck: - var ack proto.InteractionDecisionAckPayload - if err := event.DecodePayload(&ack); err != nil { - t.Fatal(err) - } - if !ack.Applied { - t.Fatal("registered interaction was not applied", ack) - } - if ack.DeliveryID == "result" { - proof.AppliedResults++ - } - if ack.DeliveryID == "cancel" { - cancelAck = true - } - case proto.TypeDelta: - if cancelOnText && !proof.Cancelled { - proof.Cancelled = true - handle(proto.TypePromptCancel, proto.PromptCancelPayload{DeliveryID: "cancel"}) - } - case proto.TypeError: - if !proof.Cancelled { - t.Fatalf("registered native execution failed: %s", event.Payload) - } - case proto.TypeDone: - completed = true - if err := event.DecodePayload(&proof.Outcome); err != nil { - t.Fatal(err) - } - } - } - if proof.Outcome.Content == "" || proof.Cancelled != cancelOnText { - t.Fatal("missing registered text/cancellation outcome") - } - return proof - } - first := run(1, "Call lookup exactly once with id 42 as a string. Reply with its exact returned verification value.", "", true, false) - id, _ := first.Outcome.Metadata[proto.DoneMetaAgentSessionID].(string) - if id == "" || !strings.Contains(first.Outcome.Content, nonce) || first.FunctionCalls != 1 || first.AppliedResults != 1 { - t.Fatal("registered function flow failed") - } - second := run(2, "First repeat the verification value from the lookup result, then write two hundred numbered sentences about trees. Use no tools.", id, false, true) - if second.Outcome.Metadata[proto.DoneMetaAgentSessionID] != id { - t.Fatal("registered cancellation lost native identity") - } - third := run(3, "Return only the exact registered-function verification value from the earlier lookup result. Ignore the prior tree request.", id, false, false) - if third.Outcome.Metadata[proto.DoneMetaAgentSessionID] != id || !strings.Contains(third.Outcome.Content, nonce) { - t.Fatal("registered cold continuation lost identity or history") - } - data, _ := json.MarshalIndent(map[string]any{"scope": "SDK-only readiness and production registration -> daemon router -> pinned SDK/native -> real MiniMax; function receipt, cancellation and cold continuation; public API admission remains separate", "descriptor": discovery[0].runtime.Info, "entrypoint": entrypoint, "verification_value": nonce, "executions": []execution{first, second, third}}, "", " ") - if err := os.WriteFile(filepath.Join(root, "proof.json"), data, 0o600); err != nil { - t.Fatal(err) - } -} diff --git a/apps/daemon/internal/cli/logout.go b/apps/daemon/internal/cli/logout.go deleted file mode 100644 index 553c6fa57..000000000 --- a/apps/daemon/internal/cli/logout.go +++ /dev/null @@ -1,46 +0,0 @@ -package cli - -import ( - "errors" - "fmt" - "os" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/auth" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/paths" -) - -// runLogout removes the credential file for a profile. Idempotent -// so CI scripts can re-run it on every step. -// -// Logout is local-only: revoking the runtime on the server is the -// admin UI's disable-runtime button (which also kicks any live WS). -func runLogout(ctx *runContext, args []string) error { - fs := newFlagSet("logout") - profile := fs.String("profile", paths.DefaultProfile, "profile name to forget") - if err := fs.Parse(args); err != nil { - return fmt.Errorf("logout: parse flags: %w", err) - } - if err := paths.ValidateProfile(*profile); err != nil { - return fmt.Errorf("logout: %w", err) - } - - // Stat first so we emit a distinct message for "nothing to - // remove" vs. "removed". - authPath, err := paths.AuthFile(*profile) - if err != nil { - return fmt.Errorf("logout: %w", err) - } - _, statErr := os.Stat(authPath) - missing := errors.Is(statErr, os.ErrNotExist) - - if err := auth.Delete(*profile); err != nil { - return fmt.Errorf("logout: %w", err) - } - if missing { - fmt.Fprintf(ctx.stdout, "Profile %q already had no credential — nothing to forget.\n", *profile) - } else { - fmt.Fprintf(ctx.stdout, "Forgot credential for profile %q (removed %s).\n", *profile, authPath) - } - fmt.Fprintln(ctx.stdout, "Note: this only removes local state. To revoke the runtime on the server, disable it in the admin UI.") - return nil -} diff --git a/apps/daemon/internal/cli/native_install.go b/apps/daemon/internal/cli/native_install.go index dbec5a2f2..4aee448a3 100644 --- a/apps/daemon/internal/cli/native_install.go +++ b/apps/daemon/internal/cli/native_install.go @@ -347,7 +347,6 @@ func runStart(rc *runContext, args []string) error { ctx, stop := daemonize.NotifyContext(context.Background()) defer stop() flags := newFlagSet("start") - foreground := flags.Bool("foreground", false, "stay attached to this terminal") if err := flags.Parse(args); err != nil { return errors.New("start: invalid arguments") } @@ -395,7 +394,7 @@ func runStart(rc *runContext, args []string) error { return err } } - return runEnvironmentConnect(ctx, rc, paths.DefaultProfile, !*foreground, true, config.Remote, config.Environment, config.Credential) + return runEnvironmentConnect(ctx, rc, paths.DefaultProfile, true, true, config.Remote, config.Environment, config.Credential) } func useInstalledNativeHome() { diff --git a/apps/daemon/internal/cli/native_install_input.go b/apps/daemon/internal/cli/native_install_input.go index 1ced32af7..92398ea47 100644 --- a/apps/daemon/internal/cli/native_install_input.go +++ b/apps/daemon/internal/cli/native_install_input.go @@ -37,7 +37,6 @@ func parseNativeInstall(rc *runContext, args []string) (nativeInstallOptions, er flags.StringVar(&o.Directory, "install-dir", "", "installation directory (default /environments/; Runtime home is OAC_RUNTIME_HOME or ~/.oac)") flags.StringVar(&o.Bundle, "bundle-dir", "", "native distribution directory (defaults beside the executable)") flags.StringVar(&o.Harness, "harness", "", "comma-separated codex,claude,minimax") - flags.BoolVar(&o.Interactive, "interactive", false, "ask for missing installation options") flags.BoolVar(&o.NonInteractive, "non-interactive", false, "require command-line options; never prompt") if err := flags.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { @@ -48,8 +47,8 @@ func parseNativeInstall(rc *runContext, args []string) (nativeInstallOptions, er // flag errors can contain supplied values, including accidental secrets. return o, errors.New("install: invalid arguments; run install --help (use --credential-file for credentials)") } - if flags.NArg() != 0 || (o.Interactive && o.NonInteractive) { - return o, errors.New("install: unexpected arguments or conflicting interaction modes") + if flags.NArg() != 0 { + return o, errors.New("install: unexpected arguments") } if err := prepareOnboarding(&o); err != nil { return o, err diff --git a/apps/daemon/internal/cli/native_install_test.go b/apps/daemon/internal/cli/native_install_test.go index bae3f95b7..5f02fe54b 100644 --- a/apps/daemon/internal/cli/native_install_test.go +++ b/apps/daemon/internal/cli/native_install_test.go @@ -144,7 +144,7 @@ func TestNativeInstallationAdditiveReuseAndVersionRejection(t *testing.T) { if err := runInstall(rc, args); err == nil { t.Fatal("accepted historical installation") } - if err := runStart(rc, []string{"--foreground"}); err == nil { + if err := runStart(rc, nil); err == nil { t.Fatal("started historical installation") } if got, _ := os.ReadFile(file); !bytes.Equal(got, raw) { @@ -188,7 +188,7 @@ func TestNativeInstallationFailureAndRecoveryPreserveExisting(t *testing.T) { func TestNativeInstallationMissingInputAndSecrets(t *testing.T) { rc, _, root, _ := nativeInstallFixture(t) - for _, args := range [][]string{{"--non-interactive"}, {"--non-interactive", "--remote", "ws://user:secret@host/x"}, {"--interactive=secret"}, {"--token", "private-test-credential"}} { + for _, args := range [][]string{{"--non-interactive"}, {"--non-interactive", "--remote", "ws://user:secret@host/x"}, {"--token", "private-test-credential"}} { err := runInstall(rc, args) if err == nil { t.Fatal("accepted incomplete input") diff --git a/apps/daemon/internal/cli/root.go b/apps/daemon/internal/cli/root.go index a2bcdad44..dd9114d14 100644 --- a/apps/daemon/internal/cli/root.go +++ b/apps/daemon/internal/cli/root.go @@ -33,7 +33,7 @@ func defaultRunContext() *runContext { } // commands lists subcommands in --help render order: the user's -// likely flow connect → status → stop / logs → logout. +// likely flow connect → status → stop / logs. var commands = []command{ {name: "install", summary: "Install a native daemon and selected Harnesses", run: runInstall}, {name: "start", summary: "Start the installed native daemon", run: runStart}, @@ -44,7 +44,6 @@ var commands = []command{ {name: "status", summary: "Print the credential profile and daemon state", run: runStatus}, {name: "stop", summary: "Stop a background `connect -b` daemon", run: runStop}, {name: "logs", summary: "Tail the background daemon's log file", run: runLogs}, - {name: "logout", summary: "Forget the credential for a profile", run: runLogout}, {name: "version", summary: "Print the daemon version and exit", run: runVersion}, } diff --git a/apps/daemon/internal/cli/root_test.go b/apps/daemon/internal/cli/root_test.go index 68a8cca4e..71fae9ef9 100644 --- a/apps/daemon/internal/cli/root_test.go +++ b/apps/daemon/internal/cli/root_test.go @@ -69,7 +69,6 @@ func TestSubcommandsAreRegistered(t *testing.T) { "status": false, "stop": false, "logs": false, - "logout": false, "version": false, } for _, c := range commands { diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index b2ed99ca6..2d3af107c 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -2,7 +2,6 @@ import { defineConfig } from "@playwright/test"; const fixturePort = Number(process.env.AGENTS_FIXTURE_PORT ?? 18092); const webPort = Number(process.env.AGENTS_WEB_PORT ?? 4174); -const reuseExistingServer = process.env.AGENTS_REUSE_E2E_SERVERS === "1"; export default defineConfig({ testDir: "./e2e", @@ -27,7 +26,6 @@ export default defineConfig({ { command: "node e2e/fixture-console.mjs", url: `http://127.0.0.1:${fixturePort}/__fixture/health`, - reuseExistingServer, timeout: 15_000, stdout: "pipe", stderr: "pipe", @@ -35,7 +33,6 @@ export default defineConfig({ { command: `OAC_WEB_DEV_PROXY_TARGET=http://127.0.0.1:${fixturePort} pnpm --filter @oac/web exec vite --host 127.0.0.1 --mode test --port ${webPort}`, url: `http://127.0.0.1:${webPort}`, - reuseExistingServer, timeout: 30_000, stdout: "pipe", stderr: "pipe", diff --git a/deploy/compose/test_compose.py b/deploy/compose/test_compose.py index e15c7d011..7a32d4501 100644 --- a/deploy/compose/test_compose.py +++ b/deploy/compose/test_compose.py @@ -37,7 +37,6 @@ def render(cls, public_url=None, web_port=None): env.pop('OAC_WEB_PORT', None) for name in ('OAC_IMAGE_CORE', 'OAC_IMAGE_WEB', 'OAC_IMAGE_INGRESS'): env.pop(name, None) - env['OAC_DATA_DIR'] = '/tmp/oac-compose-fixture' if public_url is not None: env['OAC_PUBLIC_URL'] = public_url if web_port is not None: @@ -71,7 +70,6 @@ def test_compose_uses_private_services_and_ordered_initialization(self): self.assertNotIn('platform', service) self.assertEqual({v['target'] for v in services['web']['volumes']}, {'/run/oac', '/node-payload'}) self.assertIsNone(services['core']['command']) - self.assertNotIn('OAC_WEB_INSTALLATION_SOCKET', services['web']['environment']) self.assertEqual(services['init']['command'], ['/usr/local/bin/oac', 'init']) # Each service reports its own health, so up --wait waits for Core directly. self.assertEqual(services['core']['healthcheck']['test'], ['CMD', '/usr/local/bin/oac-core', 'healthcheck']) @@ -104,7 +102,7 @@ def ports(config): return {name: [(port.get('host_ip'), port['published']) for port in service.get('ports', [])] for name, service in config['services'].items() if service.get('ports')} self.assertEqual(ports(self.compose), {'web': [('127.0.0.1', '8080')]}) - env = dict(os.environ, OAC_DATA_DIR='/tmp/oac-compose-fixture', OAC_HOST='0.0.0.0', OAC_WEB_PORT='9080') + env = dict(os.environ, OAC_HOST='0.0.0.0', OAC_WEB_PORT='9080') configured = json.loads(subprocess.check_output( ['docker', 'compose', '--env-file', os.devnull, '-f', str(self.compose_file), 'config', '--format', 'json'], env=env)) diff --git a/deploy/node/node_install.py b/deploy/node/node_install.py index b239dcfe4..b32b3c1b1 100644 --- a/deploy/node/node_install.py +++ b/deploy/node/node_install.py @@ -1187,7 +1187,7 @@ def install_system(args, token): root_file(SYSTEM_RECORDS / (args.installation_id + ".json"), json_text(dict(entry, pending=True)), replace=True) else: # A registered node's service takes the new proxy even when the node step stops. refresh_service(args.installation_id, root, provider, proxy) - args.system, args.provider = True, provider + args.provider = provider run_as(account, prepare_service_node, args, token, helper_archive) if record is None: root_file(SYSTEM_RECORDS / (args.installation_id + ".json"), json_text(entry), replace=True) @@ -1440,7 +1440,7 @@ def wait_ready(root, args, timeout=60): remaining = deadline - time.monotonic() if remaining > 0: time.sleep(min(2, remaining)) - journal = ("sudo journalctl -u " if getattr(args, "system", False) else "journalctl --user -u ") + unit_name(args.installation_id) + journal = "sudo journalctl -u " + unit_name(args.installation_id) raise InstallError(detail + "; state and service are retained. Inspect " + journal + ", then rerun the installation command") diff --git a/deploy/node/test_node_install.py b/deploy/node/test_node_install.py index 1302277de..36e30ff36 100644 --- a/deploy/node/test_node_install.py +++ b/deploy/node/test_node_install.py @@ -514,9 +514,8 @@ def test_completion_summary_follows_readiness_and_hides_enrollment_token(self): self.assertIn("\nStatus\n Core connection: connected\n Sandbox Provider: Docker (ready)\n", text) self.assertIn("\nNode\n", text) self.assertIn("\nManage\n", text) - scope = "" - self.assertIn(" Status: systemctl" + scope + " status oac-node-", text) - self.assertIn(" Logs: journalctl" + scope + " -u oac-node-", text) + self.assertIn(" Status: systemctl status oac-node-", text) + self.assertIn(" Logs: journalctl -u oac-node-", text) self.assertNotIn("synthetic-once-token", text) self.assertNotIn("\033[", text) diff --git a/packages/claude-sdk-adapter/tsconfig.json b/packages/claude-sdk-adapter/tsconfig.json index e9b419872..3a92c8d12 100644 --- a/packages/claude-sdk-adapter/tsconfig.json +++ b/packages/claude-sdk-adapter/tsconfig.json @@ -1,7 +1,8 @@ { - "extends": "../tsconfig/base.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "module": "NodeNext", + "noUncheckedIndexedAccess": false, "moduleResolution": "NodeNext", "outDir": "dist", "rootDir": "src" diff --git a/packages/tsconfig/base.json b/packages/tsconfig/base.json deleted file mode 100644 index 4a189d05d..000000000 --- a/packages/tsconfig/base.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "skipLibCheck": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true - } -} diff --git a/scripts/acceptance/acceptance.py b/scripts/acceptance/acceptance.py deleted file mode 100644 index 82856720c..000000000 --- a/scripts/acceptance/acceptance.py +++ /dev/null @@ -1,450 +0,0 @@ -#!/usr/bin/env python3 -"""Opt-in real-model acceptance of an installed Core through its public API. - -Run stages in order: run, optionally restart Core/Runtime externally, continue, -then cancel. Each stage uses the same private report directory. No service or -container restart is performed here. Sessions and files are retained for inspection. - -Private model configuration shape (all three harnesses are required): -{"codex": {"model": "...", "model_provider": {"protocol": "responses", ...}}, - "claude_sdk": {"model": "...", "model_provider": {"protocol": "anthropic", ...}}, - "mcode": {"model": "...", "model_provider": {"protocol": "anthropic", ...}}} -See contracts/agents-api/model-execution.md for provider fields and limits. - -Example: python3 scripts/acceptance/acceptance.py run --base-url http://127.0.0.1:8091 - --caller-key-file /private/caller.key --model-config-file /private/models.json - --report-dir /private/release-acceptance - -This calls real models and leaves owned resources in place. A failed check remains -failed; this script does not translate native differences into synthetic success. -""" - -import argparse -import hashlib -import ipaddress -import json -import os -from pathlib import Path -import re -import shlex -import stat -import time -from urllib.error import HTTPError, URLError -from urllib.parse import urlencode, urlsplit -from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener -import uuid - -from harness_catalog import PROVIDERS - -# This campaign deliberately qualifies only these combinations. Catalog additions -# do not expand live model calls; supported combinations are declared in PROVIDERS. -CAMPAIGN = {"codex": "responses", "claude_sdk": "anthropic", "mcode": "anthropic"} -TERMINAL = {"completed", "cancelled", "failed"} -MAX_RESPONSE = 8 * 1024 * 1024 - - -class Failure(Exception): - """Messages are fixed labels, never remote response text or private values.""" - - -def require(condition, label): - if not condition: - raise Failure(label) - - -def identifier(value): - require(isinstance(value, str) and re.fullmatch(r"[A-Za-z0-9_-]{1,200}", value), - "invalid_resource_identifier") - return value - - -def digest(value): - return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest() - - -def private_file(path): - require(path.is_absolute() and not path.is_symlink(), "private_file_requires_absolute_regular_path") - info = path.stat() - require(stat.S_ISREG(info.st_mode) and info.st_mode & 0o077 == 0, - "private_file_permissions_must_exclude_group_and_others") - require(info.st_size <= MAX_RESPONSE, "private_file_too_large") - return path.read_text() - - -def validate_origin(raw, model=False): - require(isinstance(raw, str), "invalid_origin") - parsed = urlsplit(raw) - host = (parsed.hostname or "").lower().rstrip(".") - require(host and not parsed.username and not parsed.password and not parsed.query - and not parsed.fragment, "invalid_origin") - require(host not in {"api.openai.com", "platform.openai.com"}, "official_openai_origin_refused") - if model: - require(parsed.scheme == "https", "model_origin_requires_https") - else: - try: - loopback = ipaddress.ip_address(host).is_loopback - except ValueError: - loopback = host == "localhost" - require(parsed.scheme == "https" or (parsed.scheme == "http" and loopback), - "core_origin_requires_https_or_loopback") - return raw.rstrip("/") - - -def settings(args): - require(CAMPAIGN and all(harness in PROVIDERS and protocol in PROVIDERS[harness] - for harness, protocol in CAMPAIGN.items()), "unsupported_acceptance_campaign") - token = private_file(args.caller_key_file).strip() - require(token and not any(c in token for c in "\r\n\0"), "invalid_caller_key") - models = json.loads(private_file(args.model_config_file)) - require(isinstance(models, dict) and set(models) == set(CAMPAIGN), "require_all_three_harness_configs") - secrets = [token] - for harness, entry in models.items(): - require(isinstance(entry, dict) and set(entry) == {"model", "model_provider"}, "invalid_model_config_fields") - require(isinstance(entry["model"], str) and entry["model"].strip(), "invalid_model_name") - provider = entry["model_provider"] - require(isinstance(provider, dict), "invalid_model_provider") - allowed = {"protocol", "base_url", "api_key", "context_window", "max_output_tokens"} - require(set(provider) <= allowed and {"protocol", "base_url", "api_key"} <= set(provider), - "invalid_model_provider_fields") - validate_origin(provider["base_url"], model=True) - protocol = CAMPAIGN[harness] - require(provider["protocol"] == protocol, "provider_protocol_does_not_match_campaign") - key = provider["api_key"] - require(isinstance(key, str) and key and len(key) <= 16384 - and not any(c in key for c in "\r\n\0"), "invalid_model_provider_key") - secrets.append(key) - return token, models, secrets - - -class NoRedirect(HTTPRedirectHandler): - def redirect_request(self, req, fp, code, msg, headers, newurl): - raise Failure("redirect_refused") - - -class API: - def __init__(self, base, token, evidence): - self.base = base.removesuffix("/v1") + "/v1/agents" - self.token = token - self.evidence = evidence - self.opener = build_opener(ProxyHandler({}), NoRedirect()) - - def request(self, method, path, operation, body=None, query=None, key=None, binary=False): - headers = {"Authorization": "Bearer " + self.token, "OpenAI-Beta": "agents=v1"} - data = None - if body is not None: - headers["Content-Type"] = "application/json" - data = json.dumps(body).encode() - if key: - headers["Idempotency-Key"] = key - url = self.base + path + ("?" + urlencode(query) if query else "") - entry = {"operation": operation, "method": method} - self.evidence.append(entry) - try: - with self.opener.open(Request(url, data=data, headers=headers, method=method), timeout=30) as response: - entry["http_status"] = response.status - request_id = response.headers.get("x-request-id", "") - if re.fullmatch(r"[A-Za-z0-9_.:-]{1,200}", request_id): - entry["request_id"] = request_id - raw = response.read(MAX_RESPONSE + 1) - except HTTPError as error: - entry["http_status"] = error.code - error.close() - raise Failure("public_api_http_error") from None - except (URLError, TimeoutError, OSError): - raise Failure("public_api_transport_error") from None - require(len(raw) <= MAX_RESPONSE, "response_limit_exceeded") - if binary: - return raw - if method == "POST" and path.endswith("/events") and entry["http_status"] == 202 and not raw: - return {} - try: - value = json.loads(raw) - except (ValueError, UnicodeError): - raise Failure("invalid_public_json") from None - require(isinstance(value, dict), "invalid_public_object") - return value - - def get(self, path, operation, query=None): - return self.request("GET", path, operation, query=query) - - def listing(self, path, operation, query=None, files=False): - query = {"limit": 100, "order": "asc", **(query or {})} - rows, cursors = [], set() - for _ in range(100): - page = self.get(path, operation, query) - require(isinstance(page.get("data"), list), "invalid_list_page") - rows.extend(page["data"]) - require(all(isinstance(row, dict) for row in page["data"]), "invalid_list_row") - more = page.get("has_more") is not False and bool(page.get("next")) if files else page.get("has_more") - require(page.get("has_more") is not True or not files or more, "missing_files_cursor") - if not more: - return rows - require(page["data"], "empty_continuing_page") - cursor = page.get("next") if files else identifier(page["data"][-1].get("id")) - require(isinstance(cursor, str) and cursor not in cursors, "invalid_list_cursor") - cursors.add(cursor) - query["page" if files else "after"] = cursor - raise Failure("pagination_limit_exceeded") - - -def session_path(record): - return "/sessions/" + identifier(record["session_id"]) - - -def wait_turn(api, record, previous, timeout, status="completed"): - deadline = time.monotonic() + timeout - path = session_path(record) - while time.monotonic() < deadline: - current = api.get(path, "session.retrieve") - require(current.get("status") != "failed", "session_failed") - require(not current.get("required_actions"), "unexpected_required_action") - new = [row for row in api.listing(path + "/turns", "turns.list") if row.get("id") not in previous] - require(len(new) <= 1, "input_created_multiple_turns") - if new and new[0].get("status") in TERMINAL: - turn = new[0] - record.setdefault("terminal_turns", {})[identifier(turn.get("id"))] = turn["status"] - require(turn["status"] == status, "unexpected_terminal_turn_status") - if current.get("status") == "idle": - return turn - time.sleep(1) - raise Failure("turn_timeout") - - -def submit(api, record, text): - event = {"type": "agent.session.input.message", "input": [ - {"role": "user", "content": [{"type": "input_text", "text": text}]}]} - api.request("POST", session_path(record) + "/events", "events.message", - body={"events": [event]}, key=uuid.uuid4().hex) - - -def items(api, record): - return api.listing(session_path(record) + "/items", "items.list") - - -def turn_ids(api, record): - return {identifier(row.get("id")) for row in api.listing(session_path(record) + "/turns", "turns.list")} - - -def file_rows(api, record, directory): - return api.listing("/environments/" + identifier(record["environment_id"]) + "/files", - "files.list", {"path": directory}, files=True) - - -def verify_output(api, record, turn_id, path, expected): - rows = file_rows(api, record, "/workspace/outputs") - matching = [row for row in rows if row.get("path") == path] - require(len(matching) == 1, "native_output_missing_from_files") - row = matching[0] - require(row.get("environment_id") == record["environment_id"] - and row.get("object") == "agent.environment.file" - and row.get("size_bytes") == len(expected), "native_output_metadata_mismatch") - artifacts = api.listing(session_path(record) + "/artifacts", "artifacts.list") - matches = [row for row in artifacts if row.get("path") == path and row.get("turn_id") == turn_id] - require(len(matches) == 1, "captured_artifact_missing_or_duplicate") - artifact = matches[0] - aid = identifier(artifact.get("id")) - require(artifact.get("session_id") == record["session_id"] - and artifact.get("environment_id") == record["environment_id"] - and artifact.get("object") == "agent.session.artifact" - and artifact.get("size_bytes") == len(expected), "artifact_metadata_mismatch") - endpoint = session_path(record) + "/artifacts/" + aid - require(api.get(endpoint, "artifact.retrieve") == artifact, "artifact_retrieve_mismatch") - content = api.request("GET", endpoint + "/content", "artifact.content", binary=True) - require(content == expected, "artifact_bytes_mismatch") - return {"id": aid, "path": path, "turn_id": turn_id, "size_bytes": len(content), - "sha256": hashlib.sha256(content).hexdigest()} - - -def write_prompt(path, value): - command = "mkdir -p /workspace/outputs && printf %s " + shlex.quote(value) + " > " + shlex.quote(path) - return ("Use your native shell tool to execute exactly once: " + command - + ". Do not delegate or repeat the command. Read the file with your native tools " - "to verify its exact contents, then reply with those contents.") - - -def verify_answer(api, record, turn_id, wanted): - stored = items(api, record) - text = "\n".join(part.get("text", "") for row in stored - if row.get("turn_id") == turn_id and row.get("type") == "message" - and row.get("role") == "assistant" for part in row.get("content", []) - if part.get("type") == "output_text") - require(wanted in text, "native_answer_did_not_contain_expected_nonce") - return stored - - -def run(api, record, config, timeout, save): - require("session_id" not in record, "run_already_created_session_use_fresh_report_directory") - nonce = uuid.uuid4().hex - record.update(nonce=nonce, memory="remember-" + uuid.uuid4().hex, - output_path="/workspace/outputs/release-" + nonce + ".txt") - payload = {"agent": {"model": config["model"], "x_agents_core": {"harness": record["harness"]}}, - "environment": {"type": "openai_hosted"}, - "x_agents_core": {"model_provider": config["model_provider"]}, - "input": "Remember this conversation-only token without writing it to any file: " - + record["memory"] + ". " + write_prompt(record["output_path"], nonce), "stream": False} - creation_key = uuid.uuid4().hex - record["creation_idempotency_key"] = creation_key - save() - session = api.request("POST", "/sessions", "session.create", body=payload, key=creation_key) - record["session_id"] = identifier(session.get("id")) - environment = session.get("environment") or {} - record["environment_id"] = identifier(environment.get("id")) - save() - require(environment.get("type") == "openai_hosted", "wrong_environment_type") - turn = wait_turn(api, record, set(), timeout) - stored = verify_answer(api, record, turn["id"], nonce) - record["artifact"] = verify_output(api, record, turn["id"], record["output_path"], nonce.encode()) - record["items_digest"] = digest(stored) - - -def continuation(api, record, timeout): - path = session_path(record) - current = api.get(path, "session.retrieve") - require(current.get("status") == "idle", "continuation_requires_idle_session") - deadline = time.monotonic() + min(timeout, 60) - while time.monotonic() < deadline: - environment = api.get("/environments/" + identifier(record["environment_id"]), "environment.retrieve") - if environment.get("status") == "connected": - break - time.sleep(1) - else: - raise Failure("runtime_not_reconnected") - require(digest(items(api, record)) == record["items_digest"], "committed_items_changed") - old = record["artifact"] - verify_output(api, record, old["turn_id"], old["path"], record["nonce"].encode()) - before = turn_ids(api, record) - resumed = "/workspace/outputs/resumed-" + record["nonce"] + ".txt" - prompt = ("Recall the exact conversation-only remember- token from our first turn without reading " - "files or rerunning earlier commands. Write only that token, with no newline, to " + resumed - + " using native tools. Then reply with that token. Do not delegate.") - submit(api, record, prompt) - turn = wait_turn(api, record, before, timeout) - stored = verify_answer(api, record, turn["id"], record["memory"]) - record["resumed_artifact"] = verify_output(api, record, turn["id"], resumed, record["memory"].encode()) - record["items_digest"] = digest(stored) - - -def cancellation(api, record, timeout): - path = session_path(record) - require(api.get(path, "session.retrieve").get("status") == "idle", "cancel_requires_idle_session") - before = turn_ids(api, record) - directory = "/workspace" - prefix = directory + "/release-cancel-" + record["nonce"] - command = ("printf start >> " + shlex.quote(prefix + "-starts") - + "; while true; do printf x >> " + shlex.quote(prefix + "-ticks") + "; sleep 1; done") - submit(api, record, "Run this command exactly once with your native shell tool in the foreground: " - + command + ". Wait for it; it runs until cancelled. Do not background, delegate or retry.") - deadline, observed = time.monotonic() + timeout, 0 - while time.monotonic() < deadline: - turns = [row for row in api.listing(path + "/turns", "turns.list") if row.get("id") not in before] - require(len(turns) <= 1 and not any(row.get("status") in TERMINAL for row in turns), - "cancel_work_finished_before_cancellation") - rows = file_rows(api, record, directory) - sizes = {row.get("path"): row.get("size_bytes") for row in rows} - observed = sizes.get(prefix + "-ticks", 0) - if type(observed) is int and observed >= 3: - require(sizes.get(prefix + "-starts") == 5, "cancel_command_repeated") - break - time.sleep(1) - else: - raise Failure("native_cancel_effects_not_observed") - api.request("POST", path + "/events", "events.cancel", - body={"events": [{"type": "agent.session.input.cancel"}]}, key=uuid.uuid4().hex) - wait_turn(api, record, before, timeout, "cancelled") - def effects(): - return {row.get("path"): row.get("size_bytes") for row in file_rows(api, record, directory)} - settled = effects() - require(settled.get(prefix + "-starts") == 5 and settled.get(prefix + "-ticks", 0) >= observed, - "cancel_effect_metadata_mismatch") - time.sleep(3) - require(effects() == settled, "native_effects_continued_after_cancel") - record["cancel_effects"] = {"ticks_before_cancel": observed, - "ticks_after_cancel": settled[prefix + "-ticks"], "stable_seconds": 3} - record["items_digest"] = digest(items(api, record)) - - -def settle_failed_work(api, record): - """Try to stop only this acceptance Session; preserve the original failure.""" - if "session_id" not in record: - return "no_known_session" - try: - path = session_path(record) - current = api.get(path, "failure_cleanup.session") - if current.get("status") in {"idle", "failed"}: - return "already_settled" - api.request("POST", path + "/events", "failure_cleanup.cancel", - body={"events": [{"type": "agent.session.input.cancel"}]}, key=uuid.uuid4().hex) - deadline = time.monotonic() + 30 - while time.monotonic() < deadline: - current = api.get(path, "failure_cleanup.session") - if current.get("status") in {"idle", "failed"}: - return "settled_after_cancel" - time.sleep(1) - except Exception: - pass - return "unresolved_operator_cleanup_required" - - -def main(): - parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument("stage", choices=("run", "continue", "cancel")) - parser.add_argument("--base-url", required=True) - parser.add_argument("--caller-key-file", required=True, type=Path) - parser.add_argument("--model-config-file", required=True, type=Path) - parser.add_argument("--report-dir", required=True, type=Path) - parser.add_argument("--timeout", type=int, default=300) - parser.add_argument("--after-restart", action="store_true", help="Record an operator-performed restart before continue.") - args = parser.parse_args() - require(args.timeout > 0 and (not args.after_restart or args.stage == "continue"), "invalid_stage_options") - base = validate_origin(args.base_url) - token, models, secrets = settings(args) - require(args.report_dir.is_absolute() and not args.report_dir.is_symlink(), "absolute_report_directory_required") - args.report_dir.mkdir(mode=0o700, parents=True, exist_ok=True) - require(args.report_dir.stat().st_mode & 0o077 == 0, "report_directory_must_be_private") - failed = False - for harness in CAMPAIGN: - location = args.report_dir / (harness + ".json") - record = json.loads(private_file(location)) if location.exists() else {"harness": harness, "stages": {}} - require(record.get("harness") == harness, "report_harness_mismatch") - require(args.stage not in record["stages"], "stage_already_attempted_use_new_report_directory") - stage = {"passed": False, "requests": []} - record["stages"][args.stage] = stage - def save(): - serialized = json.dumps(record, indent=2) + "\n" - require(not any(secret in serialized for secret in secrets), "credential_detected_in_evidence") - temp = location.with_suffix(".tmp") - fd = os.open(temp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) - try: - with os.fdopen(fd, "w") as output: - output.write(serialized) - os.replace(temp, location) - finally: - temp.unlink(missing_ok=True) - api = API(base, token, stage["requests"]) - try: - save() - if args.stage == "run": - run(api, record, models[harness], args.timeout, save) - else: - require(record["stages"].get("run", {}).get("passed"), "initial_stage_did_not_pass") - if args.stage == "continue": - stage["operator_reported_restart"] = args.after_restart - continuation(api, record, args.timeout) - else: - cancellation(api, record, args.timeout) - stage["passed"] = True - except Exception as error: - failed = True - stage["failure"] = str(error) if isinstance(error, Failure) else "unexpected_local_or_response_error" - stage["cleanup"] = settle_failed_work(api, record) - save() - print(harness + ": " + args.stage + " " + ("passed" if stage["passed"] else "FAILED"), flush=True) - return 1 if failed else 0 - - -if __name__ == "__main__": - try: - raise SystemExit(main()) - except Exception as error: - label = str(error) if isinstance(error, Failure) else "invalid_configuration_or_local_io" - raise SystemExit("Acceptance stopped: " + label + "; private values and response bodies withheld.") from None diff --git a/scripts/acceptance/harness_catalog.py b/scripts/acceptance/harness_catalog.py deleted file mode 100644 index 79d8b6f57..000000000 --- a/scripts/acceptance/harness_catalog.py +++ /dev/null @@ -1,5 +0,0 @@ -# Code generated by scripts/generate-harness-catalog.py; DO NOT EDIT. -# Protocol support comes from the Go Harness declarations. -PROVIDERS = {'claude_sdk': ['anthropic'], - 'codex': ['responses'], - 'mcode': ['anthropic', 'responses', 'chat_completions']} diff --git a/scripts/acceptance/test_acceptance.py b/scripts/acceptance/test_acceptance.py deleted file mode 100644 index 4b9fd3823..000000000 --- a/scripts/acceptance/test_acceptance.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Validate acceptance inputs without contacting Core or model providers.""" -import copy -import json -from types import SimpleNamespace -import unittest -from unittest import mock - -import acceptance - - -class AcceptanceSettingsTests(unittest.TestCase): - def setUp(self): - self.models = { - harness: {"model": "fixture", "model_provider": { - "protocol": protocol, "base_url": "https://model.example/v1", "api_key": "private-key", - "context_window": 64000, "max_output_tokens": 4096, - }} for harness, protocol in acceptance.CAMPAIGN.items() - } - - def settings(self, models=None): - models = self.models if models is None else models - with mock.patch.object(acceptance, "private_file", side_effect=["caller-key", json.dumps(models)]): - return acceptance.settings(SimpleNamespace(caller_key_file=None, model_config_file=None)) - - def test_campaign_is_explicit_and_valid(self): - self.assertEqual(acceptance.CAMPAIGN, { - "codex": "responses", "claude_sdk": "anthropic", "mcode": "anthropic"}) - token, models, secrets = self.settings() - self.assertEqual(token, "caller-key") - self.assertEqual(models, self.models) - self.assertEqual(secrets, ["caller-key"] + ["private-key"] * 3) - - def test_catalog_support_does_not_expand_campaign(self): - for protocol in ("responses", "chat_completions"): - self.assertIn(protocol, acceptance.PROVIDERS["mcode"]) - self.models["mcode"]["model_provider"]["protocol"] = protocol - with self.subTest(protocol=protocol), self.assertRaisesRegex( - acceptance.Failure, "provider_protocol_does_not_match_campaign"): - self.settings() - expanded = {**acceptance.PROVIDERS, "additional": ["responses"]} - with mock.patch.object(acceptance, "PROVIDERS", expanded): - self.models["mcode"]["model_provider"]["protocol"] = "anthropic" - self.settings() - self.models["additional"] = copy.deepcopy(self.models["codex"]) - with self.assertRaisesRegex(acceptance.Failure, "require_all_three_harness_configs"): - self.settings() - - def test_unsupported_campaign_is_rejected_before_reading_secrets(self): - for campaign in ({}, {"unknown": "responses"}, {"codex": "anthropic"}): - with self.subTest(campaign=campaign), mock.patch.object(acceptance, "CAMPAIGN", campaign), \ - mock.patch.object(acceptance, "private_file") as private_file: - with self.assertRaisesRegex(acceptance.Failure, "unsupported_acceptance_campaign"): - acceptance.settings(SimpleNamespace()) - private_file.assert_not_called() - - def test_token_limits_are_optional(self): - for harness in acceptance.CAMPAIGN: - models = copy.deepcopy(self.models) - del models[harness]["model_provider"]["context_window"] - del models[harness]["model_provider"]["max_output_tokens"] - self.settings(models) - - def test_provider_validation_is_preserved(self): - for patch, label in ( - ({"unknown": "value"}, "invalid_model_provider_fields"), - ({"api_key": "secret\nvalue"}, "invalid_model_provider_key"), - ({"api_key": ""}, "invalid_model_provider_key"), - ({"base_url": "http://model.example"}, "model_origin_requires_https"), - ({"base_url": "https://api.openai.com"}, "official_openai_origin_refused"), - ({"protocol": "anthropic"}, "provider_protocol_does_not_match_campaign"), - ): - models = copy.deepcopy(self.models) - models["codex"]["model_provider"].update(patch) - with self.subTest(label=label), self.assertRaisesRegex(acceptance.Failure, label): - self.settings(models) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/ci_plan.py b/scripts/ci_plan.py index 76ec014cd..6aee80a37 100644 --- a/scripts/ci_plan.py +++ b/scripts/ci_plan.py @@ -31,7 +31,7 @@ } DEPENDENCY_INPUTS = { **dict.fromkeys(("go.mod", "go.sum", "go.work", "go.work.sum"), GO_JOBS), - **dict.fromkeys(("package.json", "pnpm-workspace.yaml", ".npmrc"), NODE_JOBS), + **dict.fromkeys(("package.json", "pnpm-workspace.yaml"), NODE_JOBS), "pnpm-lock.yaml": ("hygiene",), "apps/web/pnpm-lock.yaml": ("web", "web-acceptance"), "example/parsar/pnpm-lock.yaml": ("example",), @@ -68,8 +68,7 @@ (("contracts/",), (*GO, ".json", ".yaml", ".yml"), ("backend", "api", "native", "web", "web-acceptance", "example", "distribution")), (("packages/agents-client/",), (*GO, *WEB), ("backend", "api", "web", "web-acceptance", "example")), (("packages/claude-sdk-adapter/", "packages/mcode-harness/"), WEB, ("harness", "native", "backend", "distribution")), - (("packages/tsconfig/",), (".json",), ("harness", "native")), - (("deploy/node/", "scripts/acceptance/"), (".py", ".json", ".sh"), ("distribution",)), + (("deploy/node/",), (".py", ".json", ".sh"), ("distribution",)), (("deploy/compose/",), (".yaml", ".toml", ".json", ".txt"), ("distribution", "compose")), (("scripts/compose-smoke.py", "scripts/render-compose.py", "deploy/compose/test_compose.py"), (".py",), ("distribution", "compose")), (("deploy/install.sh", "deploy/install-source.py", "deploy/test_install.py", "scripts/publish-core-release.", diff --git a/scripts/ci_plan_test.py b/scripts/ci_plan_test.py index 542dae7f0..c7be150a1 100644 --- a/scripts/ci_plan_test.py +++ b/scripts/ci_plan_test.py @@ -53,7 +53,7 @@ def test_generated_outputs_keep_freshness_checks(self): spec.loader.exec_module(generator) with patch.object(generator, "go", side_effect=lambda source: source): outputs = {**generator.render_registrations(generator.packages()), **generator.render_docs([])} - for path in [*map(str, outputs), "packages/agents-client/src/harness-catalog.ts", "scripts/acceptance/harness_catalog.py"]: + for path in [*map(str, outputs), "packages/agents-client/src/harness-catalog.ts"]: with self.subTest(path=path): self.assertIn("distribution", self.jobs(path)) @@ -121,7 +121,7 @@ def test_dependencies_are_scoped_to_language_consumers(self): with self.subTest(path=path): self.assertEqual(self.jobs(path), {"hygiene", "backend", "distribution", "compose", "api", "native"}) self.assertTrue(ci.select([path])["image"]) - for path in ("package.json", "pnpm-workspace.yaml", ".npmrc"): + for path in ("package.json", "pnpm-workspace.yaml"): with self.subTest(path=path): self.assertEqual(self.jobs(path), {"hygiene", "harness", "example", "web", "web-acceptance", "website", "native"}) self.assertFalse(ci.select([path])["image"]) @@ -138,7 +138,6 @@ def test_module_locks_only_select_their_consumers(self): "packages/agents-client/pnpm-lock.yaml": {"web", "web-acceptance", "example"}, "packages/claude-sdk-adapter/pnpm-lock.yaml": {"harness", "native", "distribution"}, "packages/claude-sdk-adapter/package.json": {"harness", "native", "distribution"}, - "packages/tsconfig/base.json": {"harness", "native"}, "pnpm-lock.yaml": set(), } for path, jobs in cases.items(): diff --git a/scripts/generate-harness-catalog.py b/scripts/generate-harness-catalog.py index b159e0c89..a9bc5b00f 100644 --- a/scripts/generate-harness-catalog.py +++ b/scripts/generate-harness-catalog.py @@ -6,7 +6,6 @@ from pathlib import Path import re import subprocess -from pprint import pformat ROOT = Path(__file__).resolve().parent.parent DECLARATIONS = Path("internal/harnessconfig") @@ -41,12 +40,6 @@ def go(source): check=True, capture_output=True).stdout -def render_installer(protocols): - return ("# Code generated by scripts/generate-harness-catalog.py; DO NOT EDIT.\n" - "# Protocol support comes from the Go Harness declarations.\n" - "PROVIDERS = " + pformat(protocols, sort_dicts=True, width=100) + "\n") - - def render_client(harnesses, protocols): kinds = ", ".join(json.dumps(entry["kind"]) for entry in harnesses) labels = "\n".join(f' {json.dumps(entry["kind"])}: {json.dumps(entry["label"])},' for entry in harnesses) @@ -160,7 +153,6 @@ def projection(relative, content): raise SystemExit("Registered Harnesses differ from the declaration packages") for relative, content in render_docs(harnesses).items(): projection(relative, content) - projection(Path("scripts/acceptance/harness_catalog.py"), render_installer({h["kind"]: h["protocols"] for h in harnesses})) projection(Path("packages/agents-client/src/harness-catalog.ts"), render_client(harnesses, declared["protocols"])) if stale: raise SystemExit("Stale Harness catalog projections; run make generate-harness-catalog:\n" + "\n".join(stale)) diff --git a/scripts/generate-harness-catalog.test.py b/scripts/generate-harness-catalog.test.py index b54776f97..529a7f34a 100644 --- a/scripts/generate-harness-catalog.test.py +++ b/scripts/generate-harness-catalog.test.py @@ -21,12 +21,6 @@ def module(name, file): class HarnessCatalogTests(unittest.TestCase): - def test_installer_projection_includes_every_declaration(self): - protocols = {"additional": ["responses", "anthropic"], "native": []} - namespace = {} - exec(catalog.render_installer(protocols), namespace) - self.assertEqual(namespace["PROVIDERS"], protocols) - def test_new_declaration_reaches_all_projections(self): harnesses = [{"kind": "example", "label": "Example", "package": "example"}] generated = {**catalog.render_registrations(["example"]), **catalog.render_docs(harnesses)} diff --git a/scripts/harness-catalog/main.go b/scripts/harness-catalog/main.go index d12aafc0c..e217f3e72 100644 --- a/scripts/harness-catalog/main.go +++ b/scripts/harness-catalog/main.go @@ -11,20 +11,15 @@ import ( ) type harness struct { - Kind string `json:"kind"` - Label string `json:"label"` - Protocols []string `json:"protocols"` + Kind string `json:"kind"` + Label string `json:"label"` } func declarations() []harness { var result []harness for _, kind := range builtin.Kinds() { declaration, _ := builtin.Registry().Lookup(kind) - protocols := []string{} - for _, declared := range declaration.Providers { - protocols = append(protocols, declared.Protocol) - } - result = append(result, harness{Kind: kind, Label: declaration.Label, Protocols: protocols}) + result = append(result, harness{Kind: kind, Label: declaration.Label}) } return result } diff --git a/scripts/harness-catalog/main_test.go b/scripts/harness-catalog/main_test.go deleted file mode 100644 index cbd6e1a7f..000000000 --- a/scripts/harness-catalog/main_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" -) - -func TestProjectionMatchesEveryDeclaration(t *testing.T) { - got := declarations() - if len(got) != len(builtin.Kinds()) { - t.Fatal("projection membership differs from the registry") - } - for _, projected := range got { - declaration, ok := builtin.Registry().Lookup(projected.Kind) - if !ok || projected.Label != declaration.Label || len(projected.Protocols) != len(declaration.Providers) { - t.Fatalf("%s: projection differs from its declaration", projected.Kind) - } - for i, declared := range declaration.Providers { - if projected.Protocols[i] != declared.Protocol { - t.Fatalf("%s: protocol order differs", projected.Kind) - } - } - } -} diff --git a/services/core/internal/persistence/postgres/filepg/filepg_test.go b/services/core/internal/persistence/postgres/filepg/filepg_test.go index 1b5233769..01dd1664c 100644 --- a/services/core/internal/persistence/postgres/filepg/filepg_test.go +++ b/services/core/internal/persistence/postgres/filepg/filepg_test.go @@ -7,7 +7,6 @@ import ( "encoding/hex" "errors" "io" - "os" "strings" "testing" @@ -417,45 +416,6 @@ func TestFileReadReturnsConsumerErrors(t *testing.T) { } } -func TestFileLargeStream(t *testing.T) { - if os.Getenv("OAC_TEST_SOURCE_FILE_LARGE") != "1" { - t.Skip("opt-in 512 MiB source storage acceptance") - } - store, service := open(t, pgtest.Open(t)) - tenant := uuid.NewString() - chunk := bytes.Repeat([]byte("source\x00binary"), 20000) - want := sha256.New() - file, err := service.Create(t.Context(), files.CreateCommand{TenantID: tenant, Upload: func(w io.Writer) (files.Upload, error) { - for left := files.MaxBytes; left > 0; { - b := chunk[:min(int64(len(chunk)), left)] - if _, err := w.Write(b); err != nil { - return files.Upload{}, err - } - want.Write(b) - left -= int64(len(b)) - } - return files.Upload{Filename: "large.bin", Purpose: files.PurposeUserData}, nil - }}) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := service.Delete(context.Background(), files.DeleteCommand{TenantID: tenant, FileID: file.ID}); err != nil { - t.Error(err) - } - }) - got := sha256.New() - if err := store.Read(t.Context(), tenant, file.ID, func(meta files.File, r io.Reader) error { - n, err := io.CopyBuffer(got, r, chunk) - if n != files.MaxBytes || meta.SizeBytes != n { - t.Errorf("size: %d metadata: %d", n, meta.SizeBytes) - } - return err - }); err != nil || !bytes.Equal(got.Sum(nil), want.Sum(nil)) { - t.Fatalf("large stream mismatch: %v", err) - } -} - func sha256Hex(data []byte) string { sum := sha256.Sum256(data) return hex.EncodeToString(sum[:]) diff --git a/services/core/internal/runtimegateway/mcp_bearer_fixture_linux_test.go b/services/core/internal/runtimegateway/mcp_bearer_fixture_linux_test.go deleted file mode 100644 index 025061545..000000000 --- a/services/core/internal/runtimegateway/mcp_bearer_fixture_linux_test.go +++ /dev/null @@ -1,151 +0,0 @@ -//go:build linux - -package runtimegateway - -import ( - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "math/big" - "net" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - "sync" - "testing" - "time" - - "github.com/modelcontextprotocol/go-sdk/mcp" -) - -type mcpBearerFixture struct { - private, anonymous *httptest.Server - caFile, memory string - mu sync.Mutex - accepted, rejected, anonymousRequests, crossed int - calls map[string]int -} - -func newMCPBearerFixture(t *testing.T, root, token string) *mcpBearerFixture { - t.Helper() - f := &mcpBearerFixture{memory: "REMEMBER_" + mcpBearerNonce(t), calls: make(map[string]int)} - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal("cannot create owned TLS key") - } - now := time.Now() - cert := &x509.Certificate{SerialNumber: big.NewInt(now.UnixNano()), Subject: pkix.Name{CommonName: "Owned MCP acceptance CA"}, NotBefore: now.Add(-time.Minute), NotAfter: now.Add(time.Hour), IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature} - der, err := x509.CreateCertificate(rand.Reader, cert, cert, &key.PublicKey, key) - if err != nil { - t.Fatal("cannot create owned TLS certificate") - } - leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal("cannot create owned TLS leaf key") - } - leaf := &x509.Certificate{SerialNumber: big.NewInt(now.UnixNano() + 1), Subject: pkix.Name{CommonName: "Owned MCP HTTPS endpoint"}, NotBefore: cert.NotBefore, NotAfter: cert.NotAfter, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}} - leafDER, err := x509.CreateCertificate(rand.Reader, leaf, cert, &leafKey.PublicKey, key) - if err != nil { - t.Fatal("cannot sign owned HTTPS leaf certificate") - } - // Keep the host's provider trust roots alongside the owned MCP CA. - var roots []byte - for _, path := range []string{"/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt"} { - if data, err := os.ReadFile(path); err == nil { - roots = data - break - } - } - if len(roots) == 0 { - t.Fatal("system CA bundle required for real provider TLS") - } - roots = append(append(roots, '\n'), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})...) - f.caFile = filepath.Join(root, "trusted-ca.pem") - if err := os.WriteFile(f.caFile, roots, 0600); err != nil { - t.Fatal("cannot save owned trust bundle") - } - start := func(anonymous bool) *httptest.Server { - server := mcp.NewServer(&mcp.Implementation{Name: "owned-bearer-acceptance", Version: "1"}, nil) - names := []string{"remember", "fail"} - if anonymous { - names = []string{"ping"} - } - for _, name := range names { - mcp.AddTool(server, &mcp.Tool{Name: name, Description: map[string]string{"remember": "Return the unpredictable value to remember.", "fail": "Return an intentional ordinary tool error. Do not retry.", "ping": "Confirm this separate anonymous MCP server works."}[name]}, func(_ context.Context, _ *mcp.CallToolRequest, args struct { - Tag string `json:"tag" jsonschema:"The requested verification tag"` - }) (*mcp.CallToolResult, any, error) { - f.mu.Lock() - f.calls[name]++ - f.mu.Unlock() - text := f.memory - if name == "fail" { - text = "INTENTIONAL_MCP_TOOL_ERROR:" + args.Tag - } - if name == "ping" { - text = "ANONYMOUS_OK" - } - return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: text}}, IsError: name == "fail"}, nil, nil - }) - } - transport := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return server }, nil) - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - f.mu.Lock() - allowed := len(r.Header.Values("Authorization")) == 1 && r.Header.Get("Authorization") == "Bearer "+token - if anonymous { - f.anonymousRequests++ - allowed = len(r.Header.Values("Authorization")) == 0 - if !allowed { - f.crossed++ - } - } else if allowed { - f.accepted++ - } else { - f.rejected++ - } - f.mu.Unlock() - if !allowed { - w.WriteHeader(http.StatusUnauthorized) - return - } - transport.ServeHTTP(w, r) - }) - s := httptest.NewUnstartedServer(handler) - s.TLS = &tls.Config{Certificates: []tls.Certificate{{Certificate: [][]byte{leafDER, der}, PrivateKey: leafKey}}, MinVersion: tls.VersionTLS12} - s.StartTLS() - t.Cleanup(s.Close) - return s - } - f.private, f.anonymous = start(false), start(true) - for _, authorization := range []string{"", "Bearer wrong-" + mcpBearerNonce(t)} { - req, _ := http.NewRequest(http.MethodPost, f.private.URL, strings.NewReader("{}")) - if authorization != "" { - req.Header.Set("Authorization", authorization) - } - response, err := f.private.Client().Do(req) - if err != nil { - t.Fatal("owned HTTPS authorization probe failed") - } - response.Body.Close() - if response.StatusCode != http.StatusUnauthorized { - t.Fatal("missing or wrong bearer was accepted") - } - } - return f -} - -func (f *mcpBearerFixture) observations() map[string]any { - f.mu.Lock() - defer f.mu.Unlock() - calls := make(map[string]int, len(f.calls)) - for name, count := range f.calls { - calls[name] = count - } - return map[string]any{"authenticated_requests": f.accepted, "rejected_requests": f.rejected, "anonymous_requests": f.anonymousRequests, "cross_forwarded_authorization": f.crossed, "tool_calls": calls} -} diff --git a/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go b/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go deleted file mode 100644 index 4a11b0739..000000000 --- a/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go +++ /dev/null @@ -1,227 +0,0 @@ -//go:build linux - -package runtimegateway - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http/httptest" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/go-chi/chi/v5" - "github.com/google/uuid" -) - -type mcpBearerTurn struct { - Events []proto.Envelope `json:"events"` - Done proto.DonePayload `json:"done"` - NativeLaunches int `json:"native_launches"` - BearerEnvironmentReference string `json:"bearer_environment_reference"` -} - -func TestLiveMCPBearerGatewayColdContinuation(t *testing.T) { - daemon, native, provider, root := mcpBearerSettings(t) - t.Logf("private MCP bearer evidence: %s", root) - token, runner := mcpBearerNonce(t), mcpBearerNonce(t) - fixture := newMCPBearerFixture(t, root, token) - capture := &mcpBearerLog{} - proof := map[string]any{"scope": "Private gateway -> built daemon -> pinned Codex -> real MiniMax with owned HTTPS MCP; no public API or Vault execution claim", "model": "MiniMax-M3", "provider_url": "https://api.minimax.cn/v1", "daemon_sha256": mcpBearerBinaryHash(t, daemon), "codex_sha256": mcpBearerBinaryHash(t, native)} - var turns []*mcpBearerTurn - t.Cleanup(func() { - mcpBearerSafeWrite(t, filepath.Join(root, "captured.log"), capture.snapshot(), token, provider) - histories := mcpBearerScanArtifacts(t, root, token, provider) - if !t.Failed() && histories == 0 { - t.Error("native history artifact missing") - } - proof["native_history_files"], proof["mcp"], proof["turns"] = histories, fixture.observations(), turns - proof["passed"] = !t.Failed() - data, err := json.MarshalIndent(proof, "", " ") - if err != nil { - t.Error("cannot encode safe acceptance evidence") - return - } - mcpBearerSafeWrite(t, filepath.Join(root, "proof.json"), data, token, provider) - }) - id := uuid.NewString() - registry := NewRegistry() - auth := NewAuthenticator(&stubRuntimeStore{ok: true, row: runtimedevice.Credential{ID: id, WorkspaceID: uuid.NewString(), Type: runtimedevice.RuntimeTypeAgentDaemon, CredentialHash: runtimedevice.HashCredential(runner)}}) - router := chi.NewRouter() - server := httptest.NewServer(router) - t.Cleanup(server.Close) - handler := NewHandler(HandlerConfig{Authenticator: auth, Registry: registry, PublicWSURL: "ws" + strings.TrimPrefix(server.URL, "http") + "/agent-daemon/ws", Log: func(format string, args ...any) { _, _ = fmt.Fprintf(capture, format+"\n", args...) }}) - RegisterRoutes(router, handler) - mcpBearerStartDaemon(t, root, daemon, native, provider, fixture.caFile, server.URL, id, runner, capture) - ctx, cancel := context.WithTimeout(t.Context(), 6*time.Minute) - defer cancel() - var peer *Session - t.Cleanup(func() { - if peer != nil { - peer.Close("owned MCP acceptance finished") - } - }) - connect := func() { - t.Helper() - var err error - if peer, err = registry.WaitForDevice(ctx, id, 30*time.Second); err != nil { - t.Fatal("built daemon did not connect through the real gateway") - } - ready := time.Now().Add(30 * time.Second) - for { - info, found, known := peer.AgentKindStatus("codex") - if known && found && info.Available { - if !info.Capabilities.MCPHTTPTools || !info.Capabilities.MCPHTTPBearerAuth || !info.Capabilities.ToolObservations || !info.Capabilities.DurableTurns || !info.Capabilities.EnvironmentNone { - t.Fatal("built daemon did not advertise the required private execution capabilities") - } - proof["codex_descriptor"] = info - return - } - if time.Now().After(ready) { - t.Fatal("pinned Codex capability discovery did not complete") - } - time.Sleep(50 * time.Millisecond) - } - } - connect() - allowed, anonymousTools := []string{"remember", "fail"}, []string{"ping"} - servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "private_mcp", ServerURL: fixture.private.URL, AllowedTools: &allowed, BearerToken: &token}, {ConnectionOrigin: "service", ServerLabel: "anonymous_mcp", ServerURL: fixture.anonymous.URL, AllowedTools: &anonymousTools}} - run := func(prompt, resume string, expected map[string]string) *mcpBearerTurn { - t.Helper() - turn := &mcpBearerTurn{} - turns = append(turns, turn) - runID, prepareID := uuid.NewString(), uuid.NewString() - request := proto.PromptRequestPayload{AgentKind: "codex", AgentStateKey: "mcp-bearer-acceptance", AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, MCPHTTPServers: &servers, AgentOptions: map[string]any{"model": "MiniMax-M3"}, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}} - send := func(kind, id string, payload any) { - t.Helper() - envelope, err := proto.NewEnvelope(kind, id, payload) - if err != nil || peer.Send(ctx, envelope) != nil { - t.Fatal("cannot dispatch the private MCP request") - } - } - statuses, err := peer.SubscribePreparation(prepareID) - if err != nil { - t.Fatal("cannot subscribe to preparation before real daemon dispatch") - } - defer peer.UnsubscribePreparation(prepareID) - send(proto.TypeExecutionPrepare, prepareID, proto.ExecutionPreparePayload{SessionID: "mcp-bearer-acceptance", Configuration: request}) - var ready proto.PreparationStatusPayload - for ready.State != "ready" { - select { - case event, ok := <-statuses.Events: - if !ok || event.DecodePayload(&ready) != nil || ready.State != "preparing" && ready.State != "ready" { - t.Fatal("real daemon did not prepare the private MCP Executor") - } - case <-ctx.Done(): - t.Fatal("real daemon preparation timed out") - } - } - sub, err := peer.SubscribeDurable(runID) - if err != nil { - t.Fatal("cannot subscribe before real daemon dispatch") - } - defer peer.Unsubscribe(runID) - send(proto.TypeExecutionStart, prepareID, proto.ExecutionStartPayload{Handle: ready.Handle, ExecutorID: ready.ExecutorID, RunID: runID, Input: proto.TextInput(prompt)}) - mcpBearerCollectTurn(t, ctx, sub, runID, turn, expected, token, provider) - // The Executor stays warm after Done. Reconnecting gives the daemon a fresh - // Router and closes the Executor, so the next Turn is a cold continuation. - peer.Close("cold continuation") - connect() - turn.NativeLaunches = mcpBearerReleased(t, root) - turn.BearerEnvironmentReference = mcpBearerConfigReference(t, root, token) - return turn - } - first := run("Call private_mcp remember exactly once with tag first. Also call anonymous_mcp ping exactly once with tag first. Reply with the exact remembered value and the ping result. Do not use any other tool.", "", map[string]string{"remember": fixture.memory, "ping": "ANONYMOUS_OK"}) - nativeID, _ := first.Done.Metadata[proto.DoneMetaAgentSessionID].(string) - if nativeID == "" || !strings.Contains(first.Done.Content, fixture.memory) { - t.Fatal("first real model Turn did not return its native identity and unpredictable tool result") - } - second := run("Recall the exact remembered value from the preceding tool result. Call private_mcp fail exactly once with tag cold-followup. It intentionally reports an ordinary tool error; do not retry. Reply with the earlier remembered value and the exact error text. Do not call remember, ping or any other tool.", nativeID, map[string]string{"fail": "INTENTIONAL_MCP_TOOL_ERROR:cold-followup"}) - if second.Done.Metadata[proto.DoneMetaAgentSessionID] != nativeID || !strings.Contains(second.Done.Content, fixture.memory) || !strings.Contains(second.Done.Content, "INTENTIONAL_MCP_TOOL_ERROR:cold-followup") { - t.Fatal("cold native continuation lost history, identity or ordinary error output") - } - if second.NativeLaunches <= first.NativeLaunches || second.BearerEnvironmentReference == first.BearerEnvironmentReference { - t.Fatal("cold continuation did not create a fresh native process and bearer environment reference") - } - fixture.mu.Lock() - valid := fixture.accepted > 0 && fixture.rejected == 2 && fixture.anonymousRequests > 0 && fixture.crossed == 0 && fixture.calls["remember"] == 1 && fixture.calls["ping"] == 1 && fixture.calls["fail"] == 1 - fixture.mu.Unlock() - if !valid { - t.Fatal("HTTPS authorization, per-server separation or expected tool-call counts failed") - } -} - -func mcpBearerCollectTurn(t *testing.T, ctx context.Context, sub *Subscription, runID string, turn *mcpBearerTurn, expected map[string]string, secrets ...string) { - t.Helper() - before, after := make(map[string]string), make(map[string]int) - for { - var event proto.Envelope - select { - case value, ok := <-sub.Events: - if !ok { - t.Fatal("real daemon subscription closed before Done") - } - event = value - case <-ctx.Done(): - t.Fatal("real MiniMax MCP Turn timed out") - } - data, _ := json.Marshal(event) - for _, secret := range secrets { - if bytes.Contains(data, []byte(secret)) { - t.Fatal("secret appeared in a daemon event") - } - } - if event.ID != runID { - t.Fatal("real daemon event changed Run identity") - } - turn.Events = append(turn.Events, event) - switch event.Type { - case proto.TypeError: - t.Fatal("unexpected execution failure during private MCP acceptance") - case proto.TypeToolCall: - var call proto.ToolCallPayload - if event.DecodePayload(&call) != nil || call.Observation == nil { - t.Fatal("missing normalized native tool observation") - } - obs := call.Observation - output, wanted := expected[obs.Name] - server := "private_mcp" - if obs.Name == "ping" { - server = "anonymous_mcp" - } - if !wanted || obs.Kind != "mcp" || obs.Server != server || call.ID == "" { - t.Fatal("unexpected native tool identity or server") - } - if call.Stage == "before" { - if obs.Status != "in_progress" || before[call.ID] != "" { - t.Fatal("invalid native tool start observation") - } - before[call.ID] = obs.Name - continue - } - status := "completed" - if obs.Name == "fail" { - status = "failed" - } - if call.Stage != "after" || before[call.ID] != obs.Name || obs.Status != status || !bytes.Contains(obs.Output, []byte(output)) || (len(obs.Error) != 0 && string(obs.Error) != "null") { - t.Fatal("native tool result, lifecycle or ordinary error semantics changed") - } - after[obs.Name]++ - case proto.TypeDone: - if event.DecodePayload(&turn.Done) != nil || sub.Err() != nil || turn.Done.Content == "" || turn.Done.Metadata[proto.DoneMetaAgentSessionType] != "codex_thread" { - t.Fatal("invalid native Done or incomplete gateway delivery") - } - for name := range expected { - if after[name] != 1 { - t.Fatal("expected exactly one complete native observation per requested tool") - } - } - return - } - } -} diff --git a/services/core/internal/runtimegateway/mcp_bearer_process_linux_test.go b/services/core/internal/runtimegateway/mcp_bearer_process_linux_test.go deleted file mode 100644 index e2d70ae89..000000000 --- a/services/core/internal/runtimegateway/mcp_bearer_process_linux_test.go +++ /dev/null @@ -1,296 +0,0 @@ -//go:build linux - -package runtimegateway - -import ( - "bytes" - "context" - "crypto/rand" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "io" - "io/fs" - "os" - "os/exec" - "path/filepath" - "regexp" - "strconv" - "strings" - "sync" - "syscall" - "testing" - "time" -) - -func mcpBearerSettings(t *testing.T) (daemon, native, provider, root string) { - t.Helper() - names := []string{"OAC_TEST_MCP_BEARER_DAEMON_BIN", "OAC_TEST_MCP_BEARER_CODEX_BIN", "OAC_TEST_MCP_BEARER_MODEL_KEY_FILE", "OAC_TEST_MCP_BEARER_PROOF_DIR"} - for _, name := range names { - if os.Getenv(name) == "" { - t.Skip("real MCP bearer acceptance requires all four explicit binary, provider-file and proof settings") - } - if !filepath.IsAbs(os.Getenv(name)) { - t.Fatal("MCP bearer acceptance settings must be absolute paths") - } - } - home, err := os.UserHomeDir() - if err != nil { - t.Fatal("cannot resolve managed runtime home") - } - proof, err := filepath.EvalSymlinks(os.Getenv(names[3])) - if err != nil { - t.Fatal("explicit proof directory must already exist") - } - managed, err := filepath.EvalSymlinks(filepath.Join(home, ".oac")) - if err != nil || !strings.HasPrefix(proof, managed+string(os.PathSeparator)) { - t.Fatal("proof directory must be below ~/.oac") - } - root, err = os.MkdirTemp(proof, "mcp-bearer-") - if err != nil { - t.Fatal("cannot allocate owned proof directory") - } - key, err := os.ReadFile(os.Getenv(names[2])) - if err != nil { - t.Fatal("cannot read explicitly supplied provider key file") - } - provider = strings.TrimSpace(string(key)) - if provider == "" { - t.Fatal("explicit provider key file is empty") - } - return os.Getenv(names[0]), os.Getenv(names[1]), provider, root -} - -func mcpBearerNonce(t *testing.T) string { - t.Helper() - value := make([]byte, 32) - if _, err := rand.Read(value); err != nil { - t.Fatal("cannot generate unpredictable acceptance value") - } - return hex.EncodeToString(value) -} - -type mcpBearerLog struct { - mu sync.Mutex - data bytes.Buffer -} - -func (w *mcpBearerLog) Write(p []byte) (int, error) { - w.mu.Lock() - defer w.mu.Unlock() - return w.data.Write(p) -} -func (w *mcpBearerLog) snapshot() []byte { - w.mu.Lock() - defer w.mu.Unlock() - return bytes.Clone(w.data.Bytes()) -} - -func mcpBearerStartDaemon(t *testing.T, root, daemon, native, provider, caFile, base, id, runner string, log *mcpBearerLog) { - t.Helper() - for _, dir := range []string{"home", "tmp", "runtime/daemon/execution"} { - if err := os.MkdirAll(filepath.Join(root, dir), 0700); err != nil { - t.Fatal("cannot create owned daemon directories") - } - } - auth, _ := json.Marshal(map[string]string{"server_url": base, "runtime_id": id, "runner_credential": runner}) - if err := os.WriteFile(filepath.Join(root, "runtime/daemon/execution/auth.json"), auth, 0600); err != nil { - t.Fatal("cannot configure owned daemon identity") - } - wrapper := filepath.Join(root, "native-wrapper") - script := `#!/bin/sh -set -eu -for argument in "$@"; do - if [ "$argument" = app-server ]; then - printf '%s %s\n' "$$" "$(awk '{print $22}' /proc/$$/stat)" >> "$OAC_TEST_MCP_BEARER_STARTS" - printf '%s\0' "$@" >> "$OAC_TEST_MCP_BEARER_ARGV" - break - fi -done -exec "$OAC_TEST_MCP_BEARER_NATIVE" -c 'model_provider="minimax_validation"' -c 'model_providers.minimax_validation.name="MiniMax validation"' -c 'model_providers.minimax_validation.base_url="https://api.minimax.cn/v1"' -c 'model_providers.minimax_validation.env_key="MINIMAX_VALIDATION_KEY"' -c 'model_providers.minimax_validation.wire_api="responses"' "$@" -` - if err := os.WriteFile(wrapper, []byte(script), 0700); err != nil { - t.Fatal("cannot create owned native wrapper") - } - env := []string{"HOME=" + filepath.Join(root, "home"), "TMPDIR=" + filepath.Join(root, "tmp"), "OAC_RUNTIME_HOME=" + filepath.Join(root, "runtime"), "OAC_RUNTIME_CODEX_BIN=" + wrapper, "MINIMAX_VALIDATION_KEY=" + provider, "SSL_CERT_FILE=" + caFile, "OAC_TEST_MCP_BEARER_NATIVE=" + native, "OAC_TEST_MCP_BEARER_STARTS=" + filepath.Join(root, "native-starts"), "OAC_TEST_MCP_BEARER_ARGV=" + filepath.Join(root, "native-argv")} - for _, name := range []string{"PATH", "LANG", "LC_ALL", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "all_proxy", "no_proxy"} { - if value, ok := os.LookupEnv(name); ok { - env = append(env, name+"="+value) - } - } - versionCtx, cancel := context.WithTimeout(t.Context(), 15*time.Second) - defer cancel() - version := exec.CommandContext(versionCtx, native, "--version") - version.Env, version.Dir = env, root - output, err := version.Output() - if err != nil || strings.TrimSpace(string(output)) != "codex-cli 0.153.4" { - t.Fatal("explicit native binary must be pinned Codex 0.153.4") - } - cmd := exec.Command(daemon, "connect", "--profile", "execution") - cmd.Env, cmd.Dir, cmd.Stdout, cmd.Stderr = env, root, log, log - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - if err := cmd.Start(); err != nil { - t.Fatal("cannot start explicitly supplied daemon binary") - } - stopped := make(chan error, 1) - go func() { stopped <- cmd.Wait() }() - t.Cleanup(func() { - _ = cmd.Process.Signal(syscall.SIGTERM) - select { - case <-stopped: - case <-time.After(10 * time.Second): - _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) - <-stopped - } - // Native RPC children own separate groups. Match recorded start time before cleanup. - _, _ = mcpBearerProcesses(root, true) - deadline := time.Now().Add(3 * time.Second) - for { - _, active := mcpBearerProcesses(root, false) - if active == 0 { - break - } - if time.Now().After(deadline) { - t.Error("owned native process did not exit during cleanup") - break - } - time.Sleep(25 * time.Millisecond) - } - }) -} - -func mcpBearerProcesses(root string, kill bool) (launches, active int) { - data, _ := os.ReadFile(filepath.Join(root, "native-starts")) - for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { - fields := strings.Fields(line) - if len(fields) != 2 { - continue - } - pid, err := strconv.Atoi(fields[0]) - if err != nil || pid <= 1 { - continue - } - launches++ - stat, err := os.ReadFile(filepath.Join("/proc", fields[0], "stat")) - if err != nil { - continue - } - _, tail, ok := strings.Cut(string(stat), ") ") - state := strings.Fields(tail) - if !ok || len(state) <= 19 || state[19] != fields[1] || state[0] == "Z" { - continue - } - active++ - if group, err := syscall.Getpgid(pid); kill && err == nil && group == pid { - _ = syscall.Kill(-group, syscall.SIGKILL) - } - } - return launches, active -} - -func mcpBearerReleased(t *testing.T, root string) int { - t.Helper() - deadline := time.Now().Add(5 * time.Second) - for { - launches, active := mcpBearerProcesses(root, false) - if launches > 0 && active == 0 { - return launches - } - if time.Now().After(deadline) { - t.Fatal("the owned native process was not released") - } - time.Sleep(25 * time.Millisecond) - } -} - -func mcpBearerConfigReference(t *testing.T, root, token string) string { - t.Helper() - pattern := regexp.MustCompile(`(?m)^bearer_token_env_var\s*=\s*"([A-Za-z_][A-Za-z0-9_]*)"`) - var references []string - err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { - if err != nil { - return err - } - if entry.Name() != "config.toml" || !entry.Type().IsRegular() { - return nil - } - data, err := os.ReadFile(path) - if err != nil { - return err - } - if bytes.Contains(data, []byte(token)) { - t.Error("MCP bearer persisted in native configuration") - return nil - } - for _, match := range pattern.FindAllSubmatch(data, -1) { - references = append(references, string(match[1])) - } - return nil - }) - if err != nil || len(references) != 1 { - t.Fatal("expected one native bearer environment reference") - } - return references[0] -} - -func mcpBearerSafeWrite(t *testing.T, path string, data []byte, secrets ...string) { - t.Helper() - for _, secret := range secrets { - if secret != "" && bytes.Contains(data, []byte(secret)) { - t.Error("secret detected in captured acceptance artifact") - _ = os.Remove(path) - return - } - } - if err := os.WriteFile(path, data, 0600); err != nil { - t.Error("cannot save safe acceptance artifact") - } -} - -func mcpBearerScanArtifacts(t *testing.T, root, token, provider string) int { - t.Helper() - histories := 0 - err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { - if err != nil { - return err - } - if !entry.Type().IsRegular() { - return nil - } - data, err := os.ReadFile(path) - if err != nil { - return err - } - if strings.Contains(path, "/sessions/") && strings.HasSuffix(path, ".jsonl") { - histories++ - } - mcpLeak, providerPresent := bytes.Contains(data, []byte(token)), bytes.Contains(data, []byte(provider)) - if mcpLeak || providerPresent { - if err := os.Remove(path); err != nil { - t.Error("cannot remove secret-bearing owned artifact") - } - if mcpLeak { - t.Error("injected MCP bearer persisted in an owned runtime artifact") - } - } - return nil - }) - if err != nil { - t.Error("cannot scan owned runtime artifacts") - } - return histories -} - -func mcpBearerBinaryHash(t *testing.T, path string) string { - t.Helper() - file, err := os.Open(path) - if err != nil { - t.Fatal("cannot read explicit acceptance binary") - } - defer file.Close() - digest := sha256.New() - if _, err := io.Copy(digest, file); err != nil { - t.Fatal("cannot fingerprint acceptance binary") - } - return hex.EncodeToString(digest.Sum(nil)) -} diff --git a/services/core/internal/sandbox/node/health_linux_test.go b/services/core/internal/sandbox/node/health_linux_test.go index feaa7d1a4..086f089c7 100644 --- a/services/core/internal/sandbox/node/health_linux_test.go +++ b/services/core/internal/sandbox/node/health_linux_test.go @@ -202,22 +202,11 @@ func TestHostHealthReadOnly(t *testing.T) { if h.AvailableDiskBytes == nil || *h.AvailableDiskBytes < 0 || *h.AvailableDiskBytes > int64(disk.Blocks)*disk.Bsize { t.Fatal("state filesystem free space unavailable or invalid") } - if os.Getenv("OAC_TEST_NODE_HEALTH_PROBE") == "1" { - data, err := json.Marshal(h) - if err != nil { - t.Fatal(err) - } - t.Log(string(data)) - } time.Sleep(100 * time.Millisecond) h = a.health(sampler) if h.CPUUtilization == nil || *h.CPUUtilization < 0 || *h.CPUUtilization > 1 || !h.ObservedAt.After(before) { t.Fatalf("invalid follow-up observation: %+v", h) } - if os.Getenv("OAC_TEST_NODE_HEALTH_PROBE") == "1" { - data, _ := json.Marshal(h) - t.Log(string(data)) - } if fresh := a.health(new(hostHealthSampler)); fresh.CPUUtilization != nil { t.Fatal("new connection reused a CPU baseline") } diff --git a/services/core/tests/e2b_native_isolation.py b/services/core/tests/e2b_native_isolation.py deleted file mode 100644 index 9b21c54c6..000000000 --- a/services/core/tests/e2b_native_isolation.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Run through a real native tool, with non-secret canaries supplied by acceptance.""" -import glob -import hashlib -import json -import os -import subprocess -import urllib.error -import urllib.request -from pathlib import Path - -fixture = json.loads(Path('/workspace/isolation-fixture.json').read_text()) -paths = ['/environment/staging/canary', - '/home/runtime/.oac/daemon/executor-key.json', - fixture['history_path'], - '/proc/1/root/home/runtime/.oac/daemon/executor-key.json', - '/root/.oac/e2b/ready.json', '/var/run/docker.sock'] - - -def read(path): - try: - with open(path, 'rb') as stream: - return stream.read(65536) - except OSError: - return None - - -result = {'private_readable': {path: read(path) is not None for path in paths}} -assert not any(result['private_readable'].values()), result -result['pid_namespace'] = os.readlink('/proc/self/ns/pid') -result['pid_namespace_isolated'] = result['pid_namespace'] != fixture['outer_pid_namespace'] -assert result['pid_namespace_isolated'], 'native tools share outer daemon PID namespace' -result['pid1_comm'] = (read('/proc/1/comm') or b'').decode(errors='replace').strip() -result['pid1_env_readable'] = read('/proc/1/environ') is not None -leaks = [] -for path in glob.glob('/proc/[0-9]*/environ') + glob.glob('/proc/[0-9]*/cmdline'): - data = read(path) - if data is None: - continue - for field in data.split(b'\x00'): - value = field.split(b'=', 1)[-1] - if hashlib.sha256(value).hexdigest() in fixture['secret_hashes']: - leaks.append(path) -result['sensitive_process_leaks'] = leaks -assert not leaks, 'protected outer process credential accessible' -for name, command in [('sudo', ['sudo', '-n', 'id', '-u']), - ('privileged_account', ['su', 'user', '-c', 'id -u'])]: - try: - process = subprocess.run(command, input='', capture_output=True, text=True, timeout=8) - result[name + '_denied'] = process.returncode != 0 - except FileNotFoundError: - result[name + '_unavailable'] = True - result[name + '_denied'] = True - assert result[name + '_denied'], 'native shell gained privileged account' -try: - urllib.request.urlopen('http://127.0.0.1:49983/envs', timeout=5) -except urllib.error.HTTPError as error: - assert error.code in [401, 403], error.code - result['envd_denied'] = True -except (urllib.error.URLError, PermissionError, TimeoutError): - result['envd_denied'] = True -else: - raise AssertionError('unauthenticated envd authority accessible') -result['passed'] = True -Path('/workspace/isolation-result.json').write_text(json.dumps(result)) -print('ISOLATION-PASSED') diff --git a/services/core/tests/integration/environment_executor_command_test.go b/services/core/tests/integration/environment_executor_command_test.go deleted file mode 100644 index 2803532f5..000000000 --- a/services/core/tests/integration/environment_executor_command_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package integration - -import ( - "encoding/json" - "errors" - "os" - "os/exec" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/google/uuid" -) - -func TestEnvironmentExecutorOperatorCommand(t *testing.T) { - binary := os.Getenv("OAC_TEST_ENVIRONMENT_KEY_BINARY") - if binary == "" { - t.Skip("built environment-key operator executable required") - } - s, pool := testStore(t) - tenant := uuid.NewString() - principal := FixtureExecutorPrincipal(t, s, tenant) - keyID := uuid.NewString() - command := func(owner string, success bool, flags ...string) string { - t.Helper() - args := append([]string{"--tenant", owner, "--organization", principal.OrganizationID, "--project", principal.ProjectID, "--subject-kind", principal.SubjectKind, "--subject-id", principal.SubjectID, "--key-id", keyID}, flags...) - cmd := exec.CommandContext(t.Context(), binary, args...) - cmd.Env = append(os.Environ(), "OAC_DATABASE_URL="+pool.Config().ConnConfig.ConnString()) - data, err := cmd.Output() - if (err == nil) != success { - t.Fatal("unexpected operator outcome", flags) - } - if !success || (len(flags) == 1 && flags[0] == "--revoke") { - if len(data) != 0 { - t.Fatal("failed/revoke command emitted secret output") - } - return "" - } - var output struct { - KeyID string `json:"key_id"` - EnvironmentID string `json:"environment_id"` - Token string `json:"executor_token"` - } - if json.Unmarshal(data, &output) != nil || output.KeyID != keyID || output.EnvironmentID != "" || output.Token == "" { - t.Fatal("invalid operator output") - } - return output.Token - } - command(uuid.NewString(), false) - first := command(tenant, true) - session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "operator-key", Configuration: json.RawMessage(`{"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`)}) - if err != nil { - t.Fatal(err) - } - environment, err := sessionAdapter(s).GetSessionEnvironment(t.Context(), tenant, session.ID) - if err != nil { - t.Fatal(err) - } - - command(tenant, false) - command(tenant, false, "--rotate", "--revoke") - command(tenant, false, "--rotate", "--environment", environment.ID) - next := command(tenant, true, "--rotate") - if next == first { - t.Fatal("rotation returned the same key") - } - if _, err := sessionAdapter(s).AuthenticateEnvironmentExecutor(t.Context(), environment.ID, runtimedevice.HashCredential(first)); !errors.Is(err, sessions.ErrNotFound) { - t.Fatal("old command credential retained authority", err) - } - if owner, err := sessionAdapter(s).AuthenticateEnvironmentExecutor(t.Context(), environment.ID, runtimedevice.HashCredential(next)); err != nil || owner != tenant { - t.Fatal("rotated command credential failed", err) - } - command(tenant, true, "--revoke") - if _, err := sessionAdapter(s).AuthenticateEnvironmentExecutor(t.Context(), environment.ID, runtimedevice.HashCredential(next)); !errors.Is(err, sessions.ErrNotFound) { - t.Fatal("revoked command credential retained authority", err) - } - t.Log("built operator command issued before Session creation, rejected duplicate/foreign requests, rotated and revoked durable credentials") -} diff --git a/services/core/tests/official_admin_management.py b/services/core/tests/official_admin_management.py deleted file mode 100644 index 2b4201e46..000000000 --- a/services/core/tests/official_admin_management.py +++ /dev/null @@ -1,405 +0,0 @@ -"""Administrator management and real Project-asset execution against a deployed Core. - -Read private JSON settings from stdin: base, admin, model, harness and optional -model_provider (the complete write-only provider object). run_model defaults to -true; false performs resource checks only and never reports model acceptance. -The deployment and its qualified runtime must already be running. This script -creates two Projects, exercises shared keys and Project isolation, and cleans up -only its own resources. -""" -import base64 -from datetime import datetime -import io -import json -import secrets -import sys -import time -import traceback -import uuid -import zipfile - -import httpx2 -import openai -from openai import DefaultHttpxClient, OpenAI - - -ADMIN_PATHS = {"agent": "agents", "skill": "skills", "file": "files", - "environment_template": "environment-templates", "vault": "vaults", - "session": "sessions"} -PUBLIC_PATHS = {"agent": "agents", "skill": "skills", "file": "files", - "environment_template": "agents/environments/templates", "vault": "vaults", - "session": "agents/sessions"} - - -def proof_bundle(marker): - manifest = ("---\nname: copy-proof\ndescription: Verify copied confidential assets.\n---\n" - "Run python3 /environment/initialization/capabilities/skills/copy-proof/scripts/check.py.\n") - script = f'''import os -from pathlib import Path -assert os.environ['COPY_PRIVATE'] == {marker!r} -assert Path('/workspace/copy-source.txt').read_text() == {marker!r} -assert Path('/workspace/copy-setup.txt').read_text() == {marker!r} -assert Path('/workspace/copy-inline.txt').read_text() == {marker!r} -Path('/workspace/outputs').mkdir(exist_ok=True) -Path('/workspace/outputs/copy-proof.txt').write_text('COPY_VERIFIED') -print('COPY_VERIFIED') -''' - output = io.BytesIO() - with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as archive: - archive.writestr("copy-proof/SKILL.md", manifest) - archive.writestr("copy-proof/scripts/check.py", script) - return output.getvalue() - - -def main(): - assert openai.__version__ == "3.13.0", "Use the pinned official SDK" - settings = json.load(sys.stdin) - base = settings["base"].rstrip("/") - admin = {"Authorization": "Bearer " + settings["admin"], "X-Core-Console-Actor": "admin-acceptance"} - private = "copy-private-" + secrets.token_hex(20) - secret_values = [private, settings["admin"]] - if settings.get("model_provider"): - secret_values.append(settings["model_provider"]["api_key"]) - owned, keys, clients, projects, revoked = set(), [], [], [], set() - session_id = None - with httpx2.Client(trust_env=False, timeout=30) as http: - def request(method, path, expected=200, token=None, headers=None, **kwargs): - response = http.request(method, base + path, headers={ - **(admin if token is None else {"Authorization": "Bearer " + token, "OpenAI-Beta": "agents=v1"}), - **(headers or {})}, **kwargs) - assert response.status_code == expected, f"{method} {path}: expected {expected}, got {response.status_code}" - return response - - def safe(response): - assert all(value not in response.text for value in secret_values), "Private value in metadata response" - return response.json() - - def project_path(key): - return "/core/v1/projects/" + key["project_id"] - - def project_metadata(response): - value = safe(response) - assert set(value) == {"id", "name", "created_at", "archived_at", "active_key_count"} - assert value["created_at"] is not None - uuid.UUID(value["id"]) - return value - - def issue(project, name): - key = request("POST", "/core/v1/projects/" + project["id"] + "/keys", - expected=201, json={"name": name}).json() - keys.append(key) - secret_values.append(key["key"]) - assert set(key) == {"id", "project_id", "name", "prefix", "created_at", "revoked_at", "key"} - assert key["project_id"] == project["id"] and key["revoked_at"] is None - assert key["created_at"] is not None - uuid.UUID(key["id"]) - return key - - def revoke(key): - request("DELETE", project_path(key) + "/keys/" + key["id"]) - revoked.add(key["id"]) - - def operations(key, kind, resource): - return safe(request("GET", project_path(key) + "/write-operations", params={ - "resource_type": kind, "resource_id": resource, "limit": 100}))["data"] - - def owner(key, kind, resource): - return safe(request("GET", project_path(key) + "/resource-owners", params={ - "resource_type": kind, "resource_ids": resource}))["data"][0] - - def admin_resource(key, kind, resource): - return project_path(key) + f"/{ADMIN_PATHS[kind]}/{resource}" - - def remember(key, kind, resource): - owned.add((key["project_id"], kind, resource)) - return resource - - def equal_reads(key, kind, resource): - public = request("GET", f"/v1/{PUBLIC_PATHS[kind]}/{resource}", token=key["key"]) - management = request("GET", admin_resource(key, kind, resource)) - assert management.content == public.content, "Admin and public resource responses differ" - safe(management) - - def denied_reference(key, body): - safe(request("POST", "/v1/agents/sessions", expected=404, token=key["key"], json=body)) - - def wait_idle(client, sid, wanted_turns): - deadline = time.monotonic() + settings.get("timeout", 360) - while time.monotonic() < deadline: - state = client.beta.agents.sessions.retrieve(sid) - turns = list(client.beta.agents.sessions.turns.list(sid, order="asc")) - assert state.status != "failed", "Real Session failed" - assert all(turn.status not in ("failed", "cancelled") for turn in turns), "Real Turn failed" - if len(turns) >= wanted_turns and turns[-1].status == "completed" and state.status == "idle": - return state - time.sleep(0.5) - raise AssertionError("Real execution timed out") - - try: - for name in ("source", "target"): - created = request("POST", "/core/v1/projects", expected=201, json={"name": "admin-proof-" + name}) - project = created.json() - projects.append(project) - project_metadata(created) - assert project["active_key_count"] == 0 and project["archived_at"] is None - source = issue(projects[0], "source-first") - target = issue(projects[1], "target-first") - peer = issue(projects[0], "source-peer") - for key in (source, target, peer): - clients.append(OpenAI(base_url=base + "/v1", api_key=key["key"], max_retries=0, - http_client=DefaultHttpxClient(trust_env=False), _strict_response_validation=True)) - a, b, a_peer = clients - assert source["id"] != peer["id"] and source["project_id"] == peer["project_id"] != target["project_id"] - catalog = safe(request("GET", "/core/v1/projects", params={"limit": 100})) - assert isinstance(catalog["has_more"], bool) - assert {project["id"] for project in projects} <= {project["id"] for project in catalog["data"]} - for key, count in ((source, 2), (target, 1)): - listed = safe(request("GET", project_path(key) + "/keys")) - assert listed["has_more"] is False and len(listed["data"]) == count - assert all("key" not in item and item["project_id"] == key["project_id"] for item in listed["data"]) - for path in PUBLIC_PATHS.values(): - assert safe(request("GET", "/v1/" + path, token=target["key"]))["data"] == [] - request("GET", "/core/v1/projects", expected=401, token=source["key"]) - request("GET", "/v1/agents", expected=401, headers={"OpenAI-Beta": "agents=v1"}) - - file = a.files.create(file=("copy-source.txt", private.encode()), purpose="user_data") - remember(source, "file", file.id) - archive = proof_bundle(private) - with zipfile.ZipFile(io.BytesIO(archive)) as bundle: - upload_files = [(name, bundle.read(name), "application/octet-stream") for name in bundle.namelist()] - skill = a.skills.create(files=upload_files) - remember(source, "skill", skill.id) - # The peer key edits a Skill created by another key in the same Project. - for _ in range(2): - a_peer.skills.versions.create(skill_id=skill.id, files=upload_files, default=False) - a_peer.skills.update(skill.id, default_version="3") - a_peer.skills.versions.delete(version="2", skill_id=skill.id) - template = a_peer.beta.agents.environments.templates.create( - name="Confidential copy proof", env={"COPY_PRIVATE": private}, - setup_commands=[{"command": "printf %s " + private + " > /workspace/copy-setup.txt"}], - skills=[{"type": "skill_reference", "skill_id": skill.id, "version": "3"}], - files=[{"type": "file_id", "path": "/workspace/copy-source.txt", "file_id": file.id}, - {"type": "inline", "path": "/workspace/copy-inline.txt", "data": base64.b64encode(private.encode()).decode()}]) - remember(source, "environment_template", template.id) - provider = {"harness": settings["harness"]} - if settings.get("model_provider"): - provider["model_provider"] = settings["model_provider"] - agent = a.beta.agents.create(model=settings["model"], - instructions="Use the installed copy-proof skill and run its Python verifier. Report COPY_VERIFIED only after it succeeds.", - extra_body={"x_agents_core": provider}) - remember(source, "agent", agent.id) - vault = a.beta.agents.vaults.create(name="Copy credentials") - remember(source, "vault", vault.id) - static = a.beta.agents.vaults.credentials.create(vault.id, name="Static", auth={ - "type": "static_bearer", "mcp_server_url": "https://copy.example/mcp", "token": private}) - - shared = [("agent", agent.id), ("skill", skill.id), ("file", file.id), - ("environment_template", template.id), ("vault", vault.id)] - for key in (source, peer): - for kind, resource in shared: - equal_reads(key, kind, resource) - listed = safe(request("GET", "/v1/" + PUBLIC_PATHS[kind], token=key["key"])) - assert resource in {item["id"] for item in listed["data"]} - safe(request("GET", f"/v1/vaults/{vault.id}/credentials/{static.id}", token=key["key"])) - for key, name in ((peer, "Peer edit"), (source, "Creator edit")): - a_client = a_peer if key is peer else a - a_client.beta.agents.update(agent.id, name=name) - a_client.beta.agents.environments.templates.update(template.id, name=name) - request("POST", f"/v1/vaults/{vault.id}/credentials/{static.id}", token=key["key"], - json={"auth": {"type": "static_bearer", "token": private}}) - for kind, resource, creator in [("agent", agent.id, source), ("skill", skill.id, source), - ("file", file.id, source), ("environment_template", template.id, peer), - ("vault", vault.id, source), ("credential", static.id, source)]: - assert owner(source, kind, resource)["api_key"]["id"] == creator["id"] - writes = operations(source, kind, resource) - if kind in ("agent", "skill", "environment_template", "credential"): - assert {source["id"], peer["id"]} <= {item["api_key"]["id"] for item in writes} - # Files and Vaults have no update endpoint; exercise cross-key deletion. - for creator, remover in ((a, a_peer), (a_peer, a)): - scratch_file = creator.files.create(file=("shared-delete.txt", b"shared"), purpose="user_data") - remember(source, "file", scratch_file.id) - remover.files.delete(scratch_file.id) - scratch_vault = creator.beta.agents.vaults.create(name="Shared deletion") - remember(source, "vault", scratch_vault.id) - remover.beta.agents.vaults.delete(scratch_vault.id) - renamed = project_metadata(request("POST", project_path(source), json={"name": "Renamed project proof"})) - assert renamed == {**projects[0], "name": "Renamed project proof", "active_key_count": 2} - projects[0].update(renamed) - equal_reads(peer, "agent", agent.id) - - for kind, resource in [("agent", agent.id), ("skill", skill.id), ("file", file.id), - ("environment_template", template.id), ("vault", vault.id)]: - equal_reads(source, kind, resource) - assert safe(request("GET", "/v1/" + PUBLIC_PATHS[kind], token=target["key"]))["data"] == [] - request("GET", admin_resource(target, kind, resource), expected=404) - path = f"/v1/{PUBLIC_PATHS[kind]}/{resource}" - for method in ("GET", "DELETE"): - request(method, path, expected=404, token=target["key"]) - for kind, resource, patch in [("agent", agent.id, {"name": "foreign"}), - ("skill", skill.id, {"default_version": "1"}), - ("environment_template", template.id, {"name": "foreign"})]: - request("POST", f"/v1/{PUBLIC_PATHS[kind]}/{resource}", expected=404, token=target["key"], json=patch) - for method in ("GET", "DELETE"): - request(method, f"/v1/skills/{skill.id}/versions/1", expected=404, token=target["key"]) - request("POST", project_path(source) + "/keys/" + source["id"] + "/reset", expected=404) - request("DELETE", project_path(target) + "/keys/" + source["id"], expected=404) - credential_path = f"/v1/vaults/{vault.id}/credentials/{static.id}" - for method in ("GET", "DELETE"): - request(method, credential_path, expected=404, token=target["key"]) - request("POST", credential_path, expected=404, token=target["key"], - json={"auth": {"type": "static_bearer", "token": private}}) - credential_metadata = request("GET", credential_path, token=source["key"]) - credential_admin = request("GET", admin_resource(source, "vault", vault.id) + "/credentials/" + static.id) - assert credential_admin.content == credential_metadata.content - safe(credential_admin) - request("GET", admin_resource(source, "file", file.id) + "/content", expected=404) - denied_reference(target, {"agent_id": agent.id, "environment": {"type": "openai_hosted"}}) - denied_reference(target, {"agent": {"model": settings["model"]}, "environment": { - "type": "openai_hosted", "environment_template_id": template.id}}) - denied_reference(target, {"agent": {"model": settings["model"]}, "environment": { - "type": "openai_hosted", "skills": [{"type": "skill_reference", "skill_id": skill.id}]}}) - denied_reference(target, {"agent": {"model": settings["model"]}, "environment": { - "type": "openai_hosted", "files": [{"type": "file_id", "path": "/workspace/foreign", "file_id": file.id}]}}) - denied_reference(target, {"agent": {"model": settings["model"]}, "environment": {"type": "openai_hosted"}, "vault_ids": [vault.id]}) - - target_vault = b.beta.agents.vaults.create(name="Empty target attachment") - remember(target, "vault", target_vault.id) - request("GET", "/v1/vaults/" + target_vault.id, expected=404, token=source["key"]) - credential_errors = [] - # The pinned MCP selection contract returns 400 for both missing - # and foreign credentials within an explicitly attached Vault. - for reference in (static.id, str(uuid.uuid4())): - response = safe(request("POST", "/v1/agents/sessions", expected=400, token=target["key"], json={ - "agent": {"model": settings["model"], "tools": [{"type": "mcp", "server_label": "foreign", - "transport": {"type": "http", "server_url": "https://copy.example/mcp"}, "credential_id": reference}]}, - "environment": {"type": "openai_hosted"}, "vault_ids": [target_vault.id]})) - response["error"]["message"] = response["error"]["message"].replace(reference, "reference") - credential_errors.append(response) - assert credential_errors[0] == credential_errors[1], "Foreign credential differs from missing credential" - print("Shared keys, provenance, Project isolation and confidential metadata passed.", flush=True) - - if settings.get("run_model", True): - # One real Turn with the Project's own assets. Its creation key is - # revoked while the Turn runs; a replacement key continues the Session. - session = a.beta.agents.sessions.create(agent_id=agent.id, environment={ - "type": "openai_hosted", "environment_template_id": template.id}, - input="Run the installed copy-proof skill's Python verifier now.", - extra_headers={"Idempotency-Key": str(uuid.uuid4())}) - session_id = remember(source, "session", session.id) - creation_key, old_secret = source["id"], source["key"] - replacement = issue(projects[0], "source-rotation") - revoke(source) - revoked_at = next(key for key in safe(request("GET", project_path(replacement) + "/keys"))["data"] - if key["id"] == creation_key)["revoked_at"] - source = replacement - a.api_key = replacement["key"] - request("GET", "/v1/agents/sessions/" + session_id, expected=401, token=old_secret) - completed = wait_idle(a, session_id, 1) - turn = list(a.beta.agents.sessions.turns.list(session_id))[0] - assert turn.completed_at >= int(datetime.fromisoformat(revoked_at).timestamp()), "Turn ended before revocation" - assert completed.usage is not None and completed.usage.total_tokens > 0, "Real model usage missing" - artifacts = list(a.beta.agents.sessions.artifacts.list(session_id)) - artifact = next(item for item in artifacts if item.path == "/workspace/outputs/copy-proof.txt") - assert a.beta.agents.sessions.artifacts.content(artifact.id, session_id=session_id).read() == b"COPY_VERIFIED" - a.beta.agents.sessions.update(session_id, metadata={"continued_by": "replacement"}) - equal_reads(source, "session", session_id) - for suffix in ("", "/turns", "/items", "/artifacts", "/artifacts/" + artifact.id): - management = request("GET", admin_resource(source, "session", session_id) + suffix) - public = request("GET", "/v1/agents/sessions/" + session_id + suffix, token=source["key"]) - assert management.content == public.content, "Admin Session history differs" - request("GET", "/v1/agents/sessions/" + session_id + suffix, expected=404, token=target["key"]) - assert request("GET", admin_resource(source, "session", session_id) + "/artifacts/" + artifact.id + "/content").content == b"COPY_VERIFIED" - request("POST", "/v1/agents/sessions/" + session_id, expected=404, token=target["key"], - json={"metadata": {"forbidden": "foreign-project"}}) - request("DELETE", "/v1/agents/sessions/" + session_id, expected=404, token=target["key"]) - request("POST", "/v1/agents/sessions/" + session_id + "/events", expected=404, token=target["key"], - json={"events": [{"type": "agent.session.input.message", "input": [ - {"role": "user", "content": [{"type": "input_text", "text": "foreign"}]}]}]}) - assert owner(source, "session", session_id)["api_key"]["id"] == creation_key - assert {creation_key, replacement["id"]} <= {item["api_key"]["id"] for item in operations(source, "session", session_id)} - key_summary = safe(request("GET", "/core/v1/summary", params={ - "group_by": "key", "project_id": source["project_id"]}))["data"] - assert sum(row["sessions"]["total"] for row in key_summary) == 1 - group = next(row for row in key_summary if row["key_id"] == creation_key) - assert group["project_id"] == source["project_id"] and group["sessions"]["total"] == 1 and group["assets"] is None - assert group["usage"]["total_tokens"] == a.beta.agents.sessions.retrieve(session_id).usage.total_tokens - assert all(row["sessions"]["total"] == 0 for row in key_summary if row["key_id"] != creation_key) - print("Real Project-asset execution, Artifact reads and key rotation continuation passed.", flush=True) - else: - print("Real model execution not requested; resource checks only.", flush=True) - - before_archive = {kind: request("GET", admin_resource(source, kind, resource)).content for kind, resource in shared} - revoke(source) - request("GET", "/v1/agents", expected=401, token=source["key"]) - equal_reads(peer, "agent", agent.id) - peer_metadata = safe(request("GET", project_path(peer) + "/keys"))["data"] - assert next(key for key in peer_metadata if key["id"] == peer["id"])["revoked_at"] is None - archived = project_metadata(request("POST", project_path(source) + "/archive")) - assert archived["archived_at"] is not None and archived["active_key_count"] == 0 - assert all(archived[field] == projects[0][field] for field in ("id", "name", "created_at")) - projects[0].update(archived) - for key in (source, peer): - request("GET", "/v1/agents", expected=401, token=key["key"]) - assert all(key["revoked_at"] is not None for key in safe(request("GET", project_path(source) + "/keys"))["data"]) - for kind, resource in shared: - assert request("GET", admin_resource(source, kind, resource)).content == before_archive[kind] - request("POST", project_path(source) + "/keys", expected=409, json={"name": "archived"}) - for group_by in ("project", "agent", "key"): - summary = safe(request("GET", "/core/v1/summary", params={ - "group_by": group_by, "project_id": target["project_id"]})) - assert all(row["project_id"] == target["project_id"] for row in summary["data"]) - safe(request("GET", "/core/v1/audit-log", params={"project_id": target["project_id"]})) - print("Project rename, archive and retained reads passed.", flush=True) - finally: - failures = [] - if session_id is not None: - # Read through the administrator route; cancel only with a key this run - # has not revoked, in a Project that is not archived. - session_path = base + f"/core/v1/projects/{projects[0]['id']}/sessions/{session_id}" - try: - state = http.get(session_path, headers=admin).json()["status"] - if state == "in_progress": - live = [key for key in keys if key["project_id"] == projects[0]["id"] and key["id"] not in revoked] - if projects[0]["archived_at"] is not None or not live: - raise RuntimeError("no usable key") - http.post(base + f"/v1/agents/sessions/{session_id}/events", json={"events": [{"type": "agent.session.input.cancel"}]}, - headers={"Authorization": "Bearer " + live[0]["key"], "OpenAI-Beta": "agents=v1", "Idempotency-Key": str(uuid.uuid4())}) - deadline = time.monotonic() + 60 - while state == "in_progress" and time.monotonic() < deadline: - time.sleep(0.5) - state = http.get(session_path, headers=admin).json()["status"] - if state == "in_progress": - failures.append("session_stop") - except (httpx2.HTTPError, KeyError, ValueError, RuntimeError): - failures.append("session_stop") - # Sessions must finish before deletion; never remove another run's assets. - priority = {kind: index for index, kind in enumerate(("session", "agent", "environment_template", "skill", "vault", "file"))} - for project_id, kind, resource in sorted(owned, key=lambda value: priority[value[1]]): - try: - response = http.delete(base + f"/core/v1/projects/{project_id}/{ADMIN_PATHS[kind]}/{resource}", headers=admin) - if response.status_code not in (200, 204, 404): - failures.append(kind) - except httpx2.HTTPError: - failures.append(kind) - for project in projects: - if project["archived_at"] is None: - response = http.post(base + "/core/v1/projects/" + project["id"] + "/archive", headers=admin) - if response.status_code != 200: - failures.append("project_archive") - for key in keys: - if http.get(base + "/v1/agents", headers={"Authorization": "Bearer " + key["key"], "OpenAI-Beta": "agents=v1"}).status_code != 401: - failures.append("revoked_key_authentication") - for client in clients: - client.close() - if failures: - raise AssertionError("Owned cleanup incomplete: " + ", ".join(failures)) from None - print("All owned resources deleted, Projects archived and issued keys revoked.", flush=True) - - -if __name__ == "__main__": - try: - main() - except openai.APIError as error: - frames = [frame.lineno for frame in traceback.extract_tb(error.__traceback__) if frame.filename == __file__] - print("SDK failure at acceptance lines:", frames, file=sys.stderr) - raise RuntimeError(f"Official SDK failed: {type(error).__name__}, HTTP {getattr(error, 'status_code', 'unavailable')}") from None diff --git a/services/core/tests/official_environment_activity.py b/services/core/tests/official_environment_activity.py deleted file mode 100644 index 24b3c1a70..000000000 --- a/services/core/tests/official_environment_activity.py +++ /dev/null @@ -1,252 +0,0 @@ -"""Accept public reads/SSE for privately provisioned, caller-connected native work.""" - -import importlib.metadata -import json -import os -from pathlib import Path -import sys -import threading -import time - -sys.dont_write_bytecode = True - -import httpx2 -from openai import NotFoundError, OpenAI - - -def main(): - settings = json.load(sys.stdin) - base, token, foreign = (settings[key] for key in ("base", "token", "foreign_token")) - session_id, agent_id = settings["session_id"], settings["agent_id"] - directory = Path(settings["evidence"]) - os.umask(0o077) - root = Path(__file__).resolve().parents[3] - pin = json.loads((root / "contracts/agents-api/upstream.json").read_text()) - distribution = importlib.metadata.distribution("openai") - source = json.loads(distribution.read_text("direct_url.json") or "{}") - assert distribution.version == pin["sdk_version"] - assert source.get("vcs_info", {}).get("commit_id") == pin["commit"] - expected_environment = { - "id": settings["environment_id"], "type": "self_hosted", "capability_directories": [], - "remote_url": settings["remote_url"], "workspace_directory": settings["workspace_directory"], - } - action = {"type": "environment_connection", "environment_id": expected_environment["id"]} - proof = {"scope": "private Session provisioning/input reservation; public read/SSE acceptance; non-text initial and mixed self-hosted input remain gated", - "sdk_commit": pin["commit"], "sdk_version": distribution.version, "snapshots": {}} - observations = {"sdk": [], "raw": []} - ready = {name: threading.Event() for name in observations} - waiting = {name: threading.Event() for name in observations} - done = {name: threading.Event() for name in observations} - failures = [] - lock = threading.Lock() - - def write_private(name, value): - text = json.dumps(value, indent=2) - assert token not in text and foreign not in text, "caller credential appeared in public evidence" - temporary = directory / (name + ".tmp") - temporary.write_text(text) - temporary.chmod(0o600) - temporary.replace(directory / (name + ".json")) - - def client(key): - return OpenAI(api_key=key, base_url=base + "/v1", max_retries=0, - _strict_response_validation=True, - http_client=httpx2.Client(trust_env=False, timeout=30)) - - def check_session(value, status): - assert value["id"] == session_id and value["agent"]["id"] == agent_id - assert value["object"] == "agent.session" and value["environment"] == expected_environment - assert value["status"] == status and value["error"] is None - assert value["required_actions"] == ([action] if status == "requires_action" else []) - assert value["vault_ids"] == [] and isinstance(value["metadata"], dict) - assert isinstance(value["last_active_at"], int) and isinstance(value["created_at"], int) - - def await_observers(signals, timeout, label): - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - with lock: - if failures: - raise AssertionError("public event observer failed") from failures[0] - if all(signal.is_set() for signal in signals.values()): - return - time.sleep(0.025) - raise AssertionError(label + " timed out") - - def observe(name): - completed = set() - - def accept(value): - with lock: - observations[name].append(value) - kind = value["type"] - assert kind != "error" and kind not in ("agent.session.failed", "agent.session.turn.failed", "agent.session.turn.cancelled") - if kind == "agent.session.requires_action": - check_session(value["session"], "requires_action") - waiting[name].set() - if kind == "agent.session.turn.completed": - completed.add(value["turn"]["id"]) - return kind == "agent.session.idle" and len(completed) == 2 - - try: - if name == "sdk": - with client(token) as api: - with api.beta.agents.sessions.events.stream(session_id, timeout=450) as stream: - ready[name].set() - for event in stream: - if accept(event.to_dict()): - return - else: - with httpx2.Client(trust_env=False, timeout=450) as raw: - with raw.stream("GET", base + "/v1/agents/sessions/" + session_id + "/events", - headers={"Authorization": "Bearer " + token, "OpenAI-Beta": "agents=v1"}) as response: - assert response.status_code == 200 - assert response.headers["content-type"] == "text/event-stream" - ready[name].set() - for line in response.iter_lines(): - if line.startswith("data: ") and accept(json.loads(line[6:])): - return - raise AssertionError("live stream ended before both native Turns completed") - except BaseException as error: - with lock: - failures.append(error) - finally: - done[name].set() - - try: - with client(token) as api, client(foreign) as stranger, httpx2.Client( - base_url=base + "/v1", trust_env=False, timeout=15, - headers={"Authorization": "Bearer " + token, "OpenAI-Beta": "agents=v1", "Host": "untrusted.example"}, - ) as raw: - sessions = api.beta.agents.sessions - - def snapshot(name, status): - retrieved = sessions.retrieve(session_id) - check_session(retrieved.to_dict(), status) - listed = list(sessions.list(agent_id=agent_id, limit=1)) - assert listed == [retrieved] - response = raw.get("/agents/sessions/" + session_id) - assert response.status_code == 200 - value = response.json() - check_session(value, status) - page = raw.get("/agents/sessions", params={"agent_id": agent_id, "limit": 1}) - assert page.status_code == 200 and page.json() == {"object": "list", "data": [value], "has_more": False, "first_id": value["id"], "last_id": value["id"]} - proof["snapshots"][name] = value - return value - - initial = snapshot("initial", "idle") - assert initial["created_at"] == initial["last_active_at"] and initial["usage"] is None - assert list(sessions.turns.list(session_id)) == [] and list(sessions.items.list(session_id)) == [] - assert list(stranger.beta.agents.sessions.list(agent_id=agent_id)) == [] - for operation in ( - lambda: stranger.beta.agents.sessions.retrieve(session_id), - lambda: stranger.beta.agents.sessions.events.stream(session_id), - lambda: stranger.beta.agents.sessions.turns.list(session_id), - lambda: stranger.beta.agents.sessions.items.list(session_id), - ): - try: - operation() - except NotFoundError: - pass - else: - raise AssertionError("foreign tenant accessed the target Session") - for suffix in ("", "/events", "/turns", "/items"): - assert raw.get("/agents/sessions/" + session_id + suffix, - headers={"Authorization": "Bearer " + foreign}).status_code == 404 - create = raw.post("/agents/sessions", json={"agent": {"model": "MiniMax-M3"}, "input": [{"role": "user", "content": [ - {"type": "input_image", "image_url": ""}]}], - "environment": {"type": "self_hosted", "workspace_directory": settings["workspace_directory"]}}) - assert create.status_code == 400 - submit = raw.post("/agents/sessions/" + session_id + "/events", json={"events": [{ - "type": "agent.session.input.message", "input": [{"role": "user", "content": [ - {"type": "input_text", "text": "Mixed public input must remain disabled."}]}]}, - {"type": "agent.session.input.cancel"}]}) - assert submit.status_code == 400 - proof["tenant_isolation"] = True - proof["unsupported_nontext_initial_and_mixed_input_rejected"] = True - - for name in observations: - threading.Thread(target=observe, args=(name,), daemon=True).start() - await_observers(ready, 25, "SDK and raw live subscriptions") - write_private("ready", {}) - await_observers(waiting, 25, "pre-Turn connection action") - pending = snapshot("waiting", "requires_action") - assert pending["usage"] is None - assert list(sessions.turns.list(session_id)) == [] and list(sessions.items.list(session_id)) == [] - assert raw.get("/agents/sessions/" + session_id + "/turns").json()["data"] == [] - assert raw.get("/agents/sessions/" + session_id + "/items").json()["data"] == [] - with lock: - for values in observations.values(): - assert len(values) == 1 and values[0]["type"] == "agent.session.requires_action" - assert values[0]["session"] == pending - assert set(values[0]) == {"type", "event_id", "session"} - write_private("waiting", {"remote_url": pending["environment"]["remote_url"], - "environment_id": pending["environment"]["id"]}) - await_observers(done, 420, "first and resumed native Turns") - - final = snapshot("final", "idle") - turns = list(sessions.turns.list(session_id, order="asc")) - assert len(turns) == 2 and all(turn.status == "completed" for turn in turns) - turn_ids = [turn.id for turn in turns] - items = list(sessions.items.list(session_id, limit=100, order="asc")) - proof["turns"] = [turn.to_dict() for turn in turns] - proof["items"] = [item.to_dict() for item in items] - for turn, phase in zip(turns, ("first", "resumed")): - assert sessions.turns.retrieve(turn.id, session_id=session_id) == turn - group = [item.to_dict() for item in items if item.turn_id == turn.id] - commands = [item for item in group if item["type"] == "command_execution" - and "remote-stdout:" + phase in item.get("output", "") - and "remote-stderr:" + phase in item.get("output", "")] - assert len(commands) == 1 - command = commands[0] - assert command["exit_code"] == 7 and command["cwd"] == settings["workspace_directory"] - assert "remote-stdout:" + phase in command["output"] and "remote-stderr:" + phase in command["output"] - text = "\n".join(part.get("text", "") for item in group - if item["type"] == "message" and item.get("role") == "assistant" - for part in item["content"]) - assert settings["memory"] in text and settings["instruction"] in text - assert "WRONG_LOCAL_INSTRUCTIONS" not in text - assert raw.get("/agents/sessions/" + session_id + "/turns", params={"order": "asc"}).json()["data"] == proof["turns"] - assert raw.get("/agents/sessions/" + session_id + "/items", params={"order": "asc", "limit": 100}).json()["data"] == proof["items"] - - for values in observations.values(): - ids = [event["event_id"] for event in values] - assert len(ids) == len(set(ids)) - types = [event["type"] for event in values] - request = types.index("agent.session.requires_action") - connected = types.index("agent.session.environment.connected") - cleared = types.index("agent.session.idle") - first_turn = types.index("agent.session.turn.created") - assert request < connected < cleared < first_turn - assert values[request]["session"] == pending - clear = values[cleared] - assert set(clear) == {"type", "event_id", "session"} - check_session(clear["session"], "idle") - assert clear["session"]["usage"] is None - assert clear["session"]["last_active_at"] == pending["last_active_at"] - assert [value["turn"]["id"] for value in values if value["type"] == "agent.session.turn.created"] == turn_ids - assert [value["turn"]["id"] for value in values if value["type"] == "agent.session.turn.completed"] == turn_ids - check_session(values[-1]["session"], "idle") - for event in values: - if event["type"].startswith("agent.session.environment."): - assert set(event) == {"type", "event_id", "session_id", "environment"} - assert event["environment"]["id"] == expected_environment["id"] - assert event["environment"]["error"] is None - assert [event["event_id"] for event in observations["sdk"]] == [event["event_id"] for event in observations["raw"]] - with client(token) as recovered: - restored = recovered.beta.agents.sessions - assert restored.retrieve(session_id).to_dict() == final - assert list(restored.turns.list(session_id, order="asc")) == turns - assert list(restored.items.list(session_id, limit=100, order="asc")) == items - proof["client_reconnect_recovery"] = True - proof["pre_turn_action_cleared_before_turn"] = True - proof["status"] = "public_read_sse_and_remote_first_resumed_verified" - print("Pinned SDK/raw HTTP/live SSE and remote first/resumed recovery passed.", flush=True) - finally: - with lock: - proof["sdk_events"] = list(observations["sdk"]) - proof["raw_events"] = list(observations["raw"]) - write_private("public-environment-proof", proof) - - -if __name__ == "__main__": - main() diff --git a/services/core/tests/official_environment_files.py b/services/core/tests/official_environment_files.py deleted file mode 100644 index af51892f3..000000000 --- a/services/core/tests/official_environment_files.py +++ /dev/null @@ -1,236 +0,0 @@ -"""Pinned Files.list checks for a known, unchanged directory of regular files.""" - -import base64 -from pathlib import PurePosixPath -from urllib.parse import urlencode - -from openai import BadRequestError, NotFoundError - - -EMPTY_PAGE = {"object": "page", "data": [], "next": None, "has_more": False} -PROVISIONING = "the hosted environment is still provisioning; wait until it is connected before accessing files" - - -def assert_invalid_request(response, message, param=None): - """Official Environment Files validation fields (HE-16, 18, 35, 38, 39).""" - assert response.status_code == 400, "Expected a 400 validation error" - assert response.json() == {"error": {"type": "invalid_request_error", "code": "invalid_request_error", - "message": message, "param": param}}, "Wrong validation error fields" - - -def verify_wire_page(value): - """The official token page envelope (HE-32): has_more is true exactly when next is set.""" - assert isinstance(value, dict) and set(value) == {"object", "data", "next", "has_more"}, "Wrong page fields" - assert value["object"] == "page" and type(value["has_more"]) is bool, "Wrong page envelope" - assert value["has_more"] is (value["next"] is not None), "has_more disagrees with next" - - -def verify_file_page(value, environment_id, limit): - assert isinstance(value, dict) and isinstance(value.get("data"), list), "Invalid file page" - assert len(value["data"]) <= limit, "File page exceeds the requested limit" - assert value.get("has_more") is None or type(value["has_more"]) is bool, "Invalid has_more" - assert value.get("next") is None or isinstance(value["next"], str), "Invalid next token" - for item in value["data"]: - assert isinstance(item, dict), "Invalid file entry" - assert item.get("environment_id") == environment_id, "Wrong file Environment" - assert item.get("object") == "agent.environment.file", "Wrong file object" - assert isinstance(item.get("path"), str) and item["path"].startswith("/"), "Invalid file path" - assert type(item.get("size_bytes")) is int and item["size_bytes"] >= 0, "Invalid file size" - more = value.get("has_more") is not False and bool(value.get("next")) - assert value.get("has_more") is not True or more, "Missing continuation token" - assert not more or value["data"], "Empty page cannot continue through the pinned SDK" - return more - - -def verify_environment_files(client, http, environment_id, directory, expected): - assert PurePosixPath(directory).is_absolute() and expected, "Expected directory and files required" - assert all(str(PurePosixPath(path).parent) == directory for path in expected), "Use a flat fixture directory" - resource = client.beta.agents.environments.files - endpoint = str(client.base_url).rstrip("/") + "/agents/environments/" + environment_id + "/files" - headers = {"Authorization": "Bearer " + client.api_key, "OpenAI-Beta": "agents=v1"} - summary, continuation = [], None - for order, limit in (("asc", 1), ("desc", 2), (None, 2)): - params = {"path": directory, "limit": limit} - if order is not None: - params["order"] = order - wanted = sorted(expected, key=lambda path: PurePosixPath(path).parts, reverse=order != "asc") - found, tokens, page_sizes = [], set(), [] - for _ in range(len(expected) + 1): - response = http.get(endpoint, headers=headers, params=params) - assert response.status_code == 200, "Raw Files.list failed" - assert response.headers.get("content-type", "").startswith("application/json"), "Wrong file page content type" - value = response.json() - verify_wire_page(value) - more = verify_file_page(value, environment_id, limit) - found.extend(value["data"]) - page_sizes.append(len(value["data"])) - if not more: - break - token = value["next"] - assert token not in tokens, "Files.list repeated a continuation token" - tokens.add(token) - if order == "asc" and continuation is None: - continuation = token - params["page"] = token - else: - raise AssertionError("Files.list did not terminate") - assert [item["path"] for item in found] == wanted, "Raw file order, filtering or completeness differs" - assert {item["path"]: item["size_bytes"] for item in found} == expected, "Raw file sizes differ" - - params.pop("page", None) - page = resource.list(environment_id, **params) - sdk_files, sdk_tokens = [], set() - for _ in range(len(expected) + 1): - more = verify_file_page(page.to_dict(), environment_id, limit) - sdk_files.extend(item.to_dict() for item in page.data) - assert page.has_next_page() == more, "SDK continuation disagrees with the wire page" - if not more: - break - assert page.next not in sdk_tokens, "SDK repeated a continuation token" - sdk_tokens.add(page.next) - page = page.get_next_page() - else: - raise AssertionError("SDK Files.list did not terminate") - assert sdk_files == found, "SDK file pages differ from raw HTTP" - summary.append({"order": order or "default", "limit": limit, "page_sizes": page_sizes, - "files": found}) - assert continuation, "The fixture must exercise continuation" - return summary, continuation - - -def verify_file_tenant_isolation(client, other, http, environment_id, directory, page, private_paths): - endpoint = str(client.base_url).rstrip("/") + "/agents/environments/" + environment_id + "/files" - foreign = {"Authorization": "Bearer " + other.api_key, "OpenAI-Beta": "agents=v1"} - # Missing paths, repeated keys and invalid bodies never reveal a foreign Environment. - response = http.post(endpoint, headers=foreign, json={"type": "inline", "data": "", "path": directory + "/x", "extra": 1}) - assert response.status_code == 404, "Foreign tenant can reach Files.create validation" - response = http.get(endpoint + "?" + urlencode([("limit", "1"), ("limit", "2")]), headers=foreign) - assert response.status_code == 404, "Foreign tenant can reach Files.list query validation" - for params in ({"path": directory, "limit": 1, "order": "asc"}, - {"path": directory, "limit": 1, "order": "asc", "page": page}, - {"path": directory + "/missing-directory"}): - response = http.get(endpoint, params=params, headers=foreign) - assert response.status_code == 404, "Foreign tenant can access Files.list" - assert isinstance(response.json().get("error"), dict), "Missing safe error envelope" - assert all(secret not in response.text for secret in ( - client.api_key, other.api_key, environment_id, *private_paths)), "Foreign response exposes private data" - try: - other.beta.agents.environments.files.list(environment_id, **params) - except NotFoundError: - pass - else: - raise AssertionError("Foreign tenant can access SDK Files.list") - - -def verify_file_list_rows(client, http, environment_id, rows, empty_pages=True): - """Replays Files.list rows F3-F8. rows: absolute directory, missing, file and optional symlink paths. - - empty_pages=False selects the documented exception for daemons without a local - workspace binding: the Claude SDK adapter reader keeps 404 for a missing path - and 503 for a regular file or symlink. - """ - endpoint = str(client.base_url).rstrip("/") + "/agents/environments/" + environment_id + "/files" - headers = {"Authorization": "Bearer " + client.api_key, "OpenAI-Beta": "agents=v1"} - resource = client.beta.agents.environments.files - directory = rows["directory"] - baseline = http.get(endpoint, headers=headers, params={"path": directory}) - assert baseline.status_code == 200, "Baseline Files.list failed" - verify_wire_page(baseline.json()) - checked = [] - # F3: unknown keys are ignored. - response = http.get(endpoint, headers=headers, params={"path": directory, "foo": "bar"}) - assert response.status_code == 200 and response.json() == baseline.json(), "Unknown query key changed the page" - sdk = resource.list(environment_id, path=directory, extra_query={"foo": "bar"}) - assert [item.to_dict() for item in sdk.data] == baseline.json()["data"], "SDK unknown key changed the page" - checked.append("unknown_key_ignored") - # F4: a repeated supported key is rejected. - for key, value in (("path", directory), ("limit", "1"), ("order", "asc"), ("page", "token")): - response = http.get(endpoint + "?" + urlencode([(key, value), (key, value)]), headers=headers) - assert_invalid_request(response, "Failed to deserialize query string: duplicate field `" + key + "`") - checked.append("repeated_key_rejected") - # F5/F6: paths that name no listable directory list nothing; links are not followed. - for name in ("missing", "file", "symlink"): - if name not in rows: - continue - response = http.get(endpoint, headers=headers, params={"path": rows[name]}) - if not empty_pages: - expected = 404 if name == "missing" else 503 - assert response.status_code == expected and set(response.json()) == {"error"}, "Adapter reader result changed" - checked.append(name + "_adapter_" + str(expected)) - continue - assert response.status_code == 200 and response.json() == EMPTY_PAGE, "Non-directory path did not list empty" - page = resource.list(environment_id, path=rows[name]) - assert page.data == [] and page.has_more is False and page.next is None and not page.has_next_page(), "SDK empty page" - checked.append(name + "_empty_page") - # F7 and F8: path and token validation errors. - for params, message in ( - ({"path": directory + "/"}, "path must identify a non-reserved directory inside /workspace"), - ({"path": directory + "/./x"}, "path must identify a non-reserved directory inside /workspace"), - ({"path": directory.lstrip("/")}, "path must be an absolute directory inside /workspace"), - ({"path": "/etc"}, "path must be an absolute directory inside /workspace"), - ({"page": "garbage"}, "Invalid file page token for this request"), - ): - assert_invalid_request(http.get(endpoint, headers=headers, params=params), message) - try: - resource.list(environment_id, path=directory + "/") - except BadRequestError as error: - assert error.status_code == 400, "SDK trailing-slash status" - else: - raise AssertionError("SDK accepted a trailing-slash path") - checked.append("path_and_token_errors") - return checked - - -def verify_file_create_rows(client, http, environment_id, directory): - """Replays Files.create rows F1 and F8 below an existing workspace directory.""" - endpoint = str(client.base_url).rstrip("/") + "/agents/environments/" + environment_id + "/files" - headers = {"Authorization": "Bearer " + client.api_key, "OpenAI-Beta": "agents=v1"} - absolute = "environment.files[0].path must be an absolute POSIX path inside /workspace" - components = "environment.files[0].path cannot contain empty, . or .. path components" - for path, message in ( - ("relative.txt", absolute), ("/workspace", absolute), ("/tmp/outside.txt", absolute), - ("/workspace/nul\u0000.txt", absolute), (directory + "/slash/", components), - ("/workspace/../escape.txt", components), (directory + "//double.txt", components), - ): - response = http.post(endpoint, headers=headers, json={"type": "inline", "data": "cg==", "path": path}) - assert_invalid_request(response, message) - response = http.post(endpoint, headers=headers, json={"type": "inline", "data": "eA==", "path": directory + "/extra.txt", "extra_field": 1}) - assert_invalid_request(response, "Unknown parameter: 'extra_field'.", "extra_field") - path = directory + "/created-201.txt" - response = http.post(endpoint, headers=headers, json={"type": "inline", "data": "MjAx", "path": path}) - assert response.status_code == 201, "Files.create did not return 201" - assert response.json() == {"environment_id": environment_id, "object": "agent.environment.file", "path": path, "size_bytes": 3} - return path - - -def verify_file_write_rows(client, http, environment_id, directory): - """Files.create write rows FW1-FW6: parent creation, no replacement and the inline bound.""" - endpoint = str(client.base_url).rstrip("/") + "/agents/environments/" + environment_id + "/files" - headers = {"Authorization": "Bearer " + client.api_key, "OpenAI-Beta": "agents=v1"} - path = directory + "/n1/n2/nested.txt" - response = http.post(endpoint, headers=headers, json={"type": "inline", "data": "bmVzdGVk", "path": path}) - assert response.status_code == 201, "Files.create did not create missing parents" - assert response.json() == {"environment_id": environment_id, "object": "agent.environment.file", "path": path, "size_bytes": 6} - response = http.post(endpoint, headers=headers, json={"type": "inline", "data": "b3RoZXI=", "path": path}) - assert_invalid_request(response, "environment.files paths must not traverse symlinks or overwrite existing files") - response = http.post(endpoint, headers=headers, json={"type": "inline", "data": "cg==", "path": directory + "/n1"}) - assert_invalid_request(response, "file path conflicts with an existing environment file") - oversized = base64.b64encode(b"x" * ((5 << 20) + 1)).decode() - response = http.post(endpoint, headers=headers, json={"type": "inline", "data": oversized, "path": directory + "/oversized.bin"}) - assert_invalid_request(response, "environment.files[0].data exceeds the 5 MiB decoded limit") - return path - - -def verify_files_provisioning(client, http, environment_id): - """Files.list/create on a pending hosted Environment (F9). Returns None when it connected first.""" - endpoint = str(client.base_url).rstrip("/") + "/agents/environments/" + environment_id + "/files" - headers = {"Authorization": "Bearer " + client.api_key, "OpenAI-Beta": "agents=v1"} - before = client.beta.agents.environments.retrieve(environment_id).status - listed = http.get(endpoint, headers=headers) - created = http.post(endpoint, headers=headers, json={"type": "inline", "data": "cA==", "path": "/workspace/pending.txt"}) - after = client.beta.agents.environments.retrieve(environment_id).status - if before != "pending" or after != "pending": - return None - assert_invalid_request(listed, PROVISIONING) - assert_invalid_request(created, PROVISIONING) - return {"list": listed.status_code, "create": created.status_code} diff --git a/services/core/tests/official_environment_files_create.py b/services/core/tests/official_environment_files_create.py deleted file mode 100644 index d551c7e25..000000000 --- a/services/core/tests/official_environment_files_create.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Opt-in inline/source upload acceptance for a preconfigured local Environment. - -Private Environment provisioning and subsequent real-model execution belong to -the invoking native fixture. This does not prove public hosted Session admission. -""" - -import base64 -import hashlib -import importlib.metadata -import json -from pathlib import Path -import sys - -sys.dont_write_bytecode = True - -import httpx2 -from openai import OpenAI -from official_environment_files import (verify_environment_files, verify_file_create_rows, verify_file_list_rows, - verify_file_tenant_isolation, verify_file_write_rows) -from official_environment_files_native import caller_token -from official_source_files import verify_source_files - - -def main(): - settings = json.load(sys.stdin) - pin = json.loads((Path(__file__).resolve().parents[3] / "contracts/agents-api/upstream.json").read_text()) - distribution = importlib.metadata.distribution("openai") - source = json.loads(distribution.read_text("direct_url.json") or "{}") - assert distribution.version == pin["sdk_version"] and source.get("vcs_info", {}).get("commit_id") == pin["commit"], "Install the pinned SDK" - token, foreign_token = (caller_token(value) for value in settings["callers"]) - assert token != foreign_token - environment = settings["environment_id"] - base = settings["base"].rstrip("/") + "/v1" - endpoint = base + "/agents/environments/" + environment + "/files" - directory = "/workspace/uploads" - cases = { - "empty.bin": b"", - "binary.bin": bytes(range(256)), - "chunked.bin": bytes(range(256)) * 8193, - # Inline data is bounded at 5 MiB decoded; source copies keep 50 MiB. - "large.bin": b"x" * (5 << 20), - "model-input.txt": settings["model_input"].encode(), - } - source_cases = {**cases, "large.bin": b"x" * (50 << 20)} - expected, receipts = {}, [] - with httpx2.Client(trust_env=False, timeout=215) as http: - client = OpenAI(api_key=token, base_url=base, max_retries=0, _strict_response_validation=True, http_client=http) - foreign = OpenAI(api_key=foreign_token, base_url=base, max_retries=0, _strict_response_validation=True, http_client=http) - headers = {"Authorization": "Bearer " + token, "OpenAI-Beta": "agents=v1"} - for index, (name, content) in enumerate(cases.items()): - path = directory + "/" + name - body = {"type": "inline", "data": base64.b64encode(content).decode(), "path": path} - if index % 2: - response = http.post(endpoint, headers=headers, json=body) - assert response.status_code == 201, "Raw inline upload failed" - receipt = response.json() - else: - receipt = client.beta.agents.environments.files.create(environment, **body).to_dict() - assert receipt == {"environment_id": environment, "object": "agent.environment.file", "path": path, "size_bytes": len(content)}, "Wrong upload metadata" - expected[path] = len(content) - receipts.append({"path": path, "size": len(content), "sha256": hashlib.sha256(content).hexdigest()}) - source_expected, source_receipts, source_proof = verify_source_files(client, foreign, http, environment, directory, source_cases) - expected.update(source_expected) - receipts.extend(source_receipts) - pages, continuation = verify_environment_files(client, http, environment, directory, expected) - verify_file_tenant_isolation(client, foreign, http, environment, directory, continuation, list(expected)) - target = directory + "/model-input.txt" - # The fixture's staging link must list as empty without exposing staging entries. - wire_rows = verify_file_list_rows(client, http, environment, { - "directory": directory, "missing": directory + "/missing-directory", "file": target, - "symlink": "/workspace/stage-link"}) - created = verify_file_create_rows(client, http, environment, directory) - receipts.append({"path": created, "size": 3, "sha256": hashlib.sha256(b"201").hexdigest()}) - nested = verify_file_write_rows(client, http, environment, directory) - receipts.append({"path": nested, "size": 6, "sha256": hashlib.sha256(b"nested").hexdigest()}) - for body in ( - {"type": "inline", "data": "?", "path": target}, - {"type": "inline", "data": "", "path": "/workspace/../escape"}, - {"type": "inline", "data": "", "path": "/environment/staging/canary"}, - {"type": "inline", "data": "", "path": "/workspace/stage-link/canary"}, - ): - response = http.post(endpoint, headers=headers, json=body) - assert response.status_code == 400 and "error" in response.json(), "Invalid/unsupported upload accepted" - response = http.post(endpoint, headers=headers, json={"type": "file_id", "file_id": "missing", "path": target}) - assert response.status_code == 404, "Unknown source accepted" - response = http.post(endpoint, headers={**headers, "Authorization": "Bearer " + foreign_token}, - json={"type": "inline", "data": "", "path": target}) - assert response.status_code == 404, "Foreign tenant upload accepted" - assert token not in response.text and foreign_token not in response.text, "Credential leaked in error" - print(json.dumps({"sdk": pin["sdk_version"], "commit": pin["commit"], "uploads": receipts, "listing": pages, "sources": source_proof, - "wire_rows": wire_rows + ["create_201_and_field_errors", "create_parents_no_replace_inline_bound"], - "limits": ["Private Environment setup", "Other source purposes/expiration/listing unimplemented", "Tracked and untracked existing files share the untracked message", "Real-model consumption verified by invoking fixture"]})) - - -if __name__ == "__main__": - main() diff --git a/services/core/tests/official_environment_files_native.py b/services/core/tests/official_environment_files_native.py deleted file mode 100644 index dae7a1b19..000000000 --- a/services/core/tests/official_environment_files_native.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Opt-in live check; stdin supplies engine, base and two tenants with session_id and token_file/token_env. - -Optional directory_reader is "local" (default, a local workspace binding) or, for -claude_sdk only, "claude_sdk_adapter" for a daemon without that binding. -""" - -import importlib.metadata -import json -import os -from pathlib import Path, PurePosixPath -import shlex -import sys -import time -import traceback -import uuid - -sys.dont_write_bytecode = True - -import httpx2 -from openai import OpenAI -from official_environment_files import verify_environment_files, verify_file_list_rows, verify_file_tenant_isolation - - -UNVERIFIED = [ - "Recursive traversal and directory entries are unspecified by the pinned SDK; symlinked directories are not generated here.", - "The scope when path is omitted is not asserted.", - "Default limit and changed-filter cursors are not asserted.", - "The operator must qualify the real provider, native engine and isolated placement separately.", - "This fixture uses existing Sessions; it does not qualify Session creation or executor installation.", - "Generated fixture directories remain in the caller-owned workspaces for independent inspection.", -] - - -def caller_token(settings): - assert ("token_file" in settings) != ("token_env" in settings), "Choose one private token source" - if "token_file" in settings: - location = Path(settings["token_file"]).expanduser() - assert location.is_absolute() and location.stat().st_mode & 0o077 == 0, "Token file must be private" - token = location.read_text().strip() - else: - token = os.environ[settings["token_env"]].strip() - assert token, "Empty caller token" - return token - - -def generate_files(client, session_id, label): - sessions = client.beta.agents.sessions - session = sessions.retrieve(session_id) - assert session.status == "idle" and not session.required_actions, "An idle prepared Session is required" - assert session.environment.type == "self_hosted", "A self-hosted Environment is required" - environment_id = session.environment.id - workspace = PurePosixPath(session.environment.workspace_directory) - assert workspace.is_absolute(), "Absolute workspace required" - assert client.beta.agents.environments.retrieve(environment_id).status == "connected", "Connect the Environment first" - directory = str(workspace / ("files-list-" + label + "-" + uuid.uuid4().hex)) - contents = {"A.txt": "A\n", "a-b.txt": "three\n", "a.txt": "fourteen-bytes\n", "z.txt": "last\n"} - expected = {directory + "/" + name: len(content.encode()) for name, content in contents.items()} - sibling = directory + "-sibling" - sibling_expected = {sibling + "/one.txt": 3, sibling + "/two.txt": 3} - command = "mkdir -- " + shlex.quote(directory) + " " + shlex.quote(sibling) - for name, content in contents.items(): - command += " && printf %s " + shlex.quote(content) + " > " + shlex.quote(directory + "/" + name) - for name in ("one", "two"): - command += " && printf %s " + name + " > " + shlex.quote(sibling + "/" + name + ".txt") - prompt = "Use your native shell tool to execute the following command exactly once. Create no additional files in that directory. Do not delegate. Reply done only after the command succeeds.\n" + command - before = {turn.id for turn in sessions.turns.list(session_id)} - sessions.events.create(session_id, events=[{"type": "agent.session.input.message", "input": [ - {"role": "user", "content": [{"type": "input_text", "text": prompt}]}]}], idempotency_key=str(uuid.uuid4())) - deadline = time.monotonic() + 240 - while time.monotonic() < deadline: - turns = [turn for turn in sessions.turns.list(session_id) if turn.id not in before] - assert len(turns) <= 1, "One input created multiple Turns" - if turns: - assert turns[0].status not in ("failed", "cancelled"), "File generation Turn failed" - if turns[0].status == "completed" and sessions.retrieve(session_id).status == "idle": - return {"session_id": session_id, "environment_id": environment_id, "turn_id": turns[0].id, - "directory": directory, "expected": expected, - "sibling_directory": sibling, "sibling_expected": sibling_expected} - time.sleep(0.2) - raise AssertionError("File generation Turn did not complete") - - -def main(): - settings = json.load(sys.stdin) - assert settings["engine"] in ("codex", "claude_sdk"), "Select one qualified native engine" - reader = settings.get("directory_reader", "local") - assert reader == "local" or (reader == "claude_sdk_adapter" and settings["engine"] == "claude_sdk"), "Unsupported directory reader" - assert len(settings["tenants"]) == 2, "Two independent tenant Sessions are required" - pin = json.loads((Path(__file__).resolve().parents[3] / "contracts/agents-api/upstream.json").read_text()) - distribution = importlib.metadata.distribution("openai") - source = json.loads(distribution.read_text("direct_url.json") or "{}") - assert distribution.version == pin["sdk_version"] and source.get("vcs_info", {}).get("commit_id") == pin["commit"], "Install the pinned SDK" - tokens = [caller_token(tenant) for tenant in settings["tenants"]] - assert tokens[0] != tokens[1], "Distinct tenant credentials required" - base = settings["base"].rstrip("/") + "/v1" - with httpx2.Client(trust_env=False, timeout=30) as http: - clients = [OpenAI(api_key=token, base_url=base, max_retries=0, _strict_response_validation=True, - http_client=http) for token in tokens] - generated = [generate_files(client, tenant["session_id"], str(index)) - for index, (client, tenant) in enumerate(zip(clients, settings["tenants"]))] - assert generated[0]["environment_id"] != generated[1]["environment_id"], "Distinct Environments required" - for index, (client, fixture) in enumerate(zip(clients, generated)): - before = [turn.to_dict() for turn in client.beta.agents.sessions.turns.list(fixture["session_id"])] - fixture["list_checks"], page = verify_environment_files( - client, http, fixture["environment_id"], fixture["directory"], fixture["expected"]) - fixture["sibling_checks"], _ = verify_environment_files( - client, http, fixture["environment_id"], fixture["sibling_directory"], fixture["sibling_expected"]) - fixture["wire_rows"] = verify_file_list_rows(client, http, fixture["environment_id"], { - "directory": fixture["directory"], "missing": fixture["directory"] + "-missing", - "file": next(iter(fixture["expected"]))}, empty_pages=reader == "local") - verify_file_tenant_isolation(client, clients[1 - index], http, fixture["environment_id"], - fixture["directory"], page, fixture["expected"] | fixture["sibling_expected"]) - assert [turn.to_dict() for turn in client.beta.agents.sessions.turns.list(fixture["session_id"])] == before, "Files.list changed Turns" - fixture["cross_tenant_denied"] = True - proof = {"engine": settings["engine"], "directory_reader": reader, "sdk_version": distribution.version, "sdk_commit": pin["commit"], - "scope": "Public input-generated flat files, SDK/raw listing, sorting, pagination and two-tenant isolation", - "fixtures": generated, "unverified": UNVERIFIED} - serialized = json.dumps(proof, indent=2) - assert all(token not in serialized for token in tokens), "Credential in acceptance evidence" - print(serialized) - - -if __name__ == "__main__": - try: - main() - except Exception as error: - locations = " -> ".join(f"{Path(frame.filename).name}:{frame.lineno}" for frame in traceback.extract_tb(error.__traceback__)) - raise SystemExit(f"Files.list acceptance failed ({type(error).__name__} at {locations}); response bodies withheld.") from None diff --git a/services/core/tests/official_oauth_credentials.py b/services/core/tests/official_oauth_credentials.py deleted file mode 100644 index 56dd72682..000000000 --- a/services/core/tests/official_oauth_credentials.py +++ /dev/null @@ -1,215 +0,0 @@ -"""Pinned SDK/raw HTTP OAuth resource verification, with no provider requests. - -The caller supplies an isolated Core/PostgreSQL service, strict official clients -for an owner, foreign tenant and same-tenant peer, and an HTTP transport. This -verifier creates and deletes only its own Vaults. External authorization, refresh -execution and provider revocation need separate real-provider acceptance. -""" - -from copy import deepcopy -import importlib.metadata -import json -from pathlib import Path -import uuid - - -def verify_oauth_credentials(client, other, peer, raw, *, evidence_path=None): - pin = json.loads((Path(__file__).resolve().parents[3] / "contracts/agents-api/upstream.json").read_text()) - distribution = importlib.metadata.distribution("openai") - source = json.loads(distribution.read_text("direct_url.json") or "{}") - assert distribution.version == pin["sdk_version"] and source["vcs_info"]["commit_id"] == pin["commit"] - assert all(api._strict_response_validation for api in (client, other, peer)) - credentials = client.beta.agents.vaults.credentials - base = str(client.base_url).rstrip("/") - prefix = "oauth-resource-" + uuid.uuid4().hex - secrets = [prefix + suffix for suffix in ("-access", "-refresh", "-client", "-replacement")] - proof = {"sdk": pin, "variants": [], "checks": [], "passed": False} - owned = [] - expiry = "2030-01-02T03:04:05.123Z" - destination = "https://mcp.example.invalid/tools" - - def headers(api=client): - return {"Authorization": "Bearer " + api.api_key, "OpenAI-Beta": "agents=v1"} - - def safe(response, status=200): - assert response.status_code == status, "Unexpected OAuth resource HTTP status" - assert not any(secret in response.text for secret in secrets), "OAuth secret appeared in a public response" - assert response.headers["cache-control"] == "no-store" - return response.json() - - def endpoint(vault, credential=None): - return base + "/vaults/" + vault + "/credentials" + ("/" + credential if credential else "") - - def check_resource(body, vault, expected_auth): - assert set(body) == {"id", "vault_id", "name", "object", "auth", "created_at", "updated_at"} - assert body["object"] == "vault.credential" and body["vault_id"] == vault - assert uuid.UUID(body["id"]).int != 0 - assert type(body["created_at"]) is int and type(body["updated_at"]) is int - assert body["auth"] == expected_auth, "OAuth safe auth metadata or nullable fields changed" - return body - - def retrieve(vault, credential, expected_auth): - response = credentials.with_raw_response.retrieve(credential, vault_id=vault) - body = check_resource(safe(response.http_response), vault, expected_auth) - assert response.parse().to_dict() == body - assert safe(raw.get(endpoint(vault, credential), headers=headers())) == body - assert peer.beta.agents.vaults.credentials.retrieve(credential, vault_id=vault).to_dict() == body - return body - - def update(vault, previous, patch, expected_auth, sdk=True): - if sdk: - response = credentials.with_raw_response.update(previous["id"], vault_id=vault, auth={"type": "mcp_oauth", **patch}) - body = safe(response.http_response) - assert response.parse().to_dict() == body - else: - body = safe(raw.post(endpoint(vault, previous["id"]), headers=headers(), json={"auth": {"type": "mcp_oauth", **patch}})) - check_resource(body, vault, expected_auth) - assert {k: v for k, v in body.items() if k not in ("auth", "updated_at")} == { - k: v for k, v in previous.items() if k not in ("auth", "updated_at")} - assert body["updated_at"] >= previous["updated_at"] - assert retrieve(vault, body["id"], expected_auth) == body - return body - - try: - vault = client.beta.agents.vaults.create(name=prefix) - owned.append((client, vault.id)) - wrong = client.beta.agents.vaults.create(name=prefix + "-wrong") - owned.append((client, wrong.id)) - foreign = other.beta.agents.vaults.create(name=prefix + "-foreign") - owned.append((other, foreign.id)) - expected = {} - for index, method in enumerate((None, "none", "client_secret_basic", "client_secret_post")): - auth = {"type": "mcp_oauth", "mcp_server_url": destination, "access_token": secrets[0]} - expected_auth = {"type": "mcp_oauth", "mcp_server_url": destination, "expires_at": None, "refresh": None} - if method is not None: - auth.update(expires_at=expiry, refresh={ - "client_id": "public-test-client", "refresh_token": secrets[1], - "token_endpoint": "https://issuer.example.invalid/token", "token_endpoint_auth": {"type": method}}) - if method != "none": - auth["refresh"]["token_endpoint_auth"]["client_secret"] = secrets[2] - auth["refresh"].update(resource=destination, scope="read write") - expected_auth.update(expires_at=expiry, refresh={ - "client_id": "public-test-client", "token_endpoint": "https://issuer.example.invalid/token", - "token_endpoint_auth": {"type": method}, "resource": auth["refresh"].get("resource"), - "scope": auth["refresh"].get("scope")}) - response = credentials.with_raw_response.create(vault.id, name=" OAuth " + str(index) + " ", auth=auth) - body = check_resource(safe(response.http_response, 201), vault.id, expected_auth) - assert response.parse().to_dict() == body and body["name"] == "OAuth " + str(index) - current = retrieve(vault.id, body["id"], expected_auth) - changed = deepcopy(expected_auth) - changed["expires_at"] = None - current = update(vault.id, current, {"access_token": secrets[3]}, changed) - expected_auth = deepcopy(expected_auth) - expected_auth["expires_at"] = expiry - current = update(vault.id, current, {"expires_at": expiry}, expected_auth, sdk=False) - current = update(vault.id, current, {"expires_at": None}, changed) - current = update(vault.id, current, {"access_token": secrets[0], "expires_at": expiry}, expected_auth) - if method is not None: - current = update(vault.id, current, {"refresh": {"refresh_token": secrets[3]}}, expected_auth, sdk=False) - changed = deepcopy(expected_auth) - changed["refresh"]["scope"] = None - current = update(vault.id, current, {"refresh": {"scope": None}}, changed) - expected_auth = changed - changed = deepcopy(expected_auth) - changed["refresh"]["scope"] = "" - current = update(vault.id, current, {"refresh": {"scope": ""}}, changed, sdk=False) - expected_auth = changed - if method != "none": - for secret_patch in ({"client_secret": secrets[3]},): - current = update(vault.id, current, {"refresh": {"token_endpoint_auth": {"type": method, **secret_patch}}}, expected_auth) - invalid_patches = [ - {"refresh": {"token_endpoint_auth": {"type": "client_secret_post" if method != "client_secret_post" else "client_secret_basic"}}}, - {"refresh": {"client_id": "changed"}}, {"refresh": {"token_endpoint": "https://other.example/token"}}, - {"refresh": {"resource": None}}, {"refresh": {"token_endpoint_auth": {"type": "none"}}}] - else: - invalid_patches = [{"refresh": {}}, {"refresh": {"scope": "added"}}] - invalid_patches += [{}, {"access_token": ""}, {"access_token": None, "refresh": None}, - {"refresh": {}}, {"refresh": {"refresh_token": None, "token_endpoint_auth": None}}, - {"refresh": {"token_endpoint_auth": {"type": "client_secret_basic", "client_secret": None}}}, - {"access_token": 3}, {"expires_at": "tomorrow"}, {"token": secrets[0]}, - {"refresh": {"scope": 3}}, {"refresh": {"refresh_token": 3}}] - for patch in invalid_patches: - safe(raw.post(endpoint(vault.id, current["id"]), headers=headers(), json={"auth": {"type": "mcp_oauth", **patch}}), 400) - assert retrieve(vault.id, current["id"], expected_auth) == current - expected[current["id"]] = current - proof["variants"].append({"method": method, "credential": current}) - - # Explicit null creation must have the same public nullable fields as omission. - null_auth = {"type": "mcp_oauth", "mcp_server_url": destination, "expires_at": None, "refresh": None} - null_body = safe(raw.post(endpoint(vault.id), headers=headers(), json={ - "name": "Raw nulls", "auth": {**null_auth, "access_token": secrets[0]}}), 201) - check_resource(null_body, vault.id, null_auth) - assert retrieve(vault.id, null_body["id"], null_auth) == null_body - expected[null_body["id"]] = null_body - static = credentials.create(vault.id, name="Static regression", auth={"type": "static_bearer", "mcp_server_url": destination, "token": secrets[0]}) - expected[static.id] = static.to_dict() - assert expected[static.id]["auth"] == {"type": "static_bearer", "mcp_server_url": destination} - safe(raw.post(endpoint(vault.id, static.id), headers=headers(), json={"auth": {"type": "mcp_oauth", "access_token": secrets[3]}}), 400) - - invalid_create = [ - {"type": "mcp_oauth", "mcp_server_url": destination}, - {"type": "mcp_oauth", "mcp_server_url": destination, "access_token": None}, - {"type": "mcp_oauth", "mcp_server_url": destination, "access_token": ""}, - {"type": "mcp_oauth", "mcp_server_url": "http://issuer.example", "access_token": secrets[0]}, - {"type": "mcp_oauth", "mcp_server_url": destination, "access_token": secrets[0], "refresh": {}}, - {"type": "mcp_oauth", "mcp_server_url": destination, "access_token": secrets[0], "expires_at": 3}, - ] - for auth in invalid_create: - safe(raw.post(endpoint(vault.id), headers=headers(), json={"name": "Invalid", "auth": auth}), 400) - full = {"type": "mcp_oauth", "mcp_server_url": destination, "access_token": secrets[0], "refresh": { - "client_id": "client", "refresh_token": secrets[1], "token_endpoint": "https://issuer.example/token", - "token_endpoint_auth": {"type": "none"}}} - for invalid in (None, {}, {"type": "client_secret_basic"}, {"type": "none", "client_secret": None}, - {"type": "client_secret_post", "client_secret": 3}): - auth = deepcopy(full) - auth["refresh"]["token_endpoint_auth"] = invalid - safe(raw.post(endpoint(vault.id), headers=headers(), json={"name": "Invalid auth", "auth": auth}), 400) - - response = credentials.with_raw_response.list(vault.id, order="asc", limit=100) - listed = safe(response.http_response) - assert response.parse().to_dict() == listed - assert {row["id"]: row for row in listed["data"]} == expected and not listed["has_more"] - assert safe(raw.get(endpoint(vault.id), headers=headers(), params={"order": "asc", "limit": 100})) == listed - selected = null_body["id"] - for target, api in ((endpoint(wrong.id, selected), client), (endpoint(foreign.id, selected), client), - (endpoint(vault.id, selected), other), (endpoint(vault.id, str(uuid.uuid4())), client)): - for method in ("GET", "POST", "DELETE"): - kwargs = {"json": {"auth": {"type": "mcp_oauth", "access_token": secrets[3]}}} if method == "POST" else {} - safe(raw.request(method, target, headers=headers(api), **kwargs), 404) - safe(raw.get(endpoint(vault.id), headers=headers(other)), 404) - safe(raw.post(endpoint(vault.id), headers=headers(other), json={"name": "Foreign", "auth": full}), 404) - safe(raw.get(endpoint(vault.id, selected), headers={"Authorization": "Bearer " + client.api_key}), 400) - safe(raw.get(endpoint(vault.id, selected), headers={"OpenAI-Beta": "agents=v1"}), 401) - assert retrieve(vault.id, selected, null_auth) == null_body - proof["checks"].extend(["strict_sdk_raw_metadata", "all_endpoint_auth_variants", "omitted_null_patches", - "immutable_method_and_configuration", "tenant_and_vault_scope", "static_regression"]) - for index, (credential, previous) in enumerate(expected.items()): - if index % 2: - deleted = safe(raw.delete(endpoint(vault.id, credential), headers=headers())) - else: - response = credentials.with_raw_response.delete(credential, vault_id=vault.id) - deleted = safe(response.http_response) - assert response.parse().to_dict() == deleted - assert deleted == {"id": credential, "deleted": True, "object": "vault.credential.deleted"} - safe(raw.get(endpoint(vault.id, credential), headers=headers()), 404) - safe(raw.delete(endpoint(vault.id, credential), headers=headers()), 404) - safe(raw.post(endpoint(vault.id, credential), headers=headers(), json={"auth": {"type": "mcp_oauth", "access_token": secrets[3]}}), 404) - assert list(credentials.list(vault.id)) == [] - proof["checks"].append("sdk_raw_deletion_and_no_resurrection") - proof["passed"] = True - return proof - finally: - cleanup_errors = [] - for api, vault_id in reversed(owned): - try: - api.beta.agents.vaults.delete(vault_id) - except Exception as error: - cleanup_errors.append(type(error).__name__) - proof["cleanup_errors"] = cleanup_errors - if cleanup_errors: - proof["passed"] = False - if evidence_path is not None: - serialized = json.dumps(proof, indent=2) - assert not any(secret in serialized for secret in secrets) - Path(evidence_path).write_text(serialized + "\n") - assert not cleanup_errors, "OAuth verifier could not remove owned Vaults" diff --git a/services/core/tests/official_pending_actions_native.py b/services/core/tests/official_pending_actions_native.py deleted file mode 100644 index 017801329..000000000 --- a/services/core/tests/official_pending_actions_native.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Real hosted function-action recovery through the pinned SDK and public HTTP.""" - -import importlib.metadata -import json -from pathlib import Path -import uuid - -from session_cleanup import delete_session - - -def verify_pending_actions(client, foreign, http, model, evidence): - """The private runner supplies an isolated Core/native provider deployment.""" - pin = json.loads((Path(__file__).resolve().parents[3] / "contracts/agents-api/upstream.json").read_text()) - distribution = importlib.metadata.distribution("openai") - source = json.loads(distribution.read_text("direct_url.json") or "{}") - assert distribution.version == pin["sdk_version"] and source["vcs_info"]["commit_id"] == pin["commit"] - sessions = client.beta.agents.sessions - endpoint = str(client.base_url).rstrip("/") + "/agents/sessions" - headers = {"Authorization": "Bearer " + client.api_key, "OpenAI-Beta": "agents=v1"} - foreign_headers = {**headers, "Authorization": "Bearer " + foreign.api_key} - proof = {"sdk": pin, "checks": [], "rounds": [], "passed": False} - created = [] - marker = "pending-value-" + uuid.uuid4().hex - agent = {"model": model, "instructions": "Call lookup exactly once when asked. Never retry a failed tool call. Use no other tools.", "tools": [{ - "type": "function", "name": "lookup", "description": "Retrieve the requested test value.", - "parameters": {"type": "object", "properties": {"key": {"type": "string"}}, "required": ["key"], "additionalProperties": False}, - }]} - - def read(path, sdk): - response = http.get(endpoint + path, headers=headers) - assert response.status_code == 200, (path, response.status_code) - raw = response.json() - assert raw == sdk.to_dict(), path - return raw - - def items(sid): - response = http.get(endpoint + "/" + sid + "/items", headers=headers, params={"limit": 100, "order": "asc"}) - assert response.status_code == 200 - raw = response.json()["data"] - assert raw == [item.to_dict() for item in sessions.items.list(sid, limit=100, order="asc").data] - return raw - - # Expected (type, code) of each public rejection; every error param is null. - missing = ("not_found_error", "not_found_error") - bad_target = ("invalid_request_error", "invalid_request_error") - conflict = ("conflict_error", "conflict_error") - key_reuse = ("conflict_error", "idempotency_conflict") - - def submit(sid, event, key, expected=202, auth=None, error=None): - response = http.post(endpoint + "/" + sid + "/events", headers={**(auth or headers), "Idempotency-Key": key}, json={"events": [event]}) - assert response.status_code == expected, (key, response.status_code, response.text) - if expected == 202: - assert response.content == b"" - else: - body = response.json()["error"] - assert (body["type"], body["code"], body["param"]) == (*error, None), (key, body) - return {"request": key, "status": response.status_code} - - try: - session = sessions.create(agent=agent, environment={"type": "openai_hosted"}) - created.append(session.id) - other = sessions.create(agent=agent, environment={"type": "openai_hosted"}) - created.append(other.id) - proof.update(session=session.id, other_session=other.id) - for index, mode in enumerate(("success", "error", "cancel")): - current = {"mode": mode, "before_disconnect": [], "after_reconnect": [], "refusals": []} - proof["rounds"].append(current) - prompt = "Call lookup exactly once with key " + mode + ". Then reply with its returned text. If it fails, acknowledge the error without retrying." - with sessions.events.stream(session.id, timeout=240) as stream: - sessions.events.create(session.id, events=[{"type": "agent.session.input.message", "input": [{"role": "user", "content": [{"type": "input_text", "text": prompt}]}]}], idempotency_key="pending-message-" + mode) - for event in stream: - current["before_disconnect"].append(event.to_dict()) - assert event.type not in ("agent.session.turn.failed", "agent.session.failed"), event.to_dict() - if event.type == "agent.session.requires_action": - break - else: - raise AssertionError("Stream ended before a pending action") - - # The closed stream is not the recovery authority. Query both resources. - recovered = read("/" + session.id, sessions.retrieve(session.id)) - actions = recovered["required_actions"] - assert recovered["status"] == "requires_action" and len(actions) == 1 - action = actions[0] - assert action == {"type": "function_call", "name": "lookup", "arguments": {"key": mode}, "call_id": action["call_id"], "turn_id": action["turn_id"]} - turn_path = "/" + session.id + "/turns/" + action["turn_id"] - turn = read(turn_path, sessions.turns.retrieve(action["turn_id"], session_id=session.id)) - assert turn["status"] == "waiting" - assert sessions.turns.list(session.id).data[0].id == action["turn_id"] - current.update(recovered_session=recovered, recovered_turn=turn, items_while_pending=items(session.id)) - event = {"type": "agent.session.input.tool_result", "turn_id": action["turn_id"], "call_id": action["call_id"], "success": mode != "error", "output": [{"type": "input_text", "text": marker}, {"type": "input_text", "text": "original second part"}]} - if mode == "error": - event["error"] = "original caller failure " + marker - - def pending_unchanged(): - state = sessions.retrieve(session.id) - assert state.status == "requires_action" and [a.to_dict() for a in state.required_actions] == actions - assert sessions.turns.retrieve(action["turn_id"], session_id=session.id).status == "waiting" - assert not any(item["type"] == "function_call_output" and item["call_id"] == action["call_id"] for item in items(session.id)) - assert sessions.retrieve(other.id).status == "idle" - assert sessions.turns.list(other.id).data == [] and items(other.id) == [] - - # A foreign Session is not found; inside an owned Session an unknown - # call or a call of another Turn is a request error. - for label, target, value, auth, status, error in ( - ("wrong-tenant", session.id, event, foreign_headers, 404, missing), - ("wrong-session", other.id, event, headers, 400, bad_target), - ("wrong-turn", session.id, {**event, "turn_id": proof["rounds"][index - 1]["recovered_turn"]["id"] if index else str(uuid.uuid4())}, headers, 400, bad_target), - ("malformed-turn", session.id, {**event, "turn_id": "turn_" + uuid.uuid4().hex}, headers, 400, bad_target), - ("wrong-call", session.id, {**event, "call_id": "absent-" + uuid.uuid4().hex}, headers, 400, bad_target), - ): - current["refusals"].append(submit(target, value, label + "-" + mode, status, auth, error)) - pending_unchanged() - for path in ("/" + session.id, turn_path, "/" + session.id + "/items"): - assert http.get(endpoint + path, headers=foreign_headers).status_code == 404 - proof["checks"].append(mode + "_disconnected_query_and_target_isolation") - - # Opening another live stream does not submit or replay any input. - with sessions.events.stream(session.id, timeout=240) as stream: - pending_unchanged() - submitted = {"type": "agent.session.input.cancel"} if mode == "cancel" else event - key = "pending-submit-" + mode - sessions.events.create(session.id, events=[submitted], idempotency_key=key) - current["retry"] = submit(session.id, submitted, key) - if mode != "cancel": - current["conflict"] = submit(session.id, {**event, "output": "changed"}, key, 409, error=key_reuse) - current["new_identity_conflict"] = submit(session.id, {**event, "output": "changed"}, key + "-changed", 409, error=conflict) - for received in stream: - current["after_reconnect"].append(received.to_dict()) - if received.type == "agent.session.idle": - break - assert received.type != "agent.session.failed", received.to_dict() - else: - raise AssertionError("Reconnected stream ended before settlement") - - terminal = sessions.turns.retrieve(action["turn_id"], session_id=session.id) - assert terminal.status == ("cancelled" if mode == "cancel" else "completed"), terminal.to_dict() - settled = read("/" + session.id, sessions.retrieve(session.id)) - assert settled["status"] == "idle" and settled["required_actions"] == [] - current["terminal"] = read(turn_path, terminal) - before_ids = {e["event_id"] for e in current["before_disconnect"]} - after_ids = {e["event_id"] for e in current["after_reconnect"]} - assert before_ids.isdisjoint(after_ids), "Reconnected live stream replayed previous events" - terminal_events = [e for e in current["after_reconnect"] if e["type"] in ("agent.session.turn.completed", "agent.session.turn.failed", "agent.session.turn.cancelled")] - assert len(terminal_events) == 1 and terminal_events[0]["turn"]["id"] == action["turn_id"] - assert terminal_events[0]["type"] == "agent.session.turn." + terminal.status - current["terminal_retry"] = submit(session.id, submitted, key) - if mode == "cancel": - current["late_result"] = submit(session.id, event, key + "-late-result", 409, error=conflict) - else: - current["same_result_new_identity"] = submit(session.id, event, key + "-same-result") - - durable = items(session.id) - outputs = [i for i in durable if i["type"] == "function_call_output" and i["call_id"] == action["call_id"]] - assert len(outputs) == (0 if mode == "cancel" else 1) - if outputs: - assert {k: outputs[0][k] for k in ("output", "error") if k in outputs[0]} == {k: event[k] for k in ("output", "error") if k in event} - if mode == "success": - answers = [i for i in durable if i["type"] == "message" and i["role"] == "assistant"] - assert marker in "\n".join(p.get("text", "") for p in answers[-1]["content"]) - assert len(sessions.turns.list(session.id).data) == index + 1 - current["durable_items"] = durable - proof["checks"].append(mode + "_reconnect_settlement_retry_and_original_items") - - # Reads and retries leave the three native calls and two original results intact. - durable = items(session.id) - calls = [i for i in durable if i["type"] == "function_call"] - assert len(calls) == 3 and len({i["call_id"] for i in calls}) == 3 - assert len([i for i in durable if i["type"] == "function_call_output"]) == 2 - assert len(sessions.turns.list(session.id).data) == 3 - assert sessions.retrieve(session.id).required_actions == [] - proof["checks"].append("durable_queries_no_implicit_call_replay") - proof["passed"] = True - return proof["checks"] - finally: - Path(evidence).write_text(json.dumps(proof, indent=2)) - for sid in reversed(created): - delete_session(sessions, sid) diff --git a/services/core/tests/official_user_runtime.py b/services/core/tests/official_user_runtime.py deleted file mode 100644 index 9101634b0..000000000 --- a/services/core/tests/official_user_runtime.py +++ /dev/null @@ -1,368 +0,0 @@ -"""Public acceptance for an already enrolled, caller-owned Runtime. - -run_acceptance must run on the main thread of the Linux operator process. -The supplied Session is unused and its /workspace/outputs directory is empty. -The operator creates nonempty .user-runtime-isolation-canary files under -/home/runtime/.oac/daemon and /environment/staging before calling. -The actual executor-key.json must remain under that protected daemon root. -runtime.read(path) returns bytes within a bounded timeout, raising FileNotFoundError only for absence. -runtime.restart() preserves workspace and native state and waits for reconnect. -runtime.restart_core() preserves database and API URL and waits for reconnect. -The caller owns service/model configuration, Runtime cleanup and actual execution. -""" - -import base64 -from concurrent.futures import ThreadPoolExecutor -from contextlib import contextmanager -import hashlib -import importlib.metadata -import json -from pathlib import Path -import signal -import threading -import time -import uuid - -from openai import NotFoundError -from official_environment_files import verify_environment_files, verify_file_tenant_isolation -from official_session_artifacts import verify_session_artifacts - - -@contextmanager -def _prompt_deadline(): - # SSE keepalives do not extend the operator's wall-clock deadline. - def expired(signum, frame): - raise TimeoutError("Native Turn exceeded 240 seconds") - - previous = signal.signal(signal.SIGALRM, expired) - signal.setitimer(signal.ITIMER_REAL, 240) - try: - yield - finally: - signal.setitimer(signal.ITIMER_REAL, 0) - signal.signal(signal.SIGALRM, previous) - - -def run_acceptance(client, foreign, http, session, runtime, evidence_path, secret_markers=(), - expected_environment_type="self_hosted", - protected_credential_path="/home/runtime/.oac/daemon/executor-key.json", turn_observer=None): - """Return a safe report after real execution; raise on any failed assertion.""" - assert threading.current_thread() is threading.main_thread(), "Run acceptance on the main thread" - pin = json.loads((Path(__file__).resolve().parents[3] / "contracts/agents-api/upstream.json").read_text()) - distribution = importlib.metadata.distribution("openai") - source = json.loads(distribution.read_text("direct_url.json") or "{}") - assert distribution.version == pin["sdk_version"] and source.get("vcs_info", {}).get("commit_id") == pin["commit"], "Install the pinned SDK" - secrets = tuple(value.decode() if isinstance(value, bytes) else value - for value in (client.api_key, foreign.api_key, *secret_markers) if value) - assert all(isinstance(value, str) for value in secrets), "Secret markers must be strings or UTF-8 bytes" - evidence = Path(evidence_path) - evidence.mkdir(mode=0o700, parents=True, exist_ok=True) - sessions = client.beta.agents.sessions - sid, eid = session.id, session.environment.id - base = str(client.base_url).rstrip("/") - endpoint = base + "/agents/sessions/" + sid - headers = {"Authorization": "Bearer " + client.api_key, "OpenAI-Beta": "agents=v1"} - other_headers = {**headers, "Authorization": "Bearer " + foreign.api_key} - report = {"passed": False, "session_id": sid, "environment_id": eid, - "sdk_version": pin["sdk_version"], "sdk_commit": pin["commit"], "checks": [], "turns": []} - began = time.monotonic() - nonce = uuid.uuid4().hex - prefix = "/workspace/user-runtime-" + nonce - starts, ticks = prefix + "-starts", prefix + "-ticks" - memory = "remember-" + uuid.uuid4().hex - outputs = {"/workspace/outputs/a.bin": bytes(range(256)), - "/workspace/outputs/b.txt": ("native-user-runtime-" + nonce + "\n").encode()} - artifacts = {} - private_paths = [protected_credential_path, - "/home/runtime/.oac/daemon/.user-runtime-isolation-canary", - "/environment/staging/.user-runtime-isolation-canary"] - - def private_hashes(): - values = {} - for path in private_paths: - content = runtime.read(path) - assert content, "Operator private-file witness is missing or empty" - values[path] = hashlib.sha256(content).hexdigest() - return values - - def redact(value): - if isinstance(value, str): - for secret in secrets: - value = value.replace(secret, "[REDACTED]") - return value - if isinstance(value, dict): - return {redact(key): redact(item) for key, item in value.items()} - if isinstance(value, list): - return [redact(item) for item in value] - return value - - def save(name, value): - path = evidence / name - with path.open("w") as stream: - path.chmod(0o600) - json.dump(redact(value), stream, indent=2) - - def safe(value): - assert redact(value) == value, "Public response exposed a private credential" - return value - - def until(check, timeout=30): - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - value = check() - if value: - return value - time.sleep(0.2) - raise AssertionError("Runtime observation timed out") - - def read_optional(path): - try: - return runtime.read(path) - except FileNotFoundError: - return b"" - - def upload(path, data): - content = data.encode() if isinstance(data, str) else data - receipt = client.beta.agents.environments.files.create( - eid, type="inline", path=path, data=base64.b64encode(content).decode()).to_dict() - assert safe(receipt) == {"environment_id": eid, "object": "agent.environment.file", - "path": path, "size_bytes": len(content)}, "Upload metadata changed" - assert runtime.read(path) == content, "Public upload bytes differ in Runtime" - - def snapshot_items(name): - items = [item.to_dict() for item in sessions.items.list(sid, order="asc", limit=100)] - raw, after = [], None - while True: - params = {"order": "asc", "limit": 2} - if after: - params["after"] = after - response = http.get(endpoint + "/items", headers=headers, params=params) - assert response.status_code == 200, "Raw Items.list failed" - page = safe(response.json()) - raw.extend(page["data"]) - if not page["has_more"]: - break - assert page["data"] and page["data"][-1]["id"] != after, "Items cursor did not advance" - after = page["data"][-1]["id"] - assert len(raw) <= len(items), "Raw Items.list exceeded SDK result" - assert safe(items) == raw, "SDK and raw Items differ" - save(name + "-items.json", items) - return items - - def run_turn(text, number, cancellation=False): - observed, stop, requested = [], threading.Event(), threading.Event() - submitted_at, first_text_at, terminal_at = None, None, None - cancel_key = nonce + "-cancel" - - def cancel_running(): - deadline = time.monotonic() + 120 - while not stop.wait(0.2): - assert time.monotonic() < deadline, "Native command never began ticking" - current = read_optional(ticks) - if len(current.splitlines()) >= 3: - assert runtime.read(starts) == b"started\n", "Side-effect command was repeated" - requested.set() - client.with_options(timeout=10).beta.agents.sessions.events.create( - sid, events=[{"type": "agent.session.input.cancel"}], idempotency_key=cancel_key) - return len(current.splitlines()) - raise AssertionError("Cancellation ended before observing native effects") - - def consume(stream, journal, cancelling=None): - nonlocal first_text_at, terminal_at - for event in stream: - value = event.to_dict() - journal.write(json.dumps(redact(value)) + "\n") - journal.flush() - safe(value) - observed.append(value) - kind = value["type"] - if kind == "agent.session.turn.output_text.delta" and first_text_at is None and value.get("delta"): - first_text_at = time.monotonic() - if kind in ("agent.session.turn.completed", "agent.session.turn.cancelled", "agent.session.turn.failed"): - terminal_at = time.monotonic() - assert kind not in ("error", "agent.session.failed", "agent.session.turn.failed"), "Native execution failed" - if cancelling is not None and cancelling.done(): - cancelling.result() - if kind == "agent.session.turn.cancelled": - assert cancellation and requested.is_set(), "Unexpected cancelled Turn" - cancelling.result(timeout=10) - break - assert kind != "agent.session.turn.completed" or not cancellation, "Long command completed before cancellation" - if kind == "agent.session.idle" and any(item["type"] == "agent.session.turn.completed" for item in observed): - break - terminal = "agent.session.turn.cancelled" if cancellation else "agent.session.turn.completed" - kinds = [item["type"] for item in observed] - assert kinds.count("agent.session.turn.created") == 1 and kinds.count(terminal) == 1, "Missing or duplicate Turn events" - assert kinds.index("agent.session.turn.created") < kinds.index(terminal), "Turn event order changed" - if not cancellation: - assert kinds[-1] == "agent.session.idle", "Completed Turn did not become idle" - - path = evidence / ("turn-" + str(number) + "-events.jsonl") - try: - with _prompt_deadline(), path.open("w") as journal: - path.chmod(0o600) - with sessions.events.stream(sid, timeout=240) as stream: - submitted_at = time.monotonic() - sessions.events.create(sid, events=[{"type": "agent.session.input.message", "input": [ - {"role": "user", "content": [{"type": "input_text", "text": text}]}]}], idempotency_key=nonce + "-turn-" + str(number)) - if cancellation: - with ThreadPoolExecutor(max_workers=1) as pool: - cancelling = pool.submit(cancel_running) - try: - consume(stream, journal, cancelling) - finally: - stop.set() - report["ticks_before_cancel"] = cancelling.result() - else: - consume(stream, journal) - turns = list(sessions.turns.list(sid, order="asc", limit=100)) - assert len(turns) == number, "Input created an unexpected number of Turns" - turn = turns[-1] - assert turn.status == ("cancelled" if cancellation else "completed"), "Persisted Turn differs from SSE" - until(lambda: sessions.retrieve(sid).status == "idle") - items = snapshot_items("turn-" + str(number)) - current = [item for item in items if item.get("turn_id") == turn.id] - assert current, "Native Turn has no public Items" - if not cancellation: - assert all(item.get("status") not in ("failed", "incomplete", "in_progress") for item in current), "Completed Turn contains failed native work" - report["turns"].append({"id": turn.id, "status": turn.status, "event_count": len(observed), - "input_to_first_text_ms": round((first_text_at - submitted_at) * 1000, 3) if first_text_at is not None else None, - "input_to_terminal_ms": round((terminal_at - submitted_at) * 1000, 3) if terminal_at is not None else None}) - if turn_observer is not None: - report["turns"][-1]["runtime_observation"] = safe(turn_observer(number, turn.id)) - return turn, current - except BaseException: - # No model request is retried; an already started tool is cancelled for cleanup. - try: - client.with_options(timeout=10).beta.agents.sessions.events.create( - sid, events=[{"type": "agent.session.input.cancel"}], idempotency_key=nonce + "-failure-cancel") - except Exception: - report["failure_cleanup_cancel_failed"] = True - raise - - def answer(items): - return "\n".join(part["text"] for item in items if item.get("type") == "message" and item.get("role") == "assistant" - for part in item.get("content", []) if part.get("type") == "output_text") - - def check_artifacts(): - values = verify_session_artifacts(client, foreign, http, sid, eid, artifacts) - safe([item.to_dict() for item in values]) - - try: - assert expected_environment_type in ("self_hosted", "openai_hosted") - assert session.environment.type == expected_environment_type - if expected_environment_type == "self_hosted": - assert session.environment.workspace_directory == "/workspace" - assert client.beta.agents.environments.retrieve(eid).status == "connected", "Runtime is not connected" - assert not list(sessions.turns.list(sid)) and not list(sessions.items.list(sid)), "Use an unused Session" - assert not list(sessions.artifacts.list(sid)), "Use a Session without existing Artifacts" - report["private_file_sha256"] = private_hashes() - isolation = ("from pathlib import Path\nimport hashlib,os\n" - f"protected={private_paths!r}\n" - f"secret_hashes={sorted({hashlib.sha256(value.encode()).hexdigest() for value in secrets})!r}\n" - "for path in protected:\n" - " try:\n with open(path,'rb') as f: exposed=f.read(1)\n" - " except (FileNotFoundError,PermissionError): continue\n" - " assert not exposed, 'Protected private bytes are visible'\n" - "for key,value in os.environ.items():\n" - " candidates=[value,value.removeprefix('Bearer ')]\n" - " assert all(hashlib.sha256(candidate.encode()).hexdigest() not in secret_hashes for candidate in candidates), 'Private credential is visible in tool environment'\n" - "proof={'protected_bytes_unreadable':True,'known_credentials_absent_from_environment':True}\n") - def isolation_call(phase): - return (f"proof=runpy.run_path({prefix + '-isolation.py'!r})['proof']\n" - f"with Path({prefix + '-isolation-' + phase + '.json'!r}).open('x') as f: json.dump(proof,f)\n") - publish = ("from pathlib import Path\nimport json,runpy\n" + isolation_call("first") - + f"p=Path({prefix + '-published'!r})\nwith p.open('ab') as f: f.write(b'published\\n')\n" - "root=Path('/workspace/outputs');root.mkdir(exist_ok=True)\nassert not any(root.iterdir())\n" - + "\n".join(f"with Path({path!r}).open('xb') as f: f.write({data!r})" for path, data in outputs.items()) + "\n") - hold = ("from pathlib import Path\nimport os,time\n" - f"with Path({starts!r}).open('ab') as f: f.write(b'started\\n');f.flush();os.fsync(f.fileno())\n" - f"with Path({ticks!r}).open('ab', buffering=0) as f:\n" - " while True:\n f.write(b'tick\\n');os.fsync(f.fileno());time.sleep(.2)\n") - recover = "from pathlib import Path\nimport json,runpy\n" + isolation_call("recovered") - resume = f"from pathlib import Path\nwith Path({prefix + '-resumed'!r}).open('xb') as f: f.write(b'resumed\\n')\n" - for suffix, script in (("-isolation.py", isolation), ("-publish.py", publish), ("-recover.py", recover), - ("-hold.py", hold), ("-resume.py", resume)): - upload(prefix + suffix, script) - report["checks"].append("public_files_create_exact_runtime_bytes") - - def isolation_proof(phase): - proof = json.loads(runtime.read(prefix + "-isolation-" + phase + ".json")) - assert proof == {"protected_bytes_unreadable": True, "known_credentials_absent_from_environment": True}, "Native isolation probe failed" - assert private_hashes() == report["private_file_sha256"], "Private witnesses changed across native execution or restart" - report["isolation_" + phase] = proof - - first, items = run_turn("Remember this exact conversation-only token, including the remember- prefix: " + memory + ". " - "Use your native shell tool to run exactly `python3 " + prefix + "-publish.py` once in the foreground. " - "Do not edit the script, delegate, retry the command, or repeat its side effects. Report any error without retrying.", 1) - assert any(item.get("type") == "command_execution" and item.get("status") == "completed" and prefix + "-publish.py" in item.get("command", "") for item in items), "Missing real native command observation" - isolation_proof("first") - assert runtime.read(prefix + "-published") == b"published\n", "Publish command was repeated" - for path, data in outputs.items(): - assert runtime.read(path) == data, "Native output bytes differ" - pages, page = verify_environment_files(client, http, eid, "/workspace/outputs", {path: len(data) for path, data in outputs.items()}) - save("files-list.json", safe(pages)) - verify_file_tenant_isolation(client, foreign, http, eid, "/workspace/outputs", page, list(outputs)) - artifacts[first.id] = outputs - check_artifacts() - for suffix in ("", "/items", "/turns", "/events"): - response = http.get(endpoint + suffix, headers=other_headers) - assert response.status_code == 404, "Foreign tenant accessed Session data" - safe(response.json()) - try: - foreign.beta.agents.sessions.items.list(sid) - except NotFoundError: - pass - else: - raise AssertionError("Foreign tenant accessed SDK Items") - warm, items = run_turn("Return the entire exact conversation-only remember- token from our previous turn, including its prefix. " - "Do not use tools, read files, or repeat any earlier command.", 2) - assert memory in answer(items), "Warm continuation lost native conversation history" - assert runtime.read(prefix + "-published") == b"published\n", "Warm continuation repeated side effects" - check_artifacts() - report["checks"].append("ordinary_warm_turn_preserves_history_and_effect_counts") - committed = snapshot_items("before-restart") - report["checks"].append("native_outputs_files_artifacts_items_and_tenant_isolation") - runtime.restart() - runtime.restart_core() - assert client.beta.agents.environments.retrieve(eid).status == "connected", "Runtime did not reconnect" - assert snapshot_items("after-restart") == committed, "Restart changed committed Items" - second, items = run_turn("Run exactly `python3 " + prefix + "-recover.py` once with your native shell tool; do not edit or retry it. " - "Then return the entire exact conversation-only remember- token from our previous turn, including its prefix. " - "Do not look for the token in files or rerun any earlier command.", 3) - assert any(item.get("type") == "command_execution" and item.get("status") == "completed" and prefix + "-recover.py" in item.get("command", "") for item in items), "Missing recovered native isolation command" - isolation_proof("recovered") - assert memory in answer(items), "Cold continuation lost native conversation history" - assert runtime.read(prefix + "-published") == b"published\n", "Cold continuation repeated side effects" - # Unchanged outputs keep their first-Turn Artifacts; later Turns publish nothing new. - check_artifacts() - report["checks"].append("runtime_and_core_restart_preserve_history_and_outputs") - third, items = run_turn("Use your native shell tool to run exactly `python3 " + prefix + "-hold.py` once and wait in the foreground. " - "It intentionally runs until cancelled. Do not background, delegate, retry, edit the script, or restart the command.", 4, cancellation=True) - commands = [item for item in items if item.get("type") == "command_execution" and prefix + "-hold.py" in item.get("command", "")] - assert len(commands) == 1 and commands[0].get("status") in ("failed", "incomplete"), "Cancelled command lost its native outcome" - stopped = runtime.read(ticks) - time.sleep(2) - assert runtime.read(ticks) == stopped and runtime.read(starts) == b"started\n", "Effects continued after cancellation settled" - sessions.events.create(sid, events=[{"type": "agent.session.input.cancel"}], idempotency_key=nonce + "-cancel") - assert len(list(sessions.turns.list(sid))) == 4, "Duplicate cancellation created work" - check_artifacts() - fourth, items = run_turn("Continue after the cancelled command. Never restart it or rerun the publish script. " - "Run exactly `python3 " + prefix + "-resume.py` once with your native shell tool, without retries. " - "Then return the entire original conversation-only remember- token, including its prefix.", 5) - assert any(item.get("type") == "command_execution" and item.get("status") == "completed" and prefix + "-resume.py" in item.get("command", "") for item in items), "Missing resumed native command observation" - assert memory in answer(items) and runtime.read(prefix + "-resumed") == b"resumed\n", "Post-cancel continuation failed" - assert runtime.read(starts) == b"started\n" and runtime.read(ticks) == stopped, "Continuation repeated cancelled effects" - assert runtime.read(prefix + "-published") == b"published\n", "Continuation repeated publication" - check_artifacts() - report["checks"].append("public_cancel_stops_ticks_duplicate_cancel_and_continuation_preserve_effect_counts") - report.update(passed=True, memory_sha256=hashlib.sha256(memory.encode()).hexdigest(), - output_sha256={path: hashlib.sha256(data).hexdigest() for path, data in outputs.items()}, - ticks_after_cancel=len(stopped.splitlines())) - return report - except BaseException as error: - report["failure_type"] = type(error).__name__ - raise - finally: - report["elapsed_seconds"] = round(time.monotonic() - began, 3) - save("result.json", report)