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
20 changes: 20 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ databases, credentials and migrations. The product uses Core exclusively; it has
field presence, nullability, discriminators, defaults, status transitions,
pagination, errors and streaming behavior. Engine limitations are implementation
gaps to solve, not grounds for narrowing or redefining the upstream contract.
- Preserve qualified native capability differences across harnesses. If a material
difference from the official API has no clear mapping, pause that part and ask
the user before changing its semantics. Explicit unsupported enablement rejects;
ordinary requests retain native behavior with any official default discrepancy
recorded in the coverage ledger. In particular, native programmatic tool calling
is not currently qualified as the official default-on behavior. Do not build
a separate executor or model loop to fabricate parity. This does not relax
authentication, isolation, credential protection or data consistency.
- Pin upstream source and SDK versions in `contracts/agents-api/upstream.json`.
Use official SDKs for clients and reuse upstream types or schemas where suitable.
SDK deserialization alone is not server validation or proof of compatibility:
Expand Down Expand Up @@ -1531,6 +1539,18 @@ replaced; do not carry obsolete compatibility code forward to satisfy this secti
both valid fields. This internal contract does not add public configuration or
engine support. Future native adapters must verify the same semantics before
advertising the capability.
- Explicit public `programmatic_tool_calling.enabled=false` uses the common
`ExecutionControls.DisableProgrammaticToolCalling` field and the operation-specific
`programmatic_tool_calling_disable` capability. Both public qualification and
Runtime support are required for that request; omission creates no prerequisite.
New and resumed executions retain the frozen setting. Codex disables native
code-mode features and checks managed requirements before starting/resuming a
thread, rejecting a conflicting requirement. Claude and MiniMax retain their
restricted native inventories, which exclude programmatic execution. This does
not remove unrelated native utilities or claim enabled programmatic support.
Explicit `web_search.mode=disabled` reuses the existing disabled search control.
Search remains off when omitted. Optional search settings are resource data and
do not cause execution while disabled. Enabled search remains unqualified here.
- `web_search_control` advertises the Codex adapter's explicit `web_search` option
(`disabled`, `cached`, or `live`). Agents API requires this capability before Codex dispatch;
the typed execution controls force search off on new and resumed Turns. Native configuration translation stays in the
Expand Down
5 changes: 5 additions & 0 deletions apps/parsar-daemon/internal/agent/codex/preparation.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ func newPreparation(parent context.Context, req proto.PromptRequestPayload, cfg
if _, err := rpc.Start(cancelCtx, initParams); err != nil {
return p.preparationFailed(fmt.Errorf("codex: rpc start: %w", err))
}
if req.ExecutionControls != nil && req.ExecutionControls.DisableProgrammaticToolCalling {
if err := verifyProgrammaticToolsDisabled(cancelCtx, rpc); err != nil {
return p.preparationFailed(err)
}
}
if req.DisableExecutionEnvironment {
if err := verifyNoExecutionEnvironment(cancelCtx, rpc); err != nil {
cancelFn()
Expand Down
51 changes: 51 additions & 0 deletions apps/parsar-daemon/internal/agent/codex/programmatic_tools.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package codex

import (
"context"
"encoding/json"
"errors"
"slices"

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

var programmaticFeatures = []string{"code_mode", "code_mode_only", "code_mode_prewarm"}

func disableProgrammaticTools(plan *SessionPlan, controls *proto.ExecutionControls) {
if controls == nil || !controls.DisableProgrammaticToolCalling {
return
}
for _, feature := range programmaticFeatures {
plan.EnableFeatures = slices.DeleteFunc(plan.EnableFeatures, func(value string) bool { return value == feature })
if !slices.Contains(plan.DisableFeatures, feature) {
plan.DisableFeatures = append(plan.DisableFeatures, feature)
}
plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"features." + feature, "false"})
}
}

// Managed native requirements can override command-line feature settings.
// Check before starting or resuming a native Session, not after model execution.
func verifyProgrammaticToolsDisabled(ctx context.Context, rpc *JSONRPCClient) error {
raw, err := rpc.Request(ctx, "configRequirements/read", nil)
var response struct {
Requirements json.RawMessage `json:"requirements"`
}
if err != nil || json.Unmarshal(raw, &response) != nil || len(response.Requirements) == 0 {
return errors.New("codex: programmatic tool requirements unavailable")
}
if string(response.Requirements) != "null" {
var requirements struct {
Features map[string]bool `json:"featureRequirements"`
}
if json.Unmarshal(response.Requirements, &requirements) != nil {
return errors.New("codex: invalid programmatic tool requirements")
}
for _, feature := range programmaticFeatures {
if requirements.Features[feature] {
return errors.New("codex: required programmatic tools conflict with explicit disabling")
}
}
}
return nil
}
75 changes: 75 additions & 0 deletions apps/parsar-daemon/internal/agent/codex/programmatic_tools_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package codex

import (
"context"
"encoding/json"
"reflect"
"slices"
"testing"
"time"

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

func TestProgrammaticToolsExplicitDisableOverridesNativeOptions(t *testing.T) {
plan := SessionPlan{EnableFeatures: []string{"code_mode", "unrelated", "code_mode_only", "code_mode_prewarm"}, ExtraConfig: [][2]string{{"features.code_mode", "true"}}}
before := slices.Clone(plan.EnableFeatures)
disableProgrammaticTools(&plan, nil)
if !reflect.DeepEqual(plan.EnableFeatures, before) {
t.Fatal("omission changed native configuration")
}
disableProgrammaticTools(&plan, &proto.ExecutionControls{DisableProgrammaticToolCalling: true})
if !slices.Equal(plan.EnableFeatures, []string{"unrelated"}) {
t.Fatal(plan.EnableFeatures)
}
for _, feature := range programmaticFeatures {
if !slices.Contains(plan.DisableFeatures, feature) {
t.Fatal("missing disable", feature)
}
value := ""
for _, config := range plan.ExtraConfig {
if config[0] == "features."+feature {
value = config[1]
}
}
if value != "false" {
t.Fatal("final override missing", feature)
}
}
}

func TestProgrammaticToolsRejectManagedOverridesBeforeExecution(t *testing.T) {
for _, tc := range []struct {
response string
accepted bool
}{
{`{"requirements":null}`, true},
{`{"requirements":{"featureRequirements":{"code_mode":false,"unrelated":true}}}`, true},
{`{"requirements":{"featureRequirements":{"code_mode":true}}}`, false},
{`{"requirements":{"featureRequirements":{"code_mode_only":true}}}`, false},
{`{"requirements":{"featureRequirements":{"code_mode_prewarm":true}}}`, false},
{`{}`, false}, {`null`, false}, {`{"requirements":[]}`, false},
} {
t.Run(tc.response, func(t *testing.T) {
client, server, cleanup := NewTestClient()
defer cleanup()
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
defer cancel()
done := make(chan error, 1)
go func() { done <- verifyProgrammaticToolsDisabled(ctx, client.JSONRPCClient) }()
var request struct{ ID, Method string }
if err := json.NewDecoder(server.FromClient).Decode(&request); err != nil {
t.Fatal(err)
}
if request.Method != "configRequirements/read" {
t.Fatal(request.Method)
}
if err := json.NewEncoder(server.ToClient).Encode(map[string]any{"id": request.ID, "result": json.RawMessage(tc.response)}); err != nil {
t.Fatal(err)
}
if err := <-done; (err == nil) != tc.accepted {
t.Fatal(err)
}
})
}
}
1 change: 1 addition & 0 deletions apps/parsar-daemon/internal/agent/codex/session_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func prepareSessionPlan(ctx context.Context, req proto.PromptRequestPayload, cfg
plan.Cleanup()
return SessionPlan{}, nil, err
}
disableProgrammaticTools(&plan, req.ExecutionControls)
if profile != "" {
plan.Sandbox = ""
plan.Permissions = profile
Expand Down
41 changes: 21 additions & 20 deletions apps/parsar-daemon/internal/cli/agent_discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,26 +81,27 @@ func discoverAgentCLIs(rc *runContext, profile string, checks agentCLIChecks) (a
Codex: proto.SupportedAgentKind{
Kind: "codex",
Capabilities: proto.AgentKindCapabilities{
Streaming: true,
Permissions: true,
Usage: true,
Resume: true,
Steering: true,
DurableTurns: true,
DurableInputReceipts: true,
MessageImages: true,
FunctionTools: true,
MCPHTTPTools: true,
MCPHTTPBearerAuth: true,
MessageItems: true,
ToolItems: true,
ToolObservations: true,
EnvironmentNone: true,
WebSearchControl: true,
TextVerbosity: codex.SupportsTextVerbosity,
ExecutionControls: codex.SupportsTextVerbosity,
SubagentControl: true,
SubagentObservations: true,
Streaming: true,
Permissions: true,
Usage: true,
Resume: true,
Steering: true,
DurableTurns: true,
DurableInputReceipts: true,
MessageImages: true,
FunctionTools: true,
MCPHTTPTools: true,
MCPHTTPBearerAuth: true,
MessageItems: true,
ToolItems: true,
ToolObservations: true,
EnvironmentNone: true,
WebSearchControl: true,
ProgrammaticToolCallingDisable: true,
TextVerbosity: codex.SupportsTextVerbosity,
ExecutionControls: codex.SupportsTextVerbosity,
SubagentControl: true,
SubagentObservations: true,
},
},
Pi: proto.SupportedAgentKind{
Expand Down
1 change: 1 addition & 0 deletions apps/parsar-daemon/internal/cli/claude_sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func discoverClaudeSDK(rc *runContext, profile string, check func(context.Contex
Streaming: true, Usage: true, Resume: true, Steering: true, MessageItems: true,
ToolObservations: true, EnvironmentNone: true, SubagentControl: true,
DurableTurns: true, DurableInputReceipts: true, FunctionTools: true, ExecutionControls: true,
ProgrammaticToolCallingDisable: true,
}}}
fail := func(err error) *claudeSDKDiscovery {
fmt.Fprintf(rc.stderr, "parsar-daemon: configured Claude SDK runtime unavailable: %v\n", err)
Expand Down
1 change: 1 addition & 0 deletions apps/parsar-daemon/internal/cli/mcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func discoverMCode(rc *runContext, check func(context.Context, string) (string,
result.Capabilities.DurableTurns = true
result.Capabilities.DurableInputReceipts = true
result.Capabilities.ExecutionControls = true
result.Capabilities.ProgrammaticToolCallingDisable = true
result.Capabilities.ToolObservations = true
result.Capabilities.SubagentControl = true
// Native preparation verifies the applied admission/tool profile before input.
Expand Down
3 changes: 3 additions & 0 deletions apps/parsar-daemon/internal/dispatch/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
)

func validateExecutionEnvironment(req proto.PromptRequestPayload, caps proto.AgentKindCapabilities) error {
if err := req.ValidateProgrammaticToolCallingDisable(caps.ProgrammaticToolCallingDisable); err != nil {
return err
}
if err := req.ValidateToolSearch(caps.ToolSearch); err != nil {
return err
}
Expand Down
16 changes: 15 additions & 1 deletion contracts/agents-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,20 @@ including further deployment qualification; this inventory describes merged beha
multi-agent settings default to six concurrent subagents. Function defer-loading
defaults to false and programmatic tool calling to true. Saving these values
does not itself admit a native execution. Session references are admitted separately.
- [Explicit disabled tools](tool-policy.md) can be saved, used inline or resolved from saved Agents:
`web_search.mode=disabled` and `programmatic_tool_calling.enabled=false`. Search
responses include `context_size=medium` for omitted/null size, nullable domains
and location; an empty domain list stays empty. Only explicit disabled mode is
qualified; omitted/null mode and enabled search remain gaps. Sessions reject
enabled programmatic execution, including the default true on a supplied PTC
declaration. An omitted PTC declaration preserves native behavior: this is an
approved difference from the official default-on behavior, not full compatibility.
Core carries the frozen disabled intent through the common Runtime contract;
native translation and inventory restrictions stay in adapters. Codex checks
managed requirements before new/resumed execution; conflicting forced features
reject before model input. Claude and MiniMax use their restricted tool profiles.
No independent executor or model/tool loop is introduced. Resource defaults and
hosted error parity beyond this supported subset remain unverified.
- `POST /agents/{agent_id}` updates only supplied fields. Omitted fields remain
unchanged; metadata replaces all pairs and null/empty clears it. Name/instructions
null clears them. Concurrent updates preserve unrelated fields. Existing Session
Expand Down Expand Up @@ -304,7 +318,7 @@ including further deployment qualification; this inventory describes merged beha
rechecked before dispatch-only decryption; authenticated execution requires the
separate bearer capability and never downgrades on failure. Exact URL/selection
timing, implicit response population and hosted errors remain local or unverified.
Other MCP variants and web-search remain gaps, not changes to the pinned target
Other MCP variants and enabled web-search remain gaps, not changes to the pinned target
or claims of complete resource coverage.

- Use `/agents/sessions` beneath the configured API base URL, bearer authentication
Expand Down
13 changes: 9 additions & 4 deletions contracts/agents-api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1937,8 +1937,10 @@ paths:
required defaulting to false. Saving credential_id grants no access: Session
admission checks attached Vault ownership and destination. MCP allowed_tools
preserves null versus empty; saved HTTP transport includes empty headers.
Model-derived reasoning defaults, other MCP variants, web_search and public
retry conformance remain incomplete. Session execution admits only its supported
Model-derived reasoning defaults, other MCP variants, enabled web_search and
public retry conformance remain incomplete. Explicit disabled web_search can
be saved; Session execution also accepts explicit disabled programmatic_tool_calling
through qualified Runtime controls. Session execution admits only its supported
configuration subset.'
parameters:
- description: agents=v1
Expand Down Expand Up @@ -2723,8 +2725,11 @@ paths:
updates cannot change committed Session Skill contents. Deferred function
discovery uses type-only tool_search and per-function defer_loading in the
qualified single-agent Claude environment:none function profile, including
qualified inline image messages and text results. Other combinations remain
unqualified; see the operation coverage.
qualified inline image messages and text results. Explicit web_search mode
disabled and programmatic_tool_calling enabled false use frozen common Runtime
controls. Enabled forms remain unqualified. Omitted programmatic configuration
preserves native behavior, a documented difference from the official default-on
behavior. Other combinations remain unqualified; see the operation coverage.
parameters:
- description: agents=v1
in: header
Expand Down
70 changes: 70 additions & 0 deletions contracts/agents-api/tool-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Explicit disabled tools

Protocol baseline: `upstream.json` (OpenAI Python SDK 3.13.0). This covers a
bounded execution profile, not complete tool or Agents API compatibility.

## Public behavior

Saved Agents and inline Session configuration accept these declarations:

```json
[
{"type": "web_search", "mode": "disabled"},
{"type": "programmatic_tool_calling", "enabled": false}
]
```

Saved references use the same parser and immutable Session snapshot. Disabled
search retains optional settings as resource data: omitted/null context size
resolves to `medium`; domain and location omission resolves to null; an empty
domain list remains empty. These settings cannot enable execution while disabled.
Web-search mode omission/null, enabled search and exact hosted errors remain
unqualified. Explicit enabled programmatic execution rejects at Session admission;
saving that intent remains separate from execution qualification.

Omitting programmatic configuration preserves each harness's native behavior.
The user approved this difference from the official default-on behavior. Native
feature differences stay in adapters; Core does not supply another executor or
model loop. Unrelated native utility tools are not implicitly removed.

## Runtime boundary

The common `DisableProgrammaticToolCalling` control carries explicit disabled
intent on initial execution and cold continuation. Public qualification and the
operation-specific Runtime capability must both permit this request. Omission
does not require the new capability. Search uses the existing disabled control.

| Adapter | Native enforcement |
| --- | --- |
| Codex | Disable code-mode features; check native managed requirements before thread start/resume and reject a forced conflicting feature. |
| Claude Code | Retain the restricted built-in inventory and verify native initialization against that inventory. |
| MiniMax Code | Retain the protected native tool profile, empty text-execution inventory and disabled web-search feature. |

## Acceptance

The opt-in `TestNativeToolPolicyPublicExecution` fixture and
`services/agents-api/tests/official_tool_policy.py` exercise a real PostgreSQL
database, independent API, Docker daemon and native harness using the pinned
official SDK with strict response validation and raw HTTP. Supply the existing
native-test environment variables plus `PARSAR_TOOL_POLICY_ENGINE` and a private
`PARSAR_TOOL_POLICY_REAL_OPTIONS` file. Each concurrent execution worker requires
its own dedicated test database.

On 2026-09-22, Codex and Claude with Kimi K3, and MiniMax Code with MiniMax-M2.7,
passed SDK/raw HTTP through both saved and inline configurations. Each of four
Sessions completed a first Turn and continued its random marker after a full
daemon restart with the same native Session. Checks include public configuration,
SSE order, Items, native input receipts, unsupported enablement without Session
persistence, omitted configuration admission and tenant isolation.

Native evidence separately records Codex's disabled feature arguments and search
setting, Claude's empty built-in tool argument and initialization validation, and
MiniMax's restricted native configuration. Successful model text alone does not
prove disabled tools. Controlled tests cover managed requirement conflicts,
invalid fields and the common qualification contract.

Evidence is retained privately under `~/.parsar/remediation/20260922/tool-policy`
on the development host and test server. Live qualification in this batch uses
`environment:none`; workspace provisioning, provider matrices, enabled tools,
native question extensions and complete hosted default/error semantics were not
requalified. Existing workspace mechanisms are unchanged.
Loading
Loading