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
21 changes: 18 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -1938,6 +1938,10 @@ 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.

Agents Core is pre-release. Replace superseded internal interfaces and execution
paths cleanly; do not retain version fallbacks or compatibility shims. Preserve
the pinned official public protocol, valid data and still-used infrastructure.

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.
Expand All @@ -1952,7 +1956,7 @@ 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.
basic workspace execution; function preparations require that verified combination.
External hosted MCP remains unqualified. Function callbacks do not change file,
credential, history, subagent or network authority.

Expand Down Expand Up @@ -2179,8 +2183,19 @@ status/configuration or control responses. Diagnostic projections must whitelist
safe fields. This does not permit filtering actual model/tool output to hide a leak.
The bounded adapter profile currently requires connected servers, reserves the
`functions` label, accepts alphanumeric/underscore/hyphen server labels and
alphanumeric/underscore/hyphen/dot selected tool names, and excludes required startup
and remote environments. Public Claude static-bearer HTTPS MCP reuses the shared
alphanumeric/underscore/hyphen/dot selected tool names, and excludes remote
environments. Required startup is separately qualified by `mcp_http_required`.
All HTTP MCP queries use native SDK startup and an empty input iterator to
confirm initialization hooks. Required declarations additionally check connected
server status before the initial prompt is released exactly once. Pending, failed,
missing or ambiguous required status rejects before input; native startup timeouts are retained without
an adapter retry loop. Normal system/init still verifies Session identity and the
complete inventory before input readiness/tool authority. Optional servers retain
their existing inventory checks without a new pre-input connection requirement.
A Runtime must advertise the concrete required-initialization capability; there
is no fallback to an older execution path.

Public Claude static-bearer HTTPS MCP reuses the shared
Vault attachment, frozen selection and scoped decryption path. Selection and final
preclaim require the existing bearer capability; shared authentication dispatch
uses capability/placement checks rather than a Codex-name restriction. Missing keys
Expand Down
6 changes: 3 additions & 3 deletions apps/parsar-daemon/internal/agent/claudesdk/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ func validateMCP(req proto.PromptRequestPayload) error {
endpoint, err := url.Parse(server.ServerURL)
if !mcpLabel.MatchString(server.ServerLabel) || server.ServerLabel == "functions" || labels[server.ServerLabel] ||
err != nil || (endpoint.Scheme != "http" && endpoint.Scheme != "https") || endpoint.Hostname() == "" || endpoint.User != nil ||
strings.ContainsAny(server.ServerURL, "?#") || endpoint.Opaque != "" ||
server.Required {
strings.ContainsAny(server.ServerURL, "?#") || endpoint.Opaque != "" {
return fmt.Errorf("claudesdk: unsupported HTTP MCP declaration")
}
if server.BearerToken != nil && (endpoint.Scheme != "https" || !agent.ValidMCPHTTPBearerToken(*server.BearerToken)) {
Expand All @@ -54,6 +53,7 @@ type mcpHTTPServer struct {
ServerLabel string `json:"server_label"`
ServerURL string `json:"server_url"`
AllowedTools *[]string `json:"allowed_tools"`
Required bool `json:"required,omitempty"`
BearerTokenEnvVar string `json:"bearer_token_env_var,omitempty"`
}

Expand All @@ -64,7 +64,7 @@ func prepareMCPHTTP(declarations *[]proto.MCPHTTPServer) (*[]mcpHTTPServer, []st
servers := make([]mcpHTTPServer, len(*declarations))
var env []string
for i, declaration := range *declarations {
server := mcpHTTPServer{ServerLabel: declaration.ServerLabel, ServerURL: declaration.ServerURL}
server := mcpHTTPServer{ServerLabel: declaration.ServerLabel, ServerURL: declaration.ServerURL, Required: declaration.Required}
if declaration.AllowedTools != nil {
tools := append([]string{}, (*declaration.AllowedTools)...)
server.AllowedTools = &tools
Expand Down
5 changes: 4 additions & 1 deletion apps/parsar-daemon/internal/agent/claudesdk/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,17 @@ func TestHTTPMCPDeclaration(t *testing.T) {
req.DisableExecutionEnvironment = false
}
start, _, err := prepare(config, req)
valid := mode == "unrestricted" || mode == "selected" || mode == "empty" || mode == "nil-slice" || mode == "auth"
valid := mode == "unrestricted" || mode == "selected" || mode == "empty" || mode == "nil-slice" || mode == "auth" || mode == "required"
if (err == nil) != valid {
t.Fatalf("unexpected admission: %v", err)
}
if !valid {
return
}
raw, _ := json.Marshal(start)
if mode == "required" && !strings.Contains(string(raw), `"required":true`) {
t.Fatal("required initialization was discarded")
}
if (mode == "empty" || mode == "nil-slice") && !strings.Contains(string(raw), `"allowed_tools":[]`) {
t.Fatal("empty selection widened")
}
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 @@ -33,6 +33,10 @@ func (info RuntimeInfo) SupportsHTTPMCPBearer() bool {
return info.SupportsHTTPMCP() && slices.Contains(info.Features, "mcp_http_bearer_auth")
}

func (info RuntimeInfo) SupportsHTTPMCPRequired() bool {
return info.SupportsHTTPMCP() && slices.Contains(info.Features, "mcp_http_required")
}

func (info RuntimeInfo) supportsWorkspace() bool {
return slices.Contains(info.Features, "workspace_tools")
}
Expand Down
13 changes: 13 additions & 0 deletions apps/parsar-daemon/internal/agent/claudesdk/readiness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ import (

const readyReport = `{"type":"runtime_ready","protocol":1,"node":"22.22.2","sdk":"0.3.269","mcp":"1.30.0","native":"2.1.269 (Claude Code)"}`

func TestRequiredMCPNeedsQualifiedRuntime(t *testing.T) {
root := t.TempDir()
t.Setenv("PARSAR_HOME", root)
config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "main.js"), StateDir: filepath.Join(root, "state"), Env: []string{
"GO_CLAUDE_READINESS_HELPER=1", "READINESS_MODE=ready-http-mcp", "GORACE=atexit_sleep_ms=0",
}}
req := proto.PromptRequestPayload{RunID: "run", Prompt: "hello", DisableExecutionEnvironment: true,
AgentOptions: map[string]any{"model": "fixture"}, MCPHTTPServers: &[]proto.MCPHTTPServer{{ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp", Required: true}}}
if _, err := NewFactory(config)(t.Context(), req, make(chan proto.Envelope, 1)); err == nil || err.Error() != "claudesdk: packaged runtime does not support required HTTP MCP" {
t.Fatalf("unqualified runtime executed required MCP: %v", err)
}
}

func TestHTTPMCPRejectsOldPackagedRuntime(t *testing.T) {
root := t.TempDir()
t.Setenv("PARSAR_HOME", root)
Expand Down
3 changes: 3 additions & 0 deletions apps/parsar-daemon/internal/agent/claudesdk/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ func NewFactory(config Config) agent.Factory {
return nil, fmt.Errorf("claudesdk: packaged runtime does not support HTTP MCP")
}
for _, server := range *start.MCPHTTPServers {
if server.Required && !info.SupportsHTTPMCPRequired() {
return nil, fmt.Errorf("claudesdk: packaged runtime does not support required HTTP MCP")
}
if server.BearerTokenEnvVar != "" && !info.SupportsHTTPMCPBearer() {
return nil, fmt.Errorf("claudesdk: packaged runtime does not support authenticated HTTP MCP")
}
Expand Down
2 changes: 2 additions & 0 deletions apps/parsar-daemon/internal/cli/claude_sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@ func discoverClaudeSDK(rc *runContext, profile string, check func(context.Contex
out.Info.Available, out.Info.Version = true, info.SDK
out.Info.Capabilities.MCPHTTPTools = info.SupportsHTTPMCP()
out.Info.Capabilities.MCPHTTPBearerAuth = info.SupportsHTTPMCPBearer()
out.Info.Capabilities.MCPHTTPRequired = info.SupportsHTTPMCPRequired()
if out.Config.Workspace != nil {
out.Info.Capabilities.MCPHTTPTools, out.Info.Capabilities.MCPHTTPBearerAuth = false, false
out.Info.Capabilities.MCPHTTPRequired = false
}
fmt.Fprintf(rc.stdout, "Claude SDK preflight ok (SDK %s, %s)\n", info.SDK, info.Native)
return out
Expand Down
5 changes: 3 additions & 2 deletions apps/parsar-daemon/internal/cli/claude_sdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"os"
"path/filepath"
"slices"
"strings"
"testing"

Expand Down Expand Up @@ -132,13 +133,13 @@ func TestClaudeSDKMCPFeatureDiscovery(t *testing.T) {
t.Fatal(err)
}
t.Setenv(claudeSDKNodeEnv, node)
for _, features := range [][]string{nil, {"mcp_http_tools"}, {"mcp_http_bearer_auth"}, {"mcp_http_tools", "mcp_http_bearer_auth"}} {
for _, features := range [][]string{nil, {"mcp_http_tools"}, {"mcp_http_bearer_auth"}, {"mcp_http_tools", "mcp_http_bearer_auth"}, {"mcp_http_required"}, {"mcp_http_tools", "mcp_http_required"}} {
out := discoverClaudeSDK(&runContext{stdout: &strings.Builder{}, stderr: &strings.Builder{}}, "default", func(context.Context, claudesdk.Config) (claudesdk.RuntimeInfo, error) {
info := claudesdk.RuntimeInfo{SDK: "0.3.269", Native: "2.1.269 (Claude Code)", Features: features}
return info, nil
})
supported := len(features) > 0 && features[0] == "mcp_http_tools"
if out == nil || !out.Info.Available || out.Info.Capabilities.MCPHTTPTools != supported || out.Info.Capabilities.MCPHTTPBearerAuth != (len(features) == 2) || out.Info.Capabilities.MCPHTTPRequired {
if out == nil || !out.Info.Available || out.Info.Capabilities.MCPHTTPTools != supported || out.Info.Capabilities.MCPHTTPBearerAuth != (supported && slices.Contains(features, "mcp_http_bearer_auth")) || out.Info.Capabilities.MCPHTTPRequired != (supported && slices.Contains(features, "mcp_http_required")) {
t.Fatal("MCP feature discovery widened the runtime profile")
}
}
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/harnesses.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ syntactically or everything either upstream harness can theoretically perform.
| Public functions in `none` | Qualified | Qualified; object-root schemas and text results |
| Public functions alongside hosted workspace tools | Qualified | Qualified; object-root schemas and text results |
| HTTP MCP and static-bearer Vault credentials in `none` | Qualified | Qualified subset |
| Required MCP initialization | Qualified | Gap; currently rejected |
| Required MCP initialization | Qualified | Qualified on `none`; native readiness before initial input |
| Hosted HTTP MCP | Gap | Gap |
| Function image results | Supported subset | Gap; currently rejected |
| Non-default verbosity | Native/model-dependent support | No equivalent qualified; medium only |
Expand Down
52 changes: 27 additions & 25 deletions contracts/agents-api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1912,31 +1912,33 @@ paths:
Omitted/null capability_directories use the empty-list default; self_hosted
requires configured execution plus executor registry. Claude SDK currently
requires medium verbosity and object-root function schemas. It supports anonymous
or attached static-bearer service-origin HTTP MCP on none with required=false
and separately advertised MCP/bearer runtime support. The shared Vault selection
and immutable binding rules apply; unsupported native labels/tool names reject
before persistence. An attached Vault with no matching credential may remain
anonymous; missing keys or failed credential lookup/decryption never fall
back to anonymous execution. Omitted stream defaults to false; stream and
agent_id cannot be null. Metadata may be null, but its values must be strings.
Initial input accepts a string or user-message array containing text. None
initial input atomically starts a Turn; self_hosted initial input is reserved
while returning its Environment connection target, with execution deferred
to native readiness and Session failure on initial timeout. Omitted or null
input creates an idle Session. With stream=true, returns live Session events
starting at creation; disconnect does not cancel execution. New Sessions retain
their authenticated creator; all creation retries require the same typed subject,
including across key rotation. Saved-Agent retries and inline requests using
Vault attachments or credential references retain caller intent independently
of later resource changes; unrelated inline retries preserve resolved/default
equivalences. Unknown historical creators reject retries; known creators without
recorded intent retain resolved-snapshot retry rules. These conflict policies
are local and not verified hosted parity. Creation retries observe future
events without replay; retry with stream=false to retrieve the Session. Non-text
initial input remains unsupported. Basic Codex and Claude SDK openai_hosted
creation requires an explicitly configured managed provider. The Claude workspace
profile supports non-deferred function tools with text results alongside native
workspace tools; HTTP MCP remains unsupported. Idle Sessions provision automatically;
or attached static-bearer service-origin HTTP MCP on none with boolean required
and separately advertised MCP/bearer/required runtime support. Required servers
must be connected before the first native input is released; pending or failed
startup rejects execution. The shared Vault selection and immutable binding
rules apply; unsupported native labels/tool names reject before persistence.
An attached Vault with no matching credential may remain anonymous; missing
keys or failed credential lookup/decryption never fall back to anonymous execution.
Omitted stream defaults to false; stream and agent_id cannot be null. Metadata
may be null, but its values must be strings. Initial input accepts a string
or user-message array containing text. None initial input atomically starts
a Turn; self_hosted initial input is reserved while returning its Environment
connection target, with execution deferred to native readiness and Session
failure on initial timeout. Omitted or null input creates an idle Session.
With stream=true, returns live Session events starting at creation; disconnect
does not cancel execution. New Sessions retain their authenticated creator;
all creation retries require the same typed subject, including across key
rotation. Saved-Agent retries and inline requests using Vault attachments
or credential references retain caller intent independently of later resource
changes; unrelated inline retries preserve resolved/default equivalences.
Unknown historical creators reject retries; known creators without recorded
intent retain resolved-snapshot retry rules. These conflict policies are local
and not verified hosted parity. Creation retries observe future events without
replay; retry with stream=false to retrieve the Session. Non-text initial
input remains unsupported. Basic Codex and Claude SDK openai_hosted creation
requires an explicitly configured managed provider. The Claude workspace profile
supports non-deferred function tools with text results alongside native workspace
tools; HTTP MCP remains unsupported. Idle Sessions provision automatically;
initial provisioning has no caller connection action. Network defaults to
enabled; disabled is also supported, while restricted domains and populated
startup installations are rejected.
Expand Down
13 changes: 11 additions & 2 deletions packages/claude-sdk-adapter/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { MCPProfile } from "./mcp.js";
import { MCPObserver, type MCPEvent } from "./mcp_observer.js";
import { CommandObserver, type CommandEvent } from "./command_observer.js";
import { WorkspaceProfile } from "./workspace.js";
import type { Prepare, Start } from "./request.js";
import { immediatePrompt, type Prepare, type Start } from "./request.js";
import { recoverSession } from "./recovery.js";
export { parseStart, type Start } from "./request.js";

Expand All @@ -29,7 +29,7 @@ export type Event =
| { type: "result"; session_id: string; text: string }
| { type: "error"; code: "invalid_request" | "history_unavailable" | "execution_failed" | "cancelled" };

export async function execute(request: Start | Prepare, emit: (event: Event) => Promise<void>, abort: AbortController, functions = new FunctionBridge(emit), inputs = new Inputs(request.type === "start" ? request.prompt : undefined), reads = new WorkspaceReads(emit, abort), directories = new WorkspaceDirectories(emit, abort)): Promise<void> {
export async function execute(request: Start | Prepare, emit: (event: Event) => Promise<void>, abort: AbortController, functions = new FunctionBridge(emit), inputs = new Inputs(immediatePrompt(request)), reads = new WorkspaceReads(emit, abort), directories = new WorkspaceDirectories(emit, abort)): Promise<void> {
const definitions = (request.functions ?? []).map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.parameters }));
const names = definitions.map(tool => `mcp__functions__${tool.name}`);
const workspace = request.workspace === undefined ? undefined : new WorkspaceProfile(request.cwd, request.workspace, names);
Expand Down Expand Up @@ -101,6 +101,15 @@ export async function execute(request: Start | Prepare, emit: (event: Event) =>
reads.bind(stream, request.cwd);
if (process.platform === "linux") await directories.bind(request.cwd);
await emit({ type: "prepared" });
} else if (profile) {
if (inputs.hasInput) throw new Error("MCP input released before initialization");
warm = await startup({ options, initializeTimeoutMs: 15000 });
stream = warm.query(inputs);
const initialized = await stream.initializationResult();
if (initialized.hooks_applied !== true || children.length !== 1) throw new Error("MCP initialization unavailable");
if (request.mcp_http_servers?.some(server => server.required)) profile.verifyRequired(await stream.mcpServerStatus());
if (abort.signal.aborted || !nativeAlive) throw new Error("MCP initialization interrupted");
inputs.release(request.prompt);
} else stream = query({ prompt: inputs, options });
for await (const message of stream) {
await functions.consume(message, nativeID);
Expand Down
4 changes: 2 additions & 2 deletions packages/claude-sdk-adapter/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Inputs } from "./inputs.js";
import { FunctionBridge } from "./function_bridge.js";
import { createInterface } from "node:readline";
import { execute, type Event } from "./adapter.js";
import { parseRequest, preparedPrompt } from "./request.js";
import { immediatePrompt, parseRequest, preparedPrompt } from "./request.js";

const abort = new AbortController();
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
Expand All @@ -29,7 +29,7 @@ try {
const functions = new FunctionBridge(output);
const reads = new WorkspaceReads(output, abort);
const directories = new WorkspaceDirectories(output, abort);
const prompts = new Inputs(request.type === "start" ? request.prompt : undefined);
const prompts = new Inputs(immediatePrompt(request));
const incoming = (async () => {
try {
for await (const line of { [Symbol.asyncIterator]: () => input }) {
Expand Down
Loading
Loading