Compare commits

..
Author SHA1 Message Date
Vasanthdev2004 6778eb5078 fix(tui): only drop an explicit effort when the target ring is authoritative
A nil ring meant both a catalog model with no effort controls and a
live-discovered custom model the catalog cannot vouch for. The drop now
requires an authoritative ring (a resolved catalog entry, whose ring may
legitimately be empty); an unknown ring preserves an explicit preference,
matching the cross-provider picker's behavior, while the profile's own
fill still applies only where support is known.
2026-07-19 00:42:46 +05:30
Vasanthdev2004 86c2c03706 fix(tui): void profile effort bookkeeping when the model-switch drop erases it
The /model path's pre-existing unsupported-effort drop ran before the
profile reconciliation and left contradictory state behind: a dropped
touched override kept blocking the profile from governing, and a
dropped profile fill left a stale applied value with the effort restore
still armed. The drop itself is load-bearing (the TUI run path forwards
the effort to the provider unfiltered), so instead of preserving a
known-unsupported level, the voided choice now resets the profile's
effort bookkeeping and the reconcile re-derives from scratch. The whole
sequence lives in one ring-parameterized helper driven by the switch
target's authoritative ring, unit-tested against synthetic rings for
both the void-then-refill arc and the supported-override survival.
2026-07-19 00:35:26 +05:30
Vasanthdev2004 b621b5bf2b fix(tui): reconcile an active profile's effort fill on model switch
The fill is per-model, not a session preference: fast selected on a
model without low fills nothing, and switching to a model that supports
it should behave like selecting the profile there; the reverse switch
must not keep sending a level the destination does not support. Both
switch paths (the /model command and the cross-provider picker) now
re-derive the fill, leaving explicitly touched efforts alone and
keeping the escalation's effort restore in step. Also: the revert-env
test asserts key removal via LookupEnv, and applyProfileTurnBudget's
pinned-budget parameter is named and documented as covering mode
presets, not just the flag.
2026-07-19 00:20:07 +05:30
Vasanthdev2004 1c56a40a6e fix(execprofile): address review findings on the profile surfaces
- --use-spec projects away a profile's self-correct knob instead of
  silently arming an option the draft runner ignores (the explicit
  --self-correct --use-spec combination is already a parse-time error)
- RunTurnBench canonicalizes and validates ExecProfile at the library
  boundary, so a direct caller's casing/whitespace cannot fork report
  labels and an unknown name fails before anything runs
- reverting a profile whose displaced budget was zero clears
  ZERO_MAX_TURNS (SetMaxTurnsEnv ignores zero, so sub-agents would have
  kept the removed profile's budget)
- the TUI effort-disarm test fails instead of skipping when the catalog
  stops filling low effort
2026-07-19 00:09:24 +05:30
Vasanthdev2004 d6688ab97b fix(execprofile): explicit effort choices disarm the escalation effort restore
An explicit effort set AFTER profile selection could be cleared by a
mid-run escalation (the effort analog of the /turns pin, introduced by
RestoreDefaultEffort): TUI /effort and Ctrl+T now disarm the restore on
a policy clone, and the spec-draft path only arms it when no explicit
--spec-reasoning-effort governs the draft.
2026-07-18 23:58:24 +05:30
Vasanthdev2004 3e96a581ca fix(execprofile): harden profiles against review findings
- fast escalates on critical-risk mutations, not high: the sandbox marks
  every shell command high before command analysis, so a high trigger
  spent the one-shot on the first go test of any task
- escalation can now restore the provider-default effort a profile
  displaced (PostureEscalation.RestoreDefaultEffort; a plain empty
  target means leave untouched)
- TUI /turns while a profile is active pins the budget: disarms the
  escalation turn target (mirroring exec's explicit --max-turns rule)
  and profile reverts use touched bits instead of value equality, so
  explicit choices survive even when they coincide with profile values
- spec-draft runs get the same escalation policy as the main run
  instead of the displaced budget with no safety net
- bench validates and canonicalizes --exec-profile before spawning:
  an unknown name previously produced an exit-0 report full of
  millisecond usage-error walls
- turn summary prints exec-profile alongside mode; zero exec rejects
  an inline-empty --exec-profile=; balanced trace label documented and
  pinned as deliberate attribution
2026-07-18 23:43:57 +05:30
Vasanthdev2004 73669678bb perf(agent): execution profiles, --exec-profile, bench passthrough, TUI /profile (PR10b+10c) 2026-07-18 23:17:01 +05:30
10 changed files with 13 additions and 180 deletions

No files matched your search

-31
View File
@@ -169,37 +169,6 @@ func TestRunResetsEmptyTurnCounterOnToolCall(t *testing.T) {
}
}
// The whole point of #702's fix: the unknown-session error's normalized
// signature must not vary with the session id, so a model probing ids 1, 2,
// 3, … produces one repeated signature the failure guard can count. If the id
// leaked into the first 80 normalized chars, each probe would reset the streak
// and the halt would never fire.
func TestUnknownExecSessionErrorSignatureIsIDInvariant(t *testing.T) {
a := errorSignature(tools.UnknownExecSessionError(1))
b := errorSignature(tools.UnknownExecSessionError(999999))
if a != b {
t.Fatalf("unknown-session signature varies with id:\n %q\n %q", a, b)
}
}
// End to end: with an id-invariant signature, probing a different unknown id
// each turn now trips the repeated-failure halt at toolFailureStopAt, where
// before the fix it never would.
func TestUnknownExecSessionProbingTripsFailureHalt(t *testing.T) {
var state guardState
var stoppedAt int
for i := 1; i <= toolFailureStopAt; i++ {
out := state.observeToolResult(tools.WriteStdinToolName, true, tools.UnknownExecSessionError(i))
if out.Stop {
stoppedAt = i
break
}
}
if stoppedAt != toolFailureStopAt {
t.Fatalf("probing distinct unknown ids stopped at %d, want %d", stoppedAt, toolFailureStopAt)
}
}
func TestGuardStateResetsToolOnlyStreakOnEmptyNonToolTurn(t *testing.T) {
var state guardState
toolOnly := zeroruntime.CollectedStream{
-3
View File
@@ -100,9 +100,6 @@ work.
- Use exec_command with `tty: true` for interactive terminal-style commands that
need stdin beyond Ctrl-C. `/ps` and `/stop` are user-facing TUI commands; when
you need to clean up a running foreground command yourself, use write_stdin.
- write_stdin's session_id is only ever an id returned by a still-running
exec_command; never guess or probe ids. If you have no such session, start one
with exec_command, or use write_file/edit_file/apply_patch for file changes.
- write_stdin with empty input polls an existing exec_command session, and
`\u0003` interrupts it. Sending other stdin bytes may require approval because
it can drive the running process beyond the original command. Non-tty sessions
-26
View File
@@ -133,26 +133,6 @@ func discoverAuthorizationServer(ctx context.Context, client *http.Client, baseU
}, nil
}
// validateResolvedEndpoints applies the shared https/loopback endpoint rule to
// every non-empty endpoint in the resolved metadata — configured or discovered
// alike — so discovery metadata can never downgrade an MCP login to an insecure
// or attacker-controlled authorization, token, or registration endpoint.
func validateResolvedEndpoints(metadata authServerMetadata) error {
for _, ep := range []struct{ kind, url string }{
{"authorization", metadata.AuthorizationEndpoint},
{"token", metadata.TokenEndpoint},
{"registration", metadata.RegistrationEndpoint},
} {
if strings.TrimSpace(ep.url) == "" {
continue
}
if err := oauth.ValidateEndpointURL(ep.url); err != nil {
return fmt.Errorf("mcp oauth: %s endpoint: %w", ep.kind, err)
}
}
return nil
}
// resolveAuthorizationServer discovers metadata and applies explicit config
// overrides. Configured endpoints take precedence over discovered ones, and act
// as a fallback when discovery fails or omits a value.
@@ -168,9 +148,6 @@ func resolveAuthorizationServer(ctx context.Context, client *http.Client, baseUR
TokenEndpoint: strings.TrimSpace(cfg.TokenEndpoint),
RegistrationEndpoint: strings.TrimSpace(cfg.RegistrationEndpoint),
}
if err := validateResolvedEndpoints(metadata); err != nil {
return authServerMetadata{}, err
}
return metadata, nil
}
@@ -202,9 +179,6 @@ func resolveAuthorizationServer(ctx context.Context, client *http.Client, baseUR
if strings.TrimSpace(metadata.TokenEndpoint) == "" {
return authServerMetadata{}, errors.New("no token endpoint discovered or configured")
}
if err := validateResolvedEndpoints(metadata); err != nil {
return authServerMetadata{}, err
}
return metadata, nil
}
-16
View File
@@ -73,22 +73,6 @@ func TestResolveEndpointsFallsBackToConfig(t *testing.T) {
}
}
// Discovered MCP metadata that serves an insecure endpoint must be rejected
// before use, so discovery cannot downgrade the MCP login to an
// attacker-controlled origin (issue #511).
func TestResolveAuthorizationServerRejectsInsecureDiscovered(t *testing.T) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
defer server.Close()
mux.HandleFunc("/.well-known/oauth-authorization-server", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"issuer":"` + server.URL + `","token_endpoint":"` + server.URL + `/token","authorization_endpoint":"http://evil.example/authorize"}`))
})
_, err := resolveAuthorizationServer(context.Background(), server.Client(), server.URL, OAuthConfig{})
if err == nil || !strings.Contains(err.Error(), "authorization endpoint") {
t.Fatalf("err = %v, want insecure authorization endpoint rejection", err)
}
}
// roundTripperFunc adapts a function to http.RoundTripper so a test can
// observe every outbound request.
type roundTripperFunc func(*http.Request) (*http.Response, error)
+5 -21
View File
@@ -39,13 +39,6 @@ func BuildAuthorizationURL(cfg Config, pkce PKCE, state, redirectURI string, ext
if endpoint == "" {
return "", errors.New("oauth: no authorization endpoint configured")
}
// Backstop: validate the authorization endpoint at the shared choke point
// both the provider and MCP flows build their browser URL through, so a
// discovery-downgraded endpoint can never open in the browser even if a
// merge site missed it.
if err := ValidateEndpointURL(endpoint); err != nil {
return "", err
}
parsed, err := url.Parse(endpoint)
if err != nil {
return "", fmt.Errorf("oauth: parse authorization endpoint: %w", err)
@@ -90,15 +83,13 @@ func isReservedAuthParam(key string) bool {
}
}
// ValidateEndpointURL refuses an OAuth endpoint that is not https, unless it is
// a loopback host (mirrors oauth2-client.js validateTokenEndpointURL). Every
// credential-bearing endpoint — configured OR learned from discovery — must
// pass this single rule, so discovery metadata can never downgrade a login to
// cleartext or redirect it to an attacker-controlled http origin.
func ValidateEndpointURL(endpoint string) error {
// validateTokenEndpoint refuses to send a credential to a token endpoint that is
// not https, unless it is a loopback host (mirrors oauth2-client.js
// validateTokenEndpointURL). This prevents leaking codes/tokens over cleartext.
func validateTokenEndpoint(endpoint string) error {
parsed, err := url.Parse(trimmed(endpoint))
if err != nil || parsed.Host == "" {
return fmt.Errorf("oauth: invalid endpoint %q", endpoint)
return fmt.Errorf("oauth: invalid token endpoint %q", endpoint)
}
if parsed.Scheme == "https" {
return nil
@@ -109,13 +100,6 @@ func ValidateEndpointURL(endpoint string) error {
return fmt.Errorf("%w: %s", ErrInsecureTokenEndpoint, parsed.Scheme+"://"+parsed.Host)
}
// validateTokenEndpoint checks a token endpoint against the shared endpoint rule
// (kept as a named helper for the token-exchange and device call sites). This
// prevents leaking codes/tokens over cleartext.
func validateTokenEndpoint(endpoint string) error {
return ValidateEndpointURL(endpoint)
}
func isLoopbackHost(host string) bool {
if host == "localhost" {
return true
-14
View File
@@ -112,20 +112,6 @@ func TestBuildAuthorizationURLRejectsPlain(t *testing.T) {
}
}
// The shared choke point both flows build their browser URL through must refuse
// an insecure authorization endpoint, so a discovery-downgraded endpoint can
// never open in the browser even if a merge site missed it (issue #511).
func TestBuildAuthorizationURLRejectsInsecureEndpoint(t *testing.T) {
_, err := BuildAuthorizationURL(
Config{AuthorizationEndpoint: "http://evil.example/authorize", ClientID: "c"},
PKCE{Method: MethodS256, Challenge: "chal"},
"state", "http://127.0.0.1/cb", nil,
)
if !errors.Is(err, ErrInsecureTokenEndpoint) {
t.Fatalf("err = %v, want ErrInsecureTokenEndpoint", err)
}
}
func TestValidateTokenEndpoint(t *testing.T) {
cases := map[string]bool{
"https://auth.example.com/token": true,
+3 -16
View File
@@ -218,26 +218,13 @@ func (m *Manager) resolveEndpoints(ctx context.Context, cfg Config) (Config, err
// may already be sufficient for the chosen flow. Only merge on success (and
// never return from the error branch, which would trip the nilerr linter).
if meta, err := m.discover(ctx, cfg.IssuerURL); err == nil {
// A discovered endpoint must pass the same https/loopback rule as a
// configured one before it is merged, so discovery metadata can never
// silently downgrade the login to an insecure or attacker-controlled
// endpoint. Fail closed on a bad endpoint rather than merging it.
if cfg.AuthorizationEndpoint == "" && meta.AuthorizationEndpoint != "" {
if err := ValidateEndpointURL(meta.AuthorizationEndpoint); err != nil {
return cfg, fmt.Errorf("oauth: discovered authorization endpoint: %w", err)
}
if cfg.AuthorizationEndpoint == "" {
cfg.AuthorizationEndpoint = meta.AuthorizationEndpoint
}
if cfg.TokenEndpoint == "" && meta.TokenEndpoint != "" {
if err := ValidateEndpointURL(meta.TokenEndpoint); err != nil {
return cfg, fmt.Errorf("oauth: discovered token endpoint: %w", err)
}
if cfg.TokenEndpoint == "" {
cfg.TokenEndpoint = meta.TokenEndpoint
}
if cfg.DeviceAuthorizationEndpoint == "" && meta.DeviceAuthorizationEndpoint != "" {
if err := ValidateEndpointURL(meta.DeviceAuthorizationEndpoint); err != nil {
return cfg, fmt.Errorf("oauth: discovered device authorization endpoint: %w", err)
}
if cfg.DeviceAuthorizationEndpoint == "" {
cfg.DeviceAuthorizationEndpoint = meta.DeviceAuthorizationEndpoint
}
}
-19
View File
@@ -2,7 +2,6 @@ package oauth
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
@@ -201,24 +200,6 @@ func TestManagerGetFreshDiscoversIssuerEndpoints(t *testing.T) {
}
}
// A discovery document that serves an insecure (non-https, non-loopback)
// endpoint must be rejected before it is merged, so discovery metadata cannot
// silently downgrade the login to an attacker-controlled origin (issue #511).
func TestResolveEndpointsRejectsInsecureDiscoveredEndpoint(t *testing.T) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
defer server.Close()
mux.HandleFunc("/.well-known/oauth-authorization-server", func(w http.ResponseWriter, _ *http.Request) {
// Loopback issuer, but an attacker-controlled http authorization endpoint.
_, _ = io.WriteString(w, `{"issuer":"`+server.URL+`","token_endpoint":"`+server.URL+`/token","authorization_endpoint":"http://evil.example/authorize"}`)
})
m := managerFor(t, map[string]string{}, nil)
_, err := m.resolveEndpoints(context.Background(), Config{IssuerURL: server.URL})
if !errors.Is(err, ErrInsecureTokenEndpoint) {
t.Fatalf("resolveEndpoints err = %v, want ErrInsecureTokenEndpoint", err)
}
}
func TestManagerGetFreshRefreshesExpired(t *testing.T) {
fp := newFakeProvider(t, `{"access_token":"fresh-at","expires_in":3600}`)
env := map[string]string{
+2 -14
View File
@@ -707,18 +707,6 @@ type writeStdinTool struct {
manager *execSessionManager
}
// UnknownExecSessionError is the result returned when write_stdin targets a
// session_id with no live exec session. The stable recovery guidance leads and
// the numeric id trails on purpose: the agent's repeated-failure guard keys on
// a normalized, truncated prefix of the error string, so keeping the id out of
// that prefix makes a model probing ids 1, 2, 3, … produce ONE signature that
// finally trips the halt instead of a fresh signature per id — while also
// telling the model how to actually recover. See TestUnknownExecSessionErrorSignatureIsIDInvariant
// in internal/agent, which pins this invariant against the real guard.
func UnknownExecSessionError(sessionID int) string {
return fmt.Sprintf("Error: write_stdin needs a session_id returned by a still-running exec_command; do not guess or probe session ids. Start a process with exec_command, or use write_file/edit_file/apply_patch for file changes. (no live session %d)", sessionID)
}
func (writeStdinTool) outputCategory(map[string]any) outputCategory {
return outputCategoryProcess
}
@@ -734,7 +722,7 @@ func NewWriteStdinTool(manager *execSessionManager) Tool {
parameters: Schema{
Type: "object",
Properties: map[string]PropertySchema{
"session_id": {Type: "integer", Description: "Identifier of a running unified exec session, as returned by exec_command. Never guess or probe ids.", Minimum: intPtr(1)},
"session_id": {Type: "integer", Description: "Identifier of the running unified exec session."},
"chars": {Type: "string", Description: "Bytes to write to stdin. Defaults to empty, which polls without writing.", Default: ""},
"yield_time_ms": {Type: "integer", Description: "Wait before yielding output. Non-empty writes default to 250 ms and cap at 30000 ms; empty polls wait 5000-300000 ms by default.", Default: defaultPollYieldTimeMS, Minimum: intPtr(1), Maximum: intPtr(maxPollYieldTimeMS)},
"max_output_tokens": {Type: "integer", Description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy.", Default: defaultMaxOutputTokens, Minimum: intPtr(1), Maximum: intPtr(maxExecOutputTokenRequest)},
@@ -796,7 +784,7 @@ func (tool writeStdinTool) RunWithOptions(ctx context.Context, args map[string]a
}
session, ok := tool.manager.get(sessionID)
if !ok {
return errorResult(UnknownExecSessionError(sessionID))
return errorResult(fmt.Sprintf("Error: Unknown exec session_id %d.", sessionID))
}
session.touch()
interrupted := false
+3 -20
View File
@@ -881,7 +881,7 @@ func TestRegistryHonorsWriteStdinArgumentPermission(t *testing.T) {
registry.Register(NewWriteStdinTool(newExecSessionManager()))
poll := registry.Run(context.Background(), WriteStdinToolName, map[string]any{"session_id": 9999})
if poll.Status != StatusError || !strings.Contains(poll.Output, "still-running exec_command") {
if poll.Status != StatusError || !strings.Contains(poll.Output, "Unknown exec session_id") {
t.Fatalf("empty poll should reach tool without permission prompt, got status=%s output=%q", poll.Status, poll.Output)
}
@@ -901,25 +901,8 @@ func TestWriteStdinReportsUnknownSession(t *testing.T) {
if result.Status != StatusError {
t.Fatalf("status = %s, want error", result.Status)
}
// The message must lead with recovery guidance and still identify the id.
if !strings.Contains(result.Output, "still-running exec_command") {
t.Fatalf("unknown-session error must carry recovery guidance, got: %q", result.Output)
}
if !strings.Contains(result.Output, "1234") {
t.Fatalf("unknown-session error must name the offending id, got: %q", result.Output)
}
}
// The runtime rejects session_id < 1 (intArg min 1); the advertised schema must
// say the same so a model sees the constraint before it probes id 0.
func TestWriteStdinSchemaPinsSessionIDMinimum(t *testing.T) {
schema := NewWriteStdinTool(nil).(writeStdinTool).parameters
prop, ok := schema.Properties["session_id"]
if !ok {
t.Fatal("session_id property missing from write_stdin schema")
}
if prop.Minimum == nil || *prop.Minimum != 1 {
t.Fatalf("session_id schema Minimum = %v, want 1 to match the runtime floor", prop.Minimum)
if !strings.Contains(result.Output, "Unknown exec session_id 1234") {
t.Fatalf("unexpected output: %q", result.Output)
}
}