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
49 changes: 39 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,13 @@ the same transaction as its Session and creation identity when this resource is
implemented. Keep mutable connection/registration state out of immutable
configuration; replacement ownership must fence stale observations.

The current delivery milestone is the Codex single-Agent path: Session
The accepted delivery baseline is the shared Codex/Claude single-Agent path: Session
creation, environment preparation, native execution, files/artifacts, cancellation,
reconnection/recovery queries, and standalone deployment acceptance. Select each
bounded task from the complete board; nonblocking local improvements stay queued.
Authentication, tenant/credential isolation, state consistency and data loss remain
material acceptance requirements. Other harnesses and protocol Subagent execution
are deferred without changing the complete pinned protocol target.
material acceptance requirements. Additional harness implementations and protocol Subagent execution
remain queued without changing the complete pinned protocol target.

The current hosted architecture is V1: Core runs independently; each Environment
sandbox contains its daemon, selected native harness, local tools and workspace.
Expand Down Expand Up @@ -382,7 +382,7 @@ implementation behind its adapter. Core acts on verified capabilities and runtim
conditions; a capability declaration alone never grants public feature admission.
Extend existing interfaces during related functional work without introducing a
second framework or a broad rewrite. The completed MVP remains Codex/Docker.
The next accepted engine profile is Claude SDK on the same dedicated Docker
Claude SDK is an equal first-class engine on the same dedicated Docker
Runtime; qualify each image with the common full-loop acceptance before deploying.
E2B, other engines and hosted remote-executor separation remain outside this batch.
Later engines must satisfy the same applicable acceptance contract while keeping
Expand Down Expand Up @@ -1928,6 +1928,34 @@ must close their query, await their native child and drain observations before
publishing completion. Process groups are lifecycle supervision, not OS isolation
or containment of descendants that deliberately leave the group.

### Harness qualification and onboarding

Codex, Claude and future harnesses have equal architectural status. The common
Runtime wire protocol and Factory/Session/Prepared interfaces own lifecycle,
input receipts, cancellation, recovery and resource access; each native adapter
retains its implementation and model/tool loop. A new engine supplies an adapter,
a qualified profile in `services/agents-api/internal/engine`, registration and
an independently verified deployment. It does not add engine-name branches to
API handlers, persistence, dispatch or scheduling.

The small static profile catalog owns engine-specific public admission and value
limits. Profile callbacks are pure and use existing public/protocol types; they
cannot query business data, decrypt credentials or control native processes.
Public schema validation, qualified engine support and actual Runtime capabilities
remain separate. Runtime advertisements alone never enable public operations.
Shared dispatch checks capability combinations, not a whitelist of engine names.
Use the same public acceptance assertions for both engines, retaining native
isolation tests where appropriate. Record unsupported or unverified combinations
as implementation gaps; do not reduce another engine's functionality to claim
parity or equate accepted parameters with applied native behavior.

Claude hosted functions compose the existing SDK function bridge with the native
workspace sandbox. Only declared function tools and the verified native tool
inventory are available. The bundle advertises this combination separately from
basic workspace execution; older bundles cannot receive function preparations.
External hosted MCP remains unqualified. Function callbacks do not change file,
credential, history, subagent or network authority.

### Claude dedicated Docker Runtime

Build the pinned SDK bundle with `scripts/build-claude-sdk-runtime.sh`, then use
Expand Down Expand Up @@ -1957,8 +1985,9 @@ The SDK adapter advertises `local_runtime_v1` only for its Linux bridge contract
Registration combines that contract with the verified operator binding. Core uses
an explicit accepted engine profile independently of advertisements. This profile
supports native Bash/Read/Edit, preparation, shared Files/Artifacts, cancellation
and same-history continuation. Functions and MCP in this workspace profile remain
unqualified and reject before persistence; their existing `none` support is retained.
and same-history continuation. The separately advertised `workspace_functions`
combination supports declared public functions with text results. External HTTP
MCP remains unqualified here; its existing `none` support is retained.
Native Bash network access uses the harness's HTTP proxy; no alternate networking
or tool loop is implemented by Core.

Expand Down Expand Up @@ -1987,8 +2016,8 @@ their streamed partial text. No phase is inferred from the final result.
SDK/native child release and output draining precede daemon completion.

`claudesdk.Config.Workspace` is a private, trusted operator binding for one
qualified placement. It enables only native Bash/Read/Edit in the existing SDK
loop. The entire factory must already run inside an outer mount/process boundary
qualified placement. It enables native Bash/Read/Edit and declared host functions
in the existing SDK loop. The entire factory must already run inside an outer mount/process boundary
that excludes application, daemon and other-tenant credentials and host policy.
The factory does not create that boundary. The dedicated Docker profile below
selects this binding at startup; cwd and request options cannot select its policy.
Expand All @@ -2011,7 +2040,7 @@ strict sandbox with no fallback or weaker isolation. Separate native file-tool
permissions and a session tool hook restrict Read/Edit to the bound workspace
and deny protected roots; background/unsandboxed Bash requests are rejected.
The deployment must retain these controls, including the SDK-owned hook.
Functions, MCP and remote-environment combinations are rejected in this private
External MCP and remote-environment combinations remain rejected in this private
profile until separately qualified. The existing `none` profile retains its
behavior. Packaged `workspace_tools` establishes bridge support only, not host
isolation or a public capability. The dedicated Runtime integration composes
Expand Down Expand Up @@ -2213,7 +2242,7 @@ Operators may configure the daemon provider environment or the existing transien
(`base_url` HTTPS and `bearer_token`). Core forwards these opaque options without
persisting them in Session configuration. The adapter exclusively selects the
provider environment and removes credentials from native tool environments. Product `claude_code` and product execution are unchanged.
The public profile accepts only
The `none` public profile accepts only
text, explicit model/system instructions, managed state, exact native resume and
declared functions with ordered text results, and the HTTP MCP subset
described above. It rejects unsupported request
Expand Down
3 changes: 3 additions & 0 deletions apps/parsar-daemon/internal/agent/claudesdk/preparation.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ func NewPreparationFactory(config Config) agent.PreparationFactory {
if start.observeFunctions && !info.supportsWorkspaceCommands() {
return nil, fmt.Errorf("claudesdk: packaged runtime does not support workspace command observations")
}
if len(start.Functions) > 0 && !info.SupportsWorkspaceFunctions() {
return nil, fmt.Errorf("claudesdk: packaged runtime does not support workspace functions")
}
s, err := launch(owner, snapshot, start, env)
if err != nil {
return nil, err
Expand Down
4 changes: 4 additions & 0 deletions apps/parsar-daemon/internal/agent/claudesdk/readiness.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ func (info RuntimeInfo) SupportsLocalRuntime() bool {
return info.supportsWorkspaceCommands() && slices.Contains(info.Features, "local_runtime_v1")
}

func (info RuntimeInfo) SupportsWorkspaceFunctions() bool {
return info.SupportsLocalRuntime() && slices.Contains(info.Features, "workspace_functions")
}

// CheckRuntime checks the packaged companion and exact execution entrypoint.
// It does not create Session state, register an engine or make a model request.
func CheckRuntime(ctx context.Context, config Config) (RuntimeInfo, error) {
Expand Down
2 changes: 1 addition & 1 deletion apps/parsar-daemon/internal/agent/claudesdk/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type workspaceProfile struct {
}

func prepareWorkspace(config Config, req proto.PromptRequestPayload) (*workspaceProfile, []string, error) {
if req.DisableExecutionEnvironment || req.RemoteEnvironment != nil || len(req.FunctionTools) != 0 || req.MCPHTTPServers != nil {
if req.DisableExecutionEnvironment || req.RemoteEnvironment != nil || req.MCPHTTPServers != nil {
return nil, nil, fmt.Errorf("claudesdk: workspace profile does not support the requested execution combination")
}
if req.WorkDir != "" && req.WorkDir != config.Workspace.Directory {
Expand Down
17 changes: 14 additions & 3 deletions apps/parsar-daemon/internal/agent/claudesdk/workspace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestWorkspaceTrustedBindingAndEnvironment(t *testing.T) {
}

func TestWorkspaceRejectsConflictsBeforeSideEffects(t *testing.T) {
for _, name := range []string{"none", "remote", "work-dir", "function", "mcp", "caller-policy", "relative", "missing", "overlap", "symlink", "rule-pattern", "ambient-setting", "duplicate-env", "bad-env", "path-empty-component", "path-workspace", "code-in-workspace"} {
for _, name := range []string{"none", "remote", "work-dir", "mcp", "caller-policy", "relative", "missing", "overlap", "symlink", "rule-pattern", "ambient-setting", "duplicate-env", "bad-env", "path-empty-component", "path-workspace", "code-in-workspace"} {
t.Run(name, func(t *testing.T) {
config := workspaceFixture(t)
req := workspaceRequest()
Expand All @@ -87,8 +87,6 @@ func TestWorkspaceRejectsConflictsBeforeSideEffects(t *testing.T) {
req.RemoteEnvironment = &proto.RemoteEnvironment{}
case "work-dir":
req.WorkDir = config.Workspace.ScratchDir
case "function":
req.FunctionTools = []proto.FunctionTool{{Name: "hello", Parameters: json.RawMessage(`{"type":"object"}`)}}
case "mcp":
req.MCPHTTPServers = &[]proto.MCPHTTPServer{}
case "caller-policy":
Expand Down Expand Up @@ -136,3 +134,16 @@ func TestWorkspaceRejectsConflictsBeforeSideEffects(t *testing.T) {
})
}
}

func TestWorkspaceRetainsDeclaredFunctions(t *testing.T) {
config := workspaceFixture(t)
req := workspaceRequest()
req.FunctionTools = []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{"type":"object"}`)}}
start, _, err := prepare(config, req)
if err != nil {
t.Fatal(err)
}
if start.Workspace == nil || len(start.Functions) != 1 || start.Functions[0].Name != "lookup" || start.MCPHTTPServers != nil {
t.Fatal("workspace function declaration was not retained independently of external MCP")
}
}
2 changes: 1 addition & 1 deletion apps/parsar-daemon/internal/cli/claude_sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func discoverClaudeSDK(rc *runContext, profile string, check func(context.Contex
return fail(fmt.Errorf("Claude SDK bundle does not support the local Runtime contract"))
}
caps := &out.Info.Capabilities
caps.EnvironmentNone, caps.FunctionTools = false, false
caps.EnvironmentNone, caps.FunctionTools = false, info.SupportsWorkspaceFunctions()
caps.Preparation, caps.LocalEnvironment, caps.LocalEnvironmentNetworkPolicy = true, true, true
caps.WorkspaceReadPreparation, caps.NativeSessionRecovery = true, true
}
Expand Down
6 changes: 3 additions & 3 deletions apps/parsar-daemon/internal/dispatch/mcp_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ func validateMCPHTTP(req proto.PromptRequestPayload, caps proto.AgentKindCapabil
if req.MCPHTTPServers == nil {
return nil
}
if req.RemoteEnvironment != nil && (req.AgentKind != "codex" || !caps.MCPHTTPTools || !caps.MCPHTTPRemoteEnvironment) {
if req.RemoteEnvironment != nil && (!caps.MCPHTTPTools || !caps.MCPHTTPRemoteEnvironment) {
return errors.New("engine does not support service-side HTTP MCP with a remote environment")
}
for _, server := range *req.MCPHTTPServers {
if server.Required && (req.AgentKind != "codex" || !caps.MCPHTTPTools || !caps.MCPHTTPRequired || req.DisableExecutionEnvironment == (req.RemoteEnvironment != nil)) {
if server.Required && (!caps.MCPHTTPTools || !caps.MCPHTTPRequired || req.DisableExecutionEnvironment == (req.RemoteEnvironment != nil)) {
return errors.New("engine does not support required service-side HTTP MCP initialization")
}
if server.BearerToken == nil {
Expand All @@ -25,7 +25,7 @@ func validateMCPHTTP(req proto.PromptRequestPayload, caps proto.AgentKindCapabil
if !caps.MCPHTTPTools || !caps.MCPHTTPBearerAuth {
return errors.New("engine does not support authenticated HTTP MCP")
}
if (req.AgentKind != "codex" && req.AgentKind != "claude_sdk") || req.DisableExecutionEnvironment == (req.RemoteEnvironment != nil) {
if req.DisableExecutionEnvironment == (req.RemoteEnvironment != nil) {
return errors.New("authenticated HTTP MCP requires a supported service-side environment")
}
if req.RemoteEnvironment != nil {
Expand Down
4 changes: 2 additions & 2 deletions apps/parsar-daemon/internal/dispatch/mcp_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func TestMCPHTTPBearerRejectsUnsupportedRequestsBeforeFactory(t *testing.T) {
return nil, errors.New("controlled factory stop")
})
err := h.router.Handle(t.Context(), mustEnv(t, proto.TypePromptRequest, "mcp-bearer", req))
if called != (mode == "supported" || mode == "claude" || mode == "credential-free" || mode == "product" || mode == "required" || mode == "optional old peer") {
if called != (mode == "supported" || mode == "claude" || mode == "claude remote" || mode == "other engine" || mode == "credential-free" || mode == "product" || mode == "required" || mode == "optional old peer") {
t.Fatal("wrong factory admission")
}
frames := h.sender.snapshot()
Expand Down Expand Up @@ -143,7 +143,7 @@ func TestRemoteMCPRejectsBeforePreparationFactory(t *testing.T) {
return nil, errors.New("controlled stop")
})
err := h.router.Handle(t.Context(), mustEnv(t, proto.TypeExecutionPrepare, "remote-mcp", req))
allowed := mode == "supported" || mode == "no declaration" || mode == "bearer supported" || mode == "required"
allowed := mode == "supported" || mode == "other engine" || mode == "no declaration" || mode == "bearer supported" || mode == "required"
if (err == nil) != allowed {
t.Fatal("wrong preparation admission", err)
}
Expand Down
2 changes: 2 additions & 0 deletions contracts/agents-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ in an execution adapter. The application core owns execution state and persisten
engine-specific shapes stay at the adapter boundary. Codex uses its native
app-server; Claude uses the maintained Agent SDK. Reuse native protocols and SDKs
for further harnesses rather than adding another model/tool loop.
The [harness contract and parity baseline](harnesses.md) describes equal-engine
registration, qualification and shared acceptance.
Verify configuration against actual execution: response defaults must not merely
describe values the adapter never applied.

Expand Down
Loading
Loading