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
25 changes: 22 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2417,8 +2417,8 @@ replaced; do not carry obsolete compatibility code forward to satisfy this secti
creation with `model_provider_required`; legacy rows without a snapshot are
rejected at message admission and dispatch, never sent to a harness that would
fall back to a built-in endpoint. There is no operator options file: its
retirement fails startup, and historical snapshots keep their frozen native
options. Keep runtime dispatch on the common adapter path and fail closed for
retirement fails startup. Sessions retain only the common frozen provider
bundle; historical native-option snapshots are unsupported. Keep runtime dispatch on the common adapter path and fail closed for
missing/decryption-failed snapshots. Agent edits/deletion, default changes,
restart and idle suspend/resume never resolve defaults again. Record caller
intent for every new hosted Session before resolving defaults; other inline
Expand All @@ -2437,6 +2437,25 @@ replaced; do not carry obsolete compatibility code forward to satisfy this secti
identity; retries cannot replace it. Keep this administrator query separate from
runtime observations and do not touch activity or wake sandboxes. The versioned
contract is `contracts/agents-api/execution-configuration.md`.
- Model communication uses `internal/modeltransport` in the Runtime. Core sends one
frozen confidential `model_provider` bundle, independent of engine and placement;
it must not manufacture native provider options or retain a historical-options
dispatch path. The adapter declares native protocols. Native matches connect
directly; mismatches use one private loopback endpoint owned by the existing
Session execution resource. Its credential is distinct from the upstream key.
Keep endpoint cleanup on final native teardown, including preparation failure,
rather than on individual Turn completion. No second Session manager is added.
CLIProxyAPI's pinned translator is an embedded conversion dependency, not a
gateway service. Requests, JSON responses and incremental SSE events share the
same conversion implementation for hosted and self-hosted execution. The
dependency owns protocol fields, tools, reasoning and usage conversion. Do not
add local field maps, parameter restoration or parallel compatibility rules.
Keep SDK format selection, HTTP/SSE framing, limits, cancellation and resource
cleanup here. Pin dependency versions and qualify upgrades with contract and
native engine tests; document upstream limitations instead of silently promising
lossless conversion. Preserve model identity, reject incomplete streams and
never redirect upstream credentials. See `contracts/agents-api/model-protocol-conversion.md` for coverage,
limits and dependency qualification. Go 1.26.8 is the pinned build toolchain.
- Provider input validation uses the adapter-owned rules in `internal/harnessconfig`.
Keep one internal registry for protocol and token-limit validation; Core owns
credential environment and endpoint admission policy. These rules are not a
Expand Down Expand Up @@ -3473,7 +3492,7 @@ Do not bypass them by dropping fields, changing model identity or fabricating us
Operators may configure the daemon provider environment or the deployment default
model provider for `claude_sdk` (HTTPS `base_url` and a write-only key), which Core
freezes in the Session's encrypted snapshot and delivers as the adapter-owned
`claude_provider`; it never enters public Session configuration. The adapter exclusively selects the
`model_provider`; it never enters public 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 `none` public profile accepts only
text, explicit model/system instructions, managed state, exact native resume and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ _openapi:
Core key only. The body is the complete x_agents_core.model_provider
bundle, including the write-only api_key; there is no partial update
and bundles are never merged. The provider is validated for this
harness: an HTTPS base_url without credentials, query or fragment, the
harness's protocol (responses for codex, anthropic for claude_sdk and
mcode) and, for mcode, positive context_window and max_output_tokens.
New openai_hosted and none Sessions that resolve no Session or Agent
harness: an HTTPS base_url without credentials, query or fragment, an
upstream protocol (responses, anthropic or chat_completions),
automatically adapted by Runtime to the selected harness and, for
mcode, positive context_window and max_output_tokens. New
openai_hosted and none Sessions that resolve no Session or Agent
bundle freeze this default into their encrypted snapshot; existing
Sessions never change. self_hosted Sessions never use it. The key is
encrypted and never returned. Each write records an administrator
Expand Down
4 changes: 3 additions & 1 deletion apps/docs/openapi/core-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ paths:
tags:
- Deployment Model Providers
put:
description: 'Core key only. The body is the complete x_agents_core.model_provider bundle, including the write-only api_key; there is no partial update and bundles are never merged. The provider is validated for this harness: an HTTPS base_url without credentials, query or fragment, the harness''s protocol (responses for codex, anthropic for claude_sdk and mcode) and, for mcode, positive context_window and max_output_tokens. New openai_hosted and none Sessions that resolve no Session or Agent bundle freeze this default into their encrypted snapshot; existing Sessions never change. self_hosted Sessions never use it. The key is encrypted and never returned. Each write records an administrator audit entry without the key.'
description: 'Core key only. The body is the complete x_agents_core.model_provider bundle, including the write-only api_key; there is no partial update and bundles are never merged. The provider is validated for this harness: an HTTPS base_url without credentials, query or fragment, an upstream protocol (responses, anthropic or chat_completions), automatically adapted by Runtime to the selected harness and, for mcode, positive context_window and max_output_tokens. New openai_hosted and none Sessions that resolve no Session or Agent bundle freeze this default into their encrypted snapshot; existing Sessions never change. self_hosted Sessions never use it. The key is encrypted and never returned. Each write records an administrator audit entry without the key.'
parameters:
- schema:
type: string
Expand Down Expand Up @@ -5677,6 +5677,7 @@ components:
enum:
- anthropic
- responses
- chat_completions
type: string
required:
- api_key
Expand All @@ -5697,6 +5698,7 @@ components:
enum:
- anthropic
- responses
- chat_completions
type: string
required:
- api_key_configured
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/openapi/public-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4397,6 +4397,7 @@ components:
enum:
- anthropic
- responses
- chat_completions
type: string
required:
- api_key
Expand All @@ -4417,6 +4418,7 @@ components:
enum:
- anthropic
- responses
- chat_completions
type: string
required:
- api_key_configured
Expand Down
10 changes: 5 additions & 5 deletions apps/docs/openapi/sources.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"sources": {
"contracts/agents-api/openapi.yaml": "a8896e8b67d78ed0608c7cc5636077ec33f4b4895a04bd6cfb8b038afe941e01",
"contracts/agents-api/core.openapi.yaml": "488a50fa308194224ee5ee879b7f6e38166d1b08dedc2bbc5bb39f38a844c87f",
"contracts/agents-api/openapi.yaml": "433a86baa0117791900f4d4c46490400b2fd00b0ca09eda1186a7b134aef24d7",
"contracts/agents-api/core.openapi.yaml": "5d6d13fdafa55c8459a61004aca6317c1d45de244df75dbfd83a589e813f2a6d",
"contracts/agents-api/runtime.openapi.yaml": "b3fb025b20a0bcf4635d60339479b2b07f6dbed80f51e72f40a63b31e42b5f4a"
},
"outputs": {
Expand All @@ -18,11 +18,11 @@
"content/docs/api-reference/skills.mdx": "774ea05a7a8250fe5af5164388b98dafca99d56dc2eae686e8f628acb4e7c10b",
"content/docs/api-reference/vaults.mdx": "d80eade1c2f8100030866abbb0fb6b7a09c94250a859c97cdf1abce32fdfdf1e",
"content/docs/api-reference/credentials.mdx": "f129a51f74a3c7021c016600518e2350023f8d29571666ac5d64b7584cd7c7d0",
"openapi/public-api.yaml": "84ae8a33717c817950d757990018edd10a4d31d5a343c8172baa099d101c5a1f",
"openapi/public-api.yaml": "54759eb63257a9b4ca791f6cabcae4b9bf3cff791071f599f525cd76bb41f385",
"content/docs/api-reference/meta.json": "56bf1dc145ccc8adde38c5f956f1a06f18ef18b646122250d01a294c516f8f83",
"content/docs/api-reference/index.mdx": "9fd6b1c54149874d2999235067c101326f9e8a2e707a70b6672c81eb3ebfe63f",
"content/docs/api-reference/core/core-administration.mdx": "a1261c7998017de3761ca90ca073a0b89a6c528ff37a634f3d8b986ee9540fc5",
"content/docs/api-reference/core/deployment-model-providers.mdx": "b2795fadcc05a1e7195b6d34c3f234cb29cc5e3f580d1521e31965453e654347",
"content/docs/api-reference/core/deployment-model-providers.mdx": "be4582d72ad10b225cb80c27cc8df2396e28fb2b5e857f7f0e20353a4425cd13",
"content/docs/api-reference/core/administrator-projects.mdx": "8fd687bb2a7119fa16e938d7c71710c2816afb9cf959eb6849ef5e6300c5419b",
"content/docs/api-reference/core/agents.mdx": "b6c089fbea4b07bc4249e7c7ca5699d47f1512a4fc2d8ee01cb26910ae541a84",
"content/docs/api-reference/core/environment-templates.mdx": "05907f35d7f02d66af9da8b1efaa3765badb4cdf7b94d0f6c3ab0413a2cfa1a9",
Expand All @@ -40,7 +40,7 @@
"content/docs/api-reference/core/vaults.mdx": "12d0aac96cd4a3472ef4eaa287f7945f78862017956e79bfc3ed0ef8e772e78a",
"content/docs/api-reference/core/credentials.mdx": "27d586b9a8fd738b9947802f32c80458195370252886bc26abedccf2125ee981",
"content/docs/api-reference/core/sandbox-manager.mdx": "36f2895b9969d27be843d5cebf935bf14ca9400ad076e56074a25905e3df5d1b",
"openapi/core-api.yaml": "9872f80cec2661c8539b6652a294c16c668f581399afd20f76b198279faae9ed",
"openapi/core-api.yaml": "fd969e19b1f8f310abafa577ed3228a67ba5878790fe80de6d5035593684192c",
"content/docs/api-reference/core/meta.json": "588b163ed2de2100b0f8376688274f80698a6bf6ac037eef56d17c93fa8a5062",
"content/docs/api-reference/core/index.mdx": "ddf0a8d87a6c21761beac703a32f2a40486a1c74abb341bd726f40c44a866405",
"content/docs/api-reference/machine/sandbox-node.mdx": "5b367a08d43cea82686345d2ba1c75cc90627a7b2970fe42a0df9d77fb3e1e80",
Expand Down
35 changes: 26 additions & 9 deletions apps/parsar-daemon/internal/agent/claudesdk/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@ import (

"github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent"
"github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto"
"github.com/MiniMax-AI-Dev/parsar/internal/modeltransport"
)

type executor struct {
mu sync.Mutex
base *session
start startRequest
active *session
ready chan error
done chan struct{}
invalid bool
nativeID string
modelEndpoint *modeltransport.Endpoint
mu sync.Mutex
base *session
start startRequest
active *session
ready chan error
done chan struct{}
invalid bool
nativeID string
}

func NewExecutorFactory(config Config) agent.ExecutorFactory {
Expand All @@ -40,6 +42,17 @@ func NewExecutorFactory(config Config) agent.ExecutorFactory {
if req.RunID != "" || len(req.Input) != 0 || req.ConversationID != "" {
return nil, errors.New("claudesdk: Executor preparation cannot submit input")
}
options, endpoint, err := modeltransport.PrepareOptions(req.AgentOptions, modeltransport.Anthropic)
if err != nil {
return nil, err
}
transferred := false
defer func() {
if !transferred && endpoint != nil {
_ = endpoint.Close()
}
}()
req.AgentOptions = options
start, env, err := prepareConfiguration(config, req)
if err != nil {
return nil, err
Expand All @@ -58,7 +71,8 @@ func NewExecutorFactory(config Config) agent.ExecutorFactory {
}
base.reads.supported = slices.Contains(info.Features, "workspace_read")
base.directories.supported = slices.Contains(info.Features, "workspace_directory")
e := &executor{base: base, start: start, ready: make(chan error, 1), done: make(chan struct{}), nativeID: start.Resume}
e := &executor{modelEndpoint: endpoint, base: base, start: start, ready: make(chan error, 1), done: make(chan struct{}), nativeID: start.Resume}
transferred = true
go e.read()
if err = e.write(start); err == nil {
select {
Expand Down Expand Up @@ -177,6 +191,9 @@ func (e *executor) read() {
if !ready {
e.ready <- readyFailure
}
if e.modelEndpoint != nil {
_ = e.modelEndpoint.Close()
}
close(e.done)
}

Expand Down
31 changes: 31 additions & 0 deletions apps/parsar-daemon/internal/agent/claudesdk/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"bufio"
"context"
"encoding/json"
"net/http"
"os"
"path/filepath"
"strconv"
Expand Down Expand Up @@ -144,14 +145,35 @@ func awaitExecutorTurn(t *testing.T, turn agent.Turn, out <-chan proto.Envelope,
}
func TestExecutorRetainsProcessAcrossTurnsAndCancellation(t *testing.T) {
config, req := persistentConfig(t, "")
req.AgentOptions["model_provider"] = map[string]any{
"protocol": "chat_completions", "base_url": "https://provider.example/v1", "api_key": "fixture-key",
}
owner, err := NewExecutorFactory(config)(t.Context(), req)
if err != nil {
t.Fatal(err)
}
defer owner.Close(context.Background())
pid := owner.(*executor).base.process.Cmd.Process.Pid
endpoint := owner.(*executor).modelEndpoint
if endpoint == nil {
t.Fatal("missing model conversion endpoint")
}
client := &http.Client{Timeout: time.Second}
checkEndpoint := func() {
t.Helper()
response, err := client.Get(endpoint.BaseURL + "/v1/messages")
if err != nil {
t.Fatal("model endpoint closed before Executor", err)
}
response.Body.Close()
if response.StatusCode != http.StatusUnauthorized {
t.Fatal("model endpoint lost its credential boundary")
}
}
checkEndpoint()
first, out := consumeExecutorTurn(t, owner, "first", "hello")
awaitExecutorTurn(t, first, out, true)
checkEndpoint()
next, out := consumeExecutorTurn(t, owner, "next", "wait")
if event := <-out; event.Type != proto.TypeDelta {
t.Fatal(event.Type)
Expand All @@ -163,6 +185,7 @@ func TestExecutorRetainsProcessAcrossTurnsAndCancellation(t *testing.T) {
t.Fatal(err)
}
awaitExecutorTurn(t, next, out, true)
checkEndpoint()
third, out := consumeExecutorTurn(t, owner, "third", "hello")
awaitExecutorTurn(t, third, out, true)
if owner.(*executor).base.process.Cmd.Process.Pid != pid {
Expand All @@ -173,6 +196,14 @@ func TestExecutorRetainsProcessAcrossTurnsAndCancellation(t *testing.T) {
t.Fatal("native process exited between Turns")
default:
}
checkEndpoint()
if err := owner.Close(t.Context()); err != nil {
t.Fatal(err)
}
if response, err := client.Get(endpoint.BaseURL + "/v1/messages"); err == nil {
response.Body.Close()
t.Fatal("model endpoint survived Executor.Close")
}
}
func TestExecutorLateTurnEventInvalidatesWithoutRetargeting(t *testing.T) {
config, req := persistentConfig(t, "late")
Expand Down
6 changes: 3 additions & 3 deletions apps/parsar-daemon/internal/agent/claudesdk/local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func TestWorkspaceProviderCredentialsReplaceAmbientSelection(t *testing.T) {
config := workspaceFixture(t)
original := slices.Clone(config.Env)
req := workspaceRequest()
req.AgentOptions["claude_provider"] = map[string]any{"base_url": "https://provider.example/anthropic", "bearer_token": "selected-secret"}
req.AgentOptions["model_provider"] = map[string]any{"protocol": "anthropic", "base_url": "https://provider.example/anthropic", "api_key": "selected-secret"}
start, env, err := prepare(config, req)
if err != nil {
t.Fatal(err)
Expand All @@ -75,8 +75,8 @@ func TestWorkspaceProviderCredentialsReplaceAmbientSelection(t *testing.T) {
if !slices.Contains(env, "ANTHROPIC_AUTH_TOKEN=selected-secret") || slices.Contains(env, "ANTHROPIC_AUTH_TOKEN=selected-provider-fixture") {
t.Fatal("provider selection was not exclusive")
}
for _, value := range []any{nil, "secret", map[string]any{"base_url": "http://provider.example", "bearer_token": "secret"}, map[string]any{"base_url": "https://user:pass@provider.example", "bearer_token": "secret"}} {
req.AgentOptions["claude_provider"] = value
for _, value := range []any{nil, "secret", map[string]any{"protocol": "anthropic", "base_url": "http://provider.example", "api_key": "secret"}, map[string]any{"protocol": "anthropic", "base_url": "https://user:pass@provider.example", "api_key": "secret"}} {
req.AgentOptions["model_provider"] = value
if _, _, err := prepare(config, req); err == nil || strings.Contains(err.Error(), "secret") {
t.Fatal("unsafe provider accepted or disclosed")
}
Expand Down
2 changes: 1 addition & 1 deletion apps/parsar-daemon/internal/agent/claudesdk/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startR
}
var provider []string
for name, raw := range req.AgentOptions {
if name == "claude_provider" {
if name == "model_provider" {
var err error
provider, err = providerEnvironment(raw)
if err != nil {
Expand Down
29 changes: 8 additions & 21 deletions apps/parsar-daemon/internal/agent/claudesdk/provider.go
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
package claudesdk

import (
"fmt"
"net/url"
"strings"

"github.com/MiniMax-AI-Dev/parsar/internal/modeltransport"
)

// Provider options are transient operator input, never public Agent fields or
// native tool environment. The workspace sandbox removes these variables.
// The factory prepares the native endpoint before producing launch variables.
// Upstream credentials remain in Runtime when translation is required.
func providerEnvironment(value any) ([]string, error) {
fail := func() ([]string, error) { return nil, fmt.Errorf("claudesdk: invalid provider configuration") }
options, ok := value.(map[string]any)
if !ok || len(options) != 2 {
return fail()
}
base, ok := options["base_url"].(string)
if !ok {
return fail()
}
token, ok := options["bearer_token"].(string)
if !ok || strings.TrimSpace(token) == "" || strings.ContainsAny(token, "\x00\r\n") {
return fail()
}
u, err := url.Parse(base)
if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
return fail()
provider, err := modeltransport.ParseProvider(value)
if err != nil || provider.Protocol != modeltransport.Anthropic {
return nil, modeltransport.ErrConfiguration
}
return []string{"ANTHROPIC_BASE_URL=" + base, "ANTHROPIC_AUTH_TOKEN=" + token}, nil
return []string{"ANTHROPIC_BASE_URL=" + provider.BaseURL, "ANTHROPIC_AUTH_TOKEN=" + provider.APIKey}, nil
}

func withProvider(env, provider []string) []string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestExecutorNativeReuse(t *testing.T) {
req := proto.PromptRequestPayload{
AgentKind: "codex", AgentStateKey: "executor-native", WorkDir: filepath.Join(isolated, "workspace"),
StrictResume: true, DisableExecutionEnvironment: true, DisableSubagents: true, ObserveMessages: true,
AgentOptions: map[string]any{"model": model, "codex_provider": map[string]any{"base_url": endpoint, "wire_api": "responses", "bearer_token": strings.TrimSpace(string(key))}},
AgentOptions: map[string]any{"model": model, "model_provider": map[string]any{"base_url": endpoint, "protocol": "responses", "api_key": strings.TrimSpace(string(key))}},
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)
Expand Down
Loading
Loading