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: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ Core must build, deploy and run independently of product services, frontends and
- Parsar owns users, workspaces, business authorization, Agent/Team definitions, capabilities, product conversations, IM/sharing, approval decisions and billing. It uses Core for execution.
- A product conversation may reference several execution Sessions. Core owns native engine session identities; an execution Session has its own lifetime, separate from a daemon connection, process or sandbox.
- Build application orchestration on the [public Session and event contract](docs/api/public-agent-api.md). Product cursor replay must be an explicit product extension. Business Team orchestration belongs to the application; Core's pinned `multi_agent` and Subagent resources remain part of the public contract.
- Daemon Skill/SP authoring is a product operation: forward it through a scoped product callback that checks the original requester and workspace. A Runtime credential alone must not authorize business writes.

### Optional application example

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func TestLiveClaudeSDKCancelResume(t *testing.T) {
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, StrictResume: true, ReleaseOnCompletion: true, ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", 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."}}
request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, ReleaseOnCompletion: true, ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", 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 := NewFactory(config)(ctx, request, out)
if err != nil {
t.Fatal(err)
Expand Down
10 changes: 3 additions & 7 deletions apps/daemon/internal/agent/claudesdk/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ func (c *commandState) receive(event bridgeEvent, start startRequest, sessionID
return fmt.Errorf("claudesdk: inconsistent command observation")
}
c.calls[event.ID] = *n
if start.observeFunctions {
emit(proto.TypeToolCall, proto.ToolCallPayload{ID: event.ID, Name: "Bash", Stage: event.Stage, Observation: n})
}
emit(proto.TypeToolCall, proto.ToolCallPayload{ID: event.ID, Name: "Bash", Stage: event.Stage, Observation: n})
return nil
}

Expand All @@ -48,15 +46,13 @@ func (c *commandState) complete() bool {
return true
}

func (c *commandState) close(start startRequest, emit func(string, any)) {
func (c *commandState) close(emit func(string, any)) {
for id, call := range c.calls {
if call.Status != "in_progress" {
continue
}
call.Status = "incomplete"
c.calls[id] = call
if start.observeFunctions {
emit(proto.TypeToolCall, proto.ToolCallPayload{ID: id, Name: "Bash", Stage: "after", Observation: &call})
}
emit(proto.TypeToolCall, proto.ToolCallPayload{ID: id, Name: "Bash", Stage: "after", Observation: &call})
}
}
102 changes: 42 additions & 60 deletions apps/daemon/internal/agent/claudesdk/commands_session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,74 +16,56 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
)

func TestWorkspaceCommandsRequirePackagedFeatureOnlyWhenRequested(t *testing.T) {
for _, observed := range []bool{false, true} {
config := preparationFixture(t, "old-command-runtime")
req := preparationRequest()
req.ObserveToolObservations = observed
resource, err := NewPreparationFactory(config)(t.Context(), req)
if observed {
if err == nil || !strings.Contains(err.Error(), "workspace command observations") {
t.Fatal("old bridge accepted requested command observations", err)
}
if _, err := os.Stat(filepath.Join(config.StateDir, "launched")); !os.IsNotExist(err) {
t.Fatal("old bridge started execution before rejection")
}
} else {
if err != nil {
t.Fatal("old bridge changed opt-out behavior", err)
}
if err := resource.Close(); err != nil {
t.Fatal(err)
}
}
func TestWorkspaceCommandsRequirePackagedFeature(t *testing.T) {
config := preparationFixture(t, "old-command-runtime")
if _, err := NewPreparationFactory(config)(t.Context(), preparationRequest()); err == nil || !strings.Contains(err.Error(), "workspace preparation is unavailable") {
t.Fatal("old bridge accepted command observations", err)
}
if _, err := os.Stat(filepath.Join(config.StateDir, "launched")); !os.IsNotExist(err) {
t.Fatal("old bridge started execution before rejection")
}
}

func TestWorkspaceCommandFramesKeepStartIdentityAndObservedOutput(t *testing.T) {
for _, observed := range []bool{false, true} {
config := preparationFixture(t, "commands-success")
req := preparationRequest()
req.ObserveToolObservations = observed
resource, err := NewPreparationFactory(config)(t.Context(), req)
if err != nil {
t.Fatal(err)
}
defer resource.Close()
if _, err := os.Stat(filepath.Join(config.StateDir, "start.json")); !os.IsNotExist(err) {
t.Fatal("preparation submitted a command")
}
out := make(chan proto.Envelope, 16)
s, err := resource.Start(t.Context(), "actual-command-run", proto.TextInput("hello"), out)
if err != nil {
t.Fatal(err)
config := preparationFixture(t, "commands-success")
resource, err := NewPreparationFactory(config)(t.Context(), preparationRequest())
if err != nil {
t.Fatal(err)
}
defer resource.Close()
if _, err := os.Stat(filepath.Join(config.StateDir, "start.json")); !os.IsNotExist(err) {
t.Fatal("preparation submitted a command")
}
out := make(chan proto.Envelope, 16)
s, err := resource.Start(t.Context(), "actual-command-run", proto.TextInput("hello"), out)
if err != nil {
t.Fatal(err)
}
defer s.Cancel(context.Background())
var frames []proto.ToolCallPayload
done := 0
for event := range out {
if event.ID != "actual-command-run" || event.Type == proto.TypeError || event.Type == proto.TypeCommandOutput {
t.Fatal("execution identity or final-only command behavior changed", event.Type)
}
defer s.Cancel(context.Background())
var frames []proto.ToolCallPayload
done := 0
for event := range out {
if event.ID != "actual-command-run" || event.Type == proto.TypeError || event.Type == proto.TypeCommandOutput {
t.Fatal("execution identity or final-only command behavior changed", event.Type)
}
if event.Type == proto.TypeToolCall {
var payload proto.ToolCallPayload
if err := event.DecodePayload(&payload); err != nil {
t.Fatal(err)
}
frames = append(frames, payload)
}
if event.Type == proto.TypeDone {
done++
if event.Type == proto.TypeToolCall {
var payload proto.ToolCallPayload
if err := event.DecodePayload(&payload); err != nil {
t.Fatal(err)
}
frames = append(frames, payload)
}
if done != 1 || observed && len(frames) != 2 || !observed && len(frames) != 0 {
t.Fatal("completion or observation opt-in changed", done, frames)
}
if observed && (frames[0].ID != "observed" || frames[1].ID != "observed" || frames[1].Observation.Status != "failed" ||
string(frames[1].Observation.Output) != `"Exit code 7\nretained"`) {
t.Fatal("native failure output was not retained", frames)
if event.Type == proto.TypeDone {
done++
}
}
if done != 1 || len(frames) != 2 {
t.Fatal("completion or observation changed", done, frames)
}
if frames[0].ID != "observed" || frames[1].ID != "observed" || frames[1].Observation.Status != "failed" ||
string(frames[1].Observation.Output) != `"Exit code 7\nretained"` {
t.Fatal("native failure output was not retained", frames)
}
}

func TestWorkspaceCommandCancellationAndBridgeFailuresCloseOnlyPendingCalls(t *testing.T) {
Expand All @@ -93,7 +75,7 @@ func TestWorkspaceCommandCancellationAndBridgeFailuresCloseOnlyPendingCalls(t *t
defer cancel()
config := preparationFixture(t, mode)
req := workspaceRequest()
req.AgentSessionID, req.ObserveToolObservations = "native-session", true
req.AgentSessionID = "native-session"
out := make(chan proto.Envelope, 32)
s, err := NewFactory(config)(ctx, req, out)
if err != nil {
Expand Down
88 changes: 40 additions & 48 deletions apps/daemon/internal/agent/claudesdk/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,61 +13,53 @@ func commandEvent(id, stage, status, command string) bridgeEvent {
Observation: &proto.ToolObservation{Kind: "command", Status: status, Command: command}}
}

func TestCommandObservationLifecycleAndOptIn(t *testing.T) {
for _, observed := range []bool{false, true} {
state := commandState{calls: map[string]proto.ToolObservation{}}
start := startRequest{Workspace: &workspaceProfile{}, observeFunctions: observed}
var events []proto.ToolCallPayload
emit := func(kind string, payload any) {
if kind != proto.TypeToolCall {
t.Fatal(kind)
}
events = append(events, payload.(proto.ToolCallPayload))
}
const command = " printf 'failure\\n'; exit 7 "
before := commandEvent("native-call", "before", "in_progress", command)
if err := state.receive(before, start, "native-session", emit); err != nil || state.complete() {
t.Fatal("call was not pending", err)
}
if err := state.receive(before, start, "native-session", emit); err == nil {
t.Fatal("duplicate bridge call accepted")
}
after := commandEvent("native-call", "after", "failed", command)
after.Observation.Output = json.RawMessage(`"Exit code 7\nfailure"`)
if err := state.receive(after, start, "native-session", emit); err != nil || !state.complete() {
t.Fatal("native result did not complete the call", err)
}
if err := state.receive(after, start, "native-session", emit); err == nil {
t.Fatal("duplicate bridge result accepted")
}
if err := state.receive(commandEvent("pending", "before", "in_progress", "sleep 30"), start, "native-session", emit); err != nil {
t.Fatal(err)
}
state.close(start, emit)
state.close(start, emit)
if !state.complete() || state.calls["pending"].Status != "incomplete" || state.calls["native-call"].Status != "failed" {
t.Fatal("closure changed an observed result or lost an unfinished call")
}
if !observed {
if len(events) != 0 {
t.Fatal("opt-out emitted observations")
}
continue
}
if len(events) != 4 || events[0].ID != "native-call" || events[0].Name != "Bash" || events[0].Observation.Command != command ||
string(events[1].Observation.Output) != `"Exit code 7\nfailure"` || events[1].Observation.ExitCode != nil ||
events[1].Observation.Cwd != nil || events[1].Observation.DurationMS != nil || events[3].ID != "pending" ||
events[3].Observation.Status != "incomplete" || len(events[3].Observation.Output) != 0 {
t.Fatal("observation identity, output or unknown metadata changed", events)
func TestCommandObservationLifecycle(t *testing.T) {
state := commandState{calls: map[string]proto.ToolObservation{}}
start := startRequest{Workspace: &workspaceProfile{}}
var events []proto.ToolCallPayload
emit := func(kind string, payload any) {
if kind != proto.TypeToolCall {
t.Fatal(kind)
}
events = append(events, payload.(proto.ToolCallPayload))
}
const command = " printf 'failure\\n'; exit 7 "
before := commandEvent("native-call", "before", "in_progress", command)
if err := state.receive(before, start, "native-session", emit); err != nil || state.complete() {
t.Fatal("call was not pending", err)
}
if err := state.receive(before, start, "native-session", emit); err == nil {
t.Fatal("duplicate bridge call accepted")
}
after := commandEvent("native-call", "after", "failed", command)
after.Observation.Output = json.RawMessage(`"Exit code 7\nfailure"`)
if err := state.receive(after, start, "native-session", emit); err != nil || !state.complete() {
t.Fatal("native result did not complete the call", err)
}
if err := state.receive(after, start, "native-session", emit); err == nil {
t.Fatal("duplicate bridge result accepted")
}
if err := state.receive(commandEvent("pending", "before", "in_progress", "sleep 30"), start, "native-session", emit); err != nil {
t.Fatal(err)
}
state.close(emit)
state.close(emit)
if !state.complete() || state.calls["pending"].Status != "incomplete" || state.calls["native-call"].Status != "failed" {
t.Fatal("closure changed an observed result or lost an unfinished call")
}
if len(events) != 4 || events[0].ID != "native-call" || events[0].Name != "Bash" || events[0].Observation.Command != command ||
string(events[1].Observation.Output) != `"Exit code 7\nfailure"` || events[1].Observation.ExitCode != nil ||
events[1].Observation.Cwd != nil || events[1].Observation.DurationMS != nil || events[3].ID != "pending" ||
events[3].Observation.Status != "incomplete" || len(events[3].Observation.Output) != 0 {
t.Fatal("observation identity, output or unknown metadata changed", events)
}
}

func TestCommandObservationsRejectUnqualifiedOrInconsistentEvents(t *testing.T) {
for _, mode := range []string{"profile", "uninitialized", "session", "id", "nil", "kind", "empty-command", "name", "cwd", "exit", "duration", "arguments", "error", "output-object", "before-output", "before-status", "after-before", "changed-command", "after-status", "stage"} {
t.Run(mode, func(t *testing.T) {
state := commandState{calls: map[string]proto.ToolObservation{}}
start := startRequest{Workspace: &workspaceProfile{}, observeFunctions: true}
start := startRequest{Workspace: &workspaceProfile{}}
sessionID := "native-session"
event := commandEvent("call", "before", "in_progress", "pwd")
if strings.HasPrefix(mode, "after-") || mode == "changed-command" {
Expand Down Expand Up @@ -130,7 +122,7 @@ func TestCommandObservationUnknownAndEmptyOutputRemainDistinct(t *testing.T) {
for _, output := range []json.RawMessage{nil, json.RawMessage(`null`), json.RawMessage(`""`)} {
state := commandState{calls: map[string]proto.ToolObservation{}}
start := startRequest{Workspace: &workspaceProfile{}}
emit := func(string, any) { t.Fatal("opt-out emitted an observation") }
emit := func(string, any) {}
if err := state.receive(commandEvent("call", "before", "in_progress", "pwd"), start, "native-session", emit); err != nil {
t.Fatal(err)
}
Expand Down
1 change: 0 additions & 1 deletion apps/daemon/internal/agent/claudesdk/declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "claude
Usage: proto.CapabilitySupported,
Resume: proto.CapabilitySupported,
NativeSessionRecovery: proto.CapabilityUnsupported,
WorkspaceAuthoring: proto.CapabilityUnsupported,
Steering: proto.CapabilitySupported,
MessageItems: proto.CapabilitySupported,
ToolObservations: proto.CapabilitySupported,
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/claudesdk/declaration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func TestRuntimeDiscoveryConfigurationAndRegistration(t *testing.T) {
registry := agent.NewRegistry()
registry.Register(Declaration, *runtime)
info := registry.SupportedAgentKinds()[0]
if info.Capabilities.WorkspaceAuthoring.IsSupported() || info.Capabilities.Preparation.IsSupported() != ready {
if info.Capabilities.Preparation.IsSupported() != ready {
t.Fatal(info)
}
if !ready {
Expand Down
3 changes: 0 additions & 3 deletions apps/daemon/internal/agent/claudesdk/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,6 @@ func validateExecutorFeatures(info RuntimeInfo, start startRequest) error {
if start.Subagents != nil && !info.SupportsSubagents() {
return errors.New("claudesdk: subagent resources are unavailable")
}
if start.Workspace != nil && start.observeFunctions && !info.supportsWorkspaceCommands() {
return errors.New("claudesdk: packaged runtime does not support workspace command observations")
}
if start.Workspace != nil && len(start.Functions) > 0 && !info.SupportsWorkspaceFunctions() {
return errors.New("claudesdk: workspace functions are unavailable")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ func TestExecutorNativeConfirmationSurvivesCleanup(t *testing.T) {
t.Fatal("initial output missing")
}
if mode == "pending_function" || mode == "pending_function_unconfirmed" {
if event := <-out; event.Type != proto.TypeToolCall {
t.Fatal("function observation missing")
}
if event := <-out; event.Type != proto.TypeFunctionCall {
t.Fatal("function obligation missing")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func TestLiveClaudeExecutorReuseAndCancel(t *testing.T) {
_ = os.WriteFile(filepath.Join(proof, "executor-evidence.json"), raw, 0600)
}
defer persist()
request := proto.PromptRequestPayload{StrictResume: true, DisableExecutionEnvironment: true, DisableSubagents: true, ObserveMessages: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", 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."}}
request := proto.PromptRequestPayload{DisableExecutionEnvironment: true, DisableSubagents: true, ObserveMessages: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", 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)
Expand Down
4 changes: 2 additions & 2 deletions apps/daemon/internal/agent/claudesdk/executor_turn.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ func (s *session) runTurn(start startRequest, out chan<- proto.Envelope) {
if !s.functionsComplete(cancelled && settlementConfirmed) || !s.steeringComplete() || !mcp.complete() || !commands.complete() {
settlementConfirmed, reusable, reason = false, false, "unsettled_native_operations"
}
mcp.close(start, emit)
commands.close(start, emit)
mcp.close(emit)
commands.close(emit)
s.stopSteering()
metadata := map[string]any{proto.DoneMetaAgentSessionType: "claude_session"}
if id := s.inputSessionID(); id != "" {
Expand Down
10 changes: 4 additions & 6 deletions apps/daemon/internal/agent/claudesdk/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,11 @@ func (s *session) receiveFunction(event bridgeEvent, start startRequest, emit fu
if err != nil {
return err
}
if start.observeFunctions {
id, stage := event.CallID, "after"
if call != nil {
id, stage = call.CallID, "before"
}
emit(proto.TypeToolCall, proto.ToolCallPayload{ID: id, Name: observation.Name, Stage: stage, Observation: observation})
id, stage := event.CallID, "after"
if call != nil {
id, stage = call.CallID, "before"
}
emit(proto.TypeToolCall, proto.ToolCallPayload{ID: id, Name: observation.Name, Stage: stage, Observation: observation})
if call != nil {
emit(proto.TypeFunctionCall, call)
} else {
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/claudesdk/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestFunctionFactoryNativeReceipts(t *testing.T) {
root := t.TempDir()
t.Setenv("OAC_RUNTIME_HOME", root)
config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=" + mode, "GORACE=atexit_sleep_ms=0"}}
request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", ObserveToolObservations: true, AgentOptions: map[string]any{"model": "fake-model", "system_prompt": "instructions"}, FunctionTools: []proto.FunctionTool{{Name: "lookup", Description: "Lookup.", Parameters: json.RawMessage(`{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}}}`)}}}
request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", AgentOptions: map[string]any{"model": "fake-model", "system_prompt": "instructions"}, FunctionTools: []proto.FunctionTool{{Name: "lookup", Description: "Lookup.", Parameters: json.RawMessage(`{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}}}`)}}}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
out := make(chan proto.Envelope, 16)
Expand Down
Loading
Loading