Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/api-acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ jobs:
OAC_TEST_SERVER_BIN: ${{ runner.temp }}/oac-core-build/oac-core
OAC_TEST_OFFICIAL_SDK_PYTHON: python
run: |
python services/core/tests/official_schema_test.py
python services/core/tests/official_client.py
go test ./services/core/tests/integration -run '^(TestFunctionStateOfficialClientReadsAndLiveEvents|TestSavedReferenceRetryOfficialClient|TestAgentUpdateOfficialClient|TestAgentDeletionOfficialClient|TestSessionAgentFilterOfficialClient|TestSessionDeletionOfficialClient|TestEnvironmentInitialFailureOfficialClient|TestSelfHostedInitialCreationOfficialClient|TestSelfHostedCancellationOfficialClient)$' -count=1
- uses: ./.github/actions/e2b-provider
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
persist-credentials: false
- name: Prepare the report schema
id: schema
run: python3 scripts/ci_review.py schema >> "$GITHUB_OUTPUT"
run: python3 scripts/ci_review.py schema "${{ matrix.kind }}" >> "$GITHUB_OUTPUT"
- name: Review with Claude Code
id: llm
continue-on-error: true
Expand Down Expand Up @@ -78,7 +78,7 @@ jobs:

${{ matrix.instructions }}

按 JSON schema 返回中文报告。status 为 ok(未发现问题)、issues(发现有证据的问题)或 incomplete(审查失败、范围未检查完整或检查结果尚未完成)。有问题且仍有未检查项时,用 issues 并在 summary 中说明缺项。summary 无问题时简短,有问题时保留依据和建议,遵守 schema 的长度限制。
按 JSON schema 返回中文报告。status 为 ok(未发现问题)、issues(发现有证据的问题)或 incomplete(审查失败、范围未检查完整或检查结果尚未完成)。有问题且仍有未检查项时,用 issues 并在对应字段中说明缺项。按 schema 把各项结论分别填入字段,不要重复标题;changes 用 1–3 条 Markdown 列表,其他字段无问题时一句话,有问题时保留依据和建议,遵守各字段长度限制。
围绕本次 diff 和受影响的文档展开,证据充分后输出结果,避免重复核对同一结论。
只读审查,不修改仓库,不触发新的 CI,不发送消息。两份报告会由后续 job 合并发送。
claude_args: >-
Expand All @@ -89,7 +89,7 @@ jobs:
env:
REVIEW_OUTCOME: ${{ steps.llm.outcome }}
REVIEW_RESULT: ${{ steps.llm.outputs.structured_output }}
run: python3 scripts/ci_review.py collect "$RUNNER_TEMP/review-${{ matrix.kind }}.json"
run: python3 scripts/ci_review.py collect "${{ matrix.kind }}" "$RUNNER_TEMP/review-${{ matrix.kind }}.json"
- uses: actions/upload-artifact@v6
if: always()
with:
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Do not multiply entities without necessity. The long-term goal is minimal code,

| Boundary | Protocol code | Protocol doc |
| --- | --- | --- |
| Application–Core (`/v1`) | Types in `contracts/agents-api/v1/` and route annotations in `services/core/internal/api/`; `make openapi` generates `contracts/agents-api/openapi.yaml` | [Agents API guide](docs/api/public-agent-api.md) |
| Application–Core (`/v1`) | Official schema pinned by `contracts/agents-api/upstream.json` plus Go-owned `x_agents_core` extensions; `make openapi` generates public Go types and `contracts/agents-api/openapi.yaml` | [Agents API guide](docs/api/public-agent-api.md) |
| Web and operators–Core (`/core/v1`) | Route annotations in `services/core/internal/api/`; `make openapi` generates `contracts/agents-api/core.openapi.yaml` | [Core administration API](contracts/agents-api/admin-api.md) |
| Nodes and daemons–Core (`/api/v1` HTTP routes; the node and daemon wire protocols are separate rows) | Route annotations in `services/core/internal/api/`; `make openapi` generates `contracts/agents-api/runtime.openapi.yaml` | [Machine connection API](contracts/agents-api/machine-api.md) |
| Core–Sandbox Provider | `services/core/internal/sandbox/sandbox_provider.go` | [Sandbox Provider guide](docs/sandbox-provider.md) |
Expand Down
15 changes: 11 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,28 @@ sqlc-generate:

SWAG ?= go run github.com/swaggo/swag/cmd/swag@$(SWAG_VERSION)

.PHONY: openapi
.PHONY: openapi check-openapi
OPENAPI_FLAGS ?=
check-openapi:
$(MAKE) openapi OPENAPI_FLAGS=--check
python3 scripts/generate-public-api.test.py

openapi:
@set -e; root="$${OAC_DEV_HOME:-$$HOME/.oac}/build"; mkdir -p "$$root"; \
output=$$(mktemp -d "$$root/core-openapi.XXXXXX"); trap 'rm -rf "$$output"' EXIT; \
python3 scripts/generate-public-api.py $(OPENAPI_FLAGS) --swag-roots "$$output/roots.go"; \
$(SWAG) init \
-g cmd/server/main.go --dir ./services/core,./contracts/agents-api/v1 \
-g cmd/server/main.go --dir "./services/core,./contracts/agents-api/v1,$$output" \
--output "$$output" \
--outputTypes yaml --parseInternal; \
python3 scripts/patch-agents-openapi.py "$$output/swagger.yaml"; \
go run ./scripts/openapi-split "$$output/swagger.yaml" contracts/agents-api/openapi.yaml contracts/agents-api/core.openapi.yaml contracts/agents-api/runtime.openapi.yaml
go run ./scripts/openapi-split $(OPENAPI_FLAGS) "$$output/swagger.yaml" "$$output/extensions.json" contracts/agents-api/core.openapi.yaml contracts/agents-api/runtime.openapi.yaml; \
python3 scripts/generate-public-api.py $(OPENAPI_FLAGS) --extensions "$$output/extensions.json"

check-sqlc:
python3 scripts/check-sqlc.py

check-go:
check-go: check-openapi
go test ./apps/daemon/... ./apps/sandboxio/... ./internal/... ./contracts/agents-api/... ./scripts/openapi-split -count=1

.PHONY: check-runtime-contract
Expand Down
4 changes: 2 additions & 2 deletions apps/daemon/internal/agent/claudesdk/declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, d
}
caps := &out.Info.Capabilities
caps.EnvironmentNone, caps.FunctionTools = proto.CapabilityUnsupported, proto.CapabilityFromBool(info.SupportsWorkspaceFunctions())
caps.Preparation, caps.LocalEnvironment = proto.CapabilitySupported, proto.CapabilitySupported
caps.WorkspaceReadPreparation, caps.NativeSessionRecovery = proto.CapabilitySupported, proto.CapabilitySupported
caps.LocalEnvironment, caps.WorkspaceReadPreparation = proto.CapabilitySupported, proto.CapabilitySupported
caps.NativeSessionRecovery = proto.CapabilitySupported
}
out.Info.Available, out.Info.Version = true, info.SDK
out.Info.Capabilities.MessageImages = proto.CapabilityFromBool(info.SupportsMessageImages())
Expand Down
4 changes: 2 additions & 2 deletions apps/daemon/internal/agent/codex/executor_native_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package codex
import (
"context"
"encoding/json"
"log/slog"
"os"
"os/exec"
"strings"
Expand All @@ -12,7 +13,6 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent"
"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
"github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider"
obslog "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log"
)

// This opt-in test uses the real pinned harness and an explicitly configured
Expand Down Expand Up @@ -48,7 +48,7 @@ func TestExecutorNativeReuse(t *testing.T) {
}
cfg := defaultSessionConfig()
cfg.codexBinary = binary
cfg.logger = obslog.Discard()
cfg.logger = slog.New(slog.DiscardHandler)
req := proto.PromptRequestPayload{
AgentKind: "codex", AgentStateKey: "executor-native",
DisableExecutionEnvironment: true, DisableSubagents: true, ObserveMessages: true,
Expand Down
136 changes: 126 additions & 10 deletions apps/daemon/internal/agent/codex/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package codex

import (
"context"
"encoding/json"
"errors"
"os"
"strings"
"sync"
"sync/atomic"
"testing"
Expand All @@ -17,22 +19,32 @@ import (
func executorFixture(t *testing.T, mode string) (*Executor, string) {
t.Helper()
req, cfg, root := preparationFixture(t)
e, err := testExecutor(t, mode, req, cfg)
if err != nil {
t.Fatal(err)
}
return e, root
}

// testExecutor prepares through the production factory and closes the owner at cleanup.
func testExecutor(t *testing.T, mode string, req proto.PromptRequestPayload, cfg sessionConfig) (*Executor, error) {
t.Helper()
t.Setenv("OAC_TEST_EXECUTOR_MODE", mode)
ownerCtx, cancelOwner := context.WithCancel(context.Background())
t.Cleanup(cancelOwner)
e, err := newExecutor(ownerCtx, req, cfg)
if err != nil {
t.Fatal(err)
if e != nil {
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := e.Close(ctx); err != nil {
t.Error(err)
}
})
}
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := e.Close(ctx); err != nil {
t.Error(err)
}
})
return e, root
return e, err
}

func awaitExecutorTurn(t *testing.T, turn agent.Turn, out <-chan proto.Envelope) agent.TurnSettlement {
t.Helper()
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
Expand All @@ -52,6 +64,21 @@ func awaitExecutorTurn(t *testing.T, turn agent.Turn, out <-chan proto.Envelope)
}
return settlement
}

// settledFrames waits for a Turn to settle and returns its complete output.
func settledFrames(t *testing.T, turn agent.Turn, out <-chan proto.Envelope) []proto.Envelope {
t.Helper()
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
if _, err := turn.AwaitSettlement(ctx); errors.Is(err, context.DeadlineExceeded) {
t.Fatal("Turn did not settle")
}
var frames []proto.Envelope
for frame := range out {
frames = append(frames, frame)
}
return frames
}
func TestExecutorNormalTurnsKeepProcessAndThread(t *testing.T) {
e, root := executorFixture(t, "complete")
var previous agent.Turn
Expand Down Expand Up @@ -84,6 +111,95 @@ func TestExecutorNormalTurnsKeepProcessAndThread(t *testing.T) {
t.Fatal("normal completion closed executor")
}
}
func TestExecutorFreezesPreparedConfiguration(t *testing.T) {
for _, resume := range []bool{false, true} {
t.Run(map[bool]string{false: "new", true: "resumed"}[resume], func(t *testing.T) {
req, cfg, root := preparationFixture(t)
expectedThread := "thread/start"
if resume {
req.AgentSessionID, expectedThread = "fixture-native-thread", "thread/resume"
}
e, err := testExecutor(t, "complete", req, cfg)
if err != nil {
t.Fatal(err)
}
assertPreparationOnly(t, root)
cwd := e.prepared.plan.Cwd
// Caller-owned data cannot revise the prepared native configuration.
req.Model = "different-model"
req.AgentSessionID = "different-thread"
copy(req.FunctionTools[0].Parameters, strings.ReplaceAll(string(req.FunctionTools[0].Parameters), "integer", "boolean"))
out := make(chan proto.Envelope, 20)
turn, err := e.StartTurn(t.Context(), "actual-run", proto.TextInput("actual prompt"), out)
if err != nil {
t.Fatal(err)
}
awaitExecutorTurn(t, turn, out)
counts := map[string]int{}
for _, frame := range preparationFrames(t, root) {
counts[frame.Method]++
var params struct {
Model string `json:"model"`
ThreadID string `json:"threadId"`
DynamicTools []dynamicFunctionTool `json:"dynamicTools"`
Cwd string `json:"cwd"`
Environments json.RawMessage `json:"environments"`
}
if err := json.Unmarshal(frame.Params, &params); err != nil {
t.Fatal(err)
}
if frame.Method == "thread/start" && (params.Model != "fixture-model" || len(params.DynamicTools) != 1 ||
!strings.Contains(string(params.DynamicTools[0].InputSchema), "integer") || params.Cwd != cwd) {
t.Fatal("prepared configuration changed", string(frame.Params))
}
if frame.Method == "thread/resume" && params.ThreadID != "fixture-native-thread" {
t.Fatal("prepared resume changed")
}
if len(params.Environments) != 0 {
t.Fatal("prepared environment changed")
}
}
if counts["initialize"] != 1 || counts["environment/status"] != 2 || counts[expectedThread] != 1 || counts["turn/start"] != 1 {
t.Fatal("unexpected native setup/start count", counts)
}
})
}
}

func TestExecutorUnavailableOwnerStartsNoTurn(t *testing.T) {
for _, reason := range []string{"closed", "owner cancelled", "rpc exited"} {
t.Run(reason, func(t *testing.T) {
req, cfg, root := preparationFixture(t)
owner, cancelOwner := context.WithCancel(context.Background())
defer cancelOwner()
e, err := newExecutor(owner, req, cfg)
if err != nil {
t.Fatal(err)
}
defer e.Close(context.Background())
switch reason {
case "closed":
err = e.Close(t.Context())
case "owner cancelled":
cancelOwner()
case "rpc exited":
err = e.prepared.session.rpc.Close()
}
if err != nil {
t.Fatal(err)
}
out := make(chan proto.Envelope, 1)
if turn, err := e.StartTurn(t.Context(), "late-run", proto.TextInput("must not start"), out); turn != nil || err == nil {
t.Fatal("unavailable executor started a Turn", err)
}
if len(out) != 0 {
t.Fatal("rejected start emitted output")
}
assertPreparationOnly(t, root)
})
}
}

func TestExecutorCancellationSettlesThenReuses(t *testing.T) {
e, root := executorFixture(t, "complete")
out := make(chan proto.Envelope, 20)
Expand Down
20 changes: 6 additions & 14 deletions apps/daemon/internal/agent/codex/mcp_http_preflight_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package codex

import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
)
Expand Down Expand Up @@ -155,9 +153,9 @@ func TestPublicMCPHTTPPreparationChecksBeforeNewAndResumedThread(t *testing.T) {
path := filepath.Join(root, "native-config.json")
writeMCPHTTPConfigResponse(t, path, response)
t.Setenv("OAC_TEST_PREPARATION_MCP_CONFIG", path)
p, err := newPreparation(t.Context(), req, cfg)
e, err := testExecutor(t, "complete", req, cfg)
if strings.HasPrefix(mode, "reject") {
if err == nil || p != nil {
if err == nil || e != nil {
t.Fatal("ambient MCP configuration admitted")
}
assertPreparationOnly(t, root)
Expand All @@ -167,18 +165,18 @@ func TestPublicMCPHTTPPreparationChecksBeforeNewAndResumedThread(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer p.Close()
assertPreparationOnly(t, root)
home, err := allocCodexHome(req.AgentStateKey)
if err != nil {
t.Fatal(err)
}
s, err := p.start(t.Context(), "actual-run", proto.TextInput("actual prompt"), make(chan proto.Envelope, 8))
out := make(chan proto.Envelope, 20)
turn, err := e.StartTurn(t.Context(), "actual-run", proto.TextInput("actual prompt"), out)
if err != nil {
t.Fatal(err)
}
defer s.Cancel(context.Background())
frames := waitPreparationMethod(t, root, "turn/start")
awaitExecutorTurn(t, turn, out)
frames := preparationFrames(t, root)
checked, statuses := false, 0
for _, frame := range frames {
if frame.Method == "environment/status" {
Expand All @@ -203,12 +201,6 @@ func TestPublicMCPHTTPPreparationChecksBeforeNewAndResumedThread(t *testing.T) {
t.Fatal("preflight started discovery")
}
}
_ = s.Cancel(context.Background())
select {
case <-s.waitDone:
case <-time.After(4 * time.Second):
t.Fatal("native fixture did not release")
}
})
}
}
Loading
Loading