mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
refactor(cli,config): remove --fail-on-unverified and agent.host_verification
Both only steered the final-readiness gate, which no longer exists: the flag chose exit 3 for an unmet readiness verdict and the setting chose whether a changed turn was held for a check, a review and a sign-off. Neither can do anything now. `run` still exits 4 when an untrusted folder refused its edits. A user config that still carries host_verification loads unchanged; the key is ignored. Removes the HoldOwedChecks and WaiveOwedChecks plumbing behind them.
This commit is contained in:
1 parent
b364c1693c
commit
8c785fa4b0
30 files changed
+28
-190
No files matched your search
+2
-3
@@ -317,7 +317,7 @@ empty when nothing was, and a refusal never changes the exit code. The same
|
||||
| `code` | Cause |
|
||||
| --- | --- |
|
||||
| `permission.unattended` | It needed an approval and nobody could give one. |
|
||||
| `permission.untrusted_folder` | The folder is not trusted, so it needed an approval nobody could give. The denial's `remedy` names `reasonix trust --dir <folder>`, which shows what the folder would run before approving; the run exits `4` (`3` under `--fail-on-unverified`) and the result's `unverified_by` carries the code. |
|
||||
| `permission.untrusted_folder` | The folder is not trusted, so it needed an approval nobody could give. The denial's `remedy` names `reasonix trust --dir <folder>`, which shows what the folder would run before approving; the run exits `4` and the result's `unverified_by` carries the code. |
|
||||
| `permission.read_only` | The session is in `read-only`. |
|
||||
| `permission.deny_rule` | A deny rule matched. |
|
||||
| `permission.declined` | A person answered no. |
|
||||
@@ -358,10 +358,9 @@ Exit statuses of `reasonix run`:
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| `0` | The model finished, including with other refused calls or unmet readiness. |
|
||||
| `0` | The model finished, including with other refused calls. |
|
||||
| `1` | The run failed: provider, configuration, limit, or cancellation. |
|
||||
| `2` | The command line was invalid. |
|
||||
| `3` | `--fail-on-unverified` was given and final readiness stayed unmet or the folder's edits were refused for lack of trust. |
|
||||
| `4` | The folder is not trusted and its edits or commands were refused, so the work was not done. Trust it with `reasonix trust --dir <folder>` or pass `--permission-mode` knowingly. |
|
||||
|
||||
### Redacted machine interfaces
|
||||
|
||||
+1
-2
@@ -269,7 +269,7 @@ reasonix run "运行测试" --output-format stream-json
|
||||
}
|
||||
```
|
||||
|
||||
`permission_denials` 列出本次运行的权限门拒绝的调用,没有被拒时为空数组;被拒一般不改变退出码;`permission.untrusted_folder` 退出 `4`(指定 `--fail-on-unverified` 时为 `3`)。
|
||||
`permission_denials` 列出本次运行的权限门拒绝的调用,没有被拒时为空数组;被拒一般不改变退出码;`permission.untrusted_folder` 退出 `4`。
|
||||
同一个 `code` 也随被拒的工具结果出现:`stream-json` 里是 `refusalCode`,`--events-jsonl`
|
||||
里是 `refusal_code`。
|
||||
|
||||
@@ -316,7 +316,6 @@ reasonix run "运行测试" --output-format stream-json
|
||||
| `0` | 模型已结束,包括有调用被拒或就绪检查未满足的情况。 |
|
||||
| `1` | 运行失败:模型服务、配置、上限或取消。 |
|
||||
| `2` | 命令行参数无效。 |
|
||||
| `3` | 指定了 `--fail-on-unverified`,且最终就绪检查未满足,或因文件夹未被信任而编辑和命令被拒绝。 |
|
||||
| `4` | 文件夹未被信任,编辑和命令被拒绝,任务没有完成。用 `reasonix trust --dir <folder>` 信任它,或明知故犯地传 `--permission-mode`。 |
|
||||
|
||||
### 脱敏机器接口
|
||||
|
||||
@@ -83,7 +83,6 @@ reasoning_language = "auto" # visible reasoning text: auto|zh|en
|
||||
# subagent_claims = "strict" # user file only; default optimistic: a sub-agent that declares no write_paths holds only the path it is writing; strict makes it hold the whole workspace
|
||||
# plan_approval = "auto" # user file only; a submitted plan starts executing without the approval click, and the transcript says so; default ask
|
||||
# default_mode = "plan" # user file only; a new conversation starts in plan mode (a resumed one keeps its own; Settings has a switch); default normal
|
||||
# host_verification = "on" # user file only; "on" holds a turn that changed files open until a check, a review and a sign-off exist; unset or "off" does not (unset still holds them under the delivery preset); the receipt always says what was checked
|
||||
# compact_ratio = 0.85 # sole auto trigger; presets 0.70 / 0.80 / 0.85
|
||||
# max_output_tokens = 0 # recommended: automatic (DeepSeek default high → ~64K; not unlimited)
|
||||
# max_output_tokens = 32768 # ordinary coding / cost control
|
||||
|
||||
@@ -92,7 +92,6 @@ reasoning_language = "auto" # 可见思考过程语言:auto|zh|en
|
||||
# plan_approval = "auto" # 仅用户配置文件;提交的计划不再等待批准点击,直接开始执行,对话里会留下说明,默认 ask
|
||||
# default_mode = "plan" # 仅用户配置文件;新对话从计划模式开始(恢复的对话保持自己的模式;设置里有对应开关),默认 normal
|
||||
# subagent_claims = "strict" # 仅用户配置文件;默认 optimistic:没有声明 write_paths 的子代理只占用正在写的路径;strict 则让它占整个工作区
|
||||
# host_verification = "on" # 仅用户配置文件;设为 on 时,改过文件的回合要先有检查、复核和签收才能结束;不设或设为 off 则不要求(不设时交付预设仍会要求);交付回执始终如实写明检查了什么
|
||||
# compact_ratio 是唯一自动维护阈值(默认 0.85;预设 0.70/0.80/0.85)
|
||||
# max_output_tokens = 0 # 推荐:自动(DeepSeek 默认 high → 约 64K;不是无限)
|
||||
# max_output_tokens = 32768 # 普通编码 / 控制费用
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ When a folder is trustable, its writes are sandbox-confined and no decision is r
|
||||
1.x wrote files and ran commands in any folder. In 2.x an untrusted folder has nobody to approve edits and shell commands, so a headless run refuses them. Nothing is loosened: the run now says so instead of looking like success.
|
||||
|
||||
- stderr names the refused tools, the code `permission.untrusted_folder` and the remedy.
|
||||
- The process exits `4` (`3` under `--fail-on-unverified`); the work was not done.
|
||||
- The process exits `4`; the work was not done.
|
||||
- `--output-format json` and `stream-json` list each refusal in `permission_denials` with its `code` and `remedy`, and set `unverified_by`.
|
||||
|
||||
To make a script behave as it did in 1.x, trust the folder once (`reasonix trust --dir <folder>`), or pass `--permission-mode auto` or `--yolo` knowingly for that run. Other exit codes are as in 1.x: `1` for an error, `2` for a usage error.
|
||||
|
||||
@@ -539,7 +539,6 @@ func (b *builder) executor() *agent.Agent {
|
||||
ProjectChecks: b.prompt.projectChecks, ProjectSensitivePaths: b.prompt.sensitivePaths,
|
||||
AgentPreset: b.model.preset,
|
||||
DeliveryProfile: b.model.delivery,
|
||||
WaiveOwedChecks: cfg.WaivesOwedChecks(b.model.delivery) && !b.opts.HoldOwedChecks,
|
||||
Ablation: b.opts.Ablation,
|
||||
WorkspaceLease: b.session.lease,
|
||||
CapabilityLedger: t.caps.ledger,
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package boot
|
||||
|
||||
import ()
|
||||
|
||||
// holdChangedTurns is the user-level opt-in that makes a turn which changed files
|
||||
// owe its check, review and sign-off; tests of what a turn owes start from it.
|
||||
const holdChangedTurns = "\n[agent]\nhost_verification = \"on\"\n"
|
||||
@@ -80,7 +80,7 @@ func runPlannedChecks(t *testing.T, files map[string]string, planArgs string, ca
|
||||
isolateConfigHome(t)
|
||||
// The subject is what the checks write, not the jail: a host without an OS
|
||||
// sandbox refuses bash fail-closed, and the checks would never run.
|
||||
writeUserConfig(t, holdChangedTurns+"\n[sandbox]\nbash = \"off\"\n")
|
||||
writeUserConfig(t, "[sandbox]\nbash = \"off\"\n")
|
||||
dir := robustTempDir(t)
|
||||
t.Chdir(dir)
|
||||
// A declared project check makes the turn answer to readiness in the
|
||||
|
||||
@@ -24,7 +24,6 @@ print("rewrote")
|
||||
func TestEffectRunScriptReadEditWriteKeepsTheBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
home := statedBootHome(t)
|
||||
writeFile(t, home, "config.toml", holdChangedTurns)
|
||||
dir := testenv.TempDir(t)
|
||||
big := strings.Repeat("// filler line that pads the file past one tool result\n", 1200) + "func OldName() {}\n"
|
||||
files := map[string]string{
|
||||
|
||||
@@ -139,7 +139,6 @@ type Options struct {
|
||||
Observe *ObserveOptions
|
||||
|
||||
GoalTurnsUnreachable bool // this assembly never arms a Goal turn; see GoalOnlyToolNames
|
||||
HoldOwedChecks bool // the caller asked for a verified result, so a changed turn owes its check whatever host_verification says
|
||||
UnattendedChild bool // an unattended child run: it offers neither best_of_n nor isolation
|
||||
// SessionRecoveryMeta and OnSessionRecovered let richer frontends attach
|
||||
// local UI metadata to automatic transcript recovery branches.
|
||||
|
||||
@@ -1137,12 +1137,11 @@ type AgentConfig struct {
|
||||
// declare non-overlapping write_paths. 0, the default, sets no limit: the
|
||||
// path claims keep writers apart, not a count. The user file only.
|
||||
MaxParallelWriters int `toml:"max_parallel_writers"`
|
||||
SubagentClaims string `toml:"subagent_claims"` // "optimistic" (default) or "strict"; the user file only
|
||||
WriteLease string `toml:"write_lease"` // "optimistic" (default) or "strict"; the user file only
|
||||
PlanApproval string `toml:"plan_approval"` // "ask" (default) or "auto": the plan card answers itself; the user file only
|
||||
DefaultMode string `toml:"default_mode"` // "normal" (default) or "plan": how a new conversation starts; the user file only
|
||||
GoalAutoRetry int `toml:"goal_auto_retry"` // 0 (default) stops at an unavailable completion check; N retries it up to N times in a row; the user file only
|
||||
HostVerification string `toml:"host_verification"` // unset (the default), "on" or "off"; the user file only
|
||||
SubagentClaims string `toml:"subagent_claims"` // "optimistic" (default) or "strict"; the user file only
|
||||
WriteLease string `toml:"write_lease"` // "optimistic" (default) or "strict"; the user file only
|
||||
PlanApproval string `toml:"plan_approval"` // "ask" (default) or "auto": the plan card answers itself; the user file only
|
||||
DefaultMode string `toml:"default_mode"` // "normal" (default) or "plan": how a new conversation starts; the user file only
|
||||
GoalAutoRetry int `toml:"goal_auto_retry"` // 0 (default) stops at an unavailable completion check; N retries it up to N times in a row; the user file only
|
||||
|
||||
// WriteLeaseMaxWaitMinutes bounds one wait behind another session's write
|
||||
// claim; 0, the default, waits for as long as the turn lasts. The user file only.
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
// WaivesOwedChecks reports whether the host drops the check, review and sign-off
|
||||
// a changed turn owes. Unset drops them except under the Delivery preset; "off"
|
||||
// and "on" decide under any preset; any other value reads as "on", so a typo
|
||||
// never weakens what the host asks.
|
||||
func (c *Config) WaivesOwedChecks(delivery bool) bool {
|
||||
if c == nil {
|
||||
return !delivery
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(c.Agent.HostVerification)) {
|
||||
case "":
|
||||
return !delivery
|
||||
case "off":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOwedChecksAreWaivedUnlessTheUserOrTheDeliveryPresetHoldsThem(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
value string
|
||||
delivery bool
|
||||
want bool
|
||||
}{
|
||||
{"", false, true}, {"", true, false},
|
||||
{"on", false, false}, {"on", true, false},
|
||||
{"off", false, true}, {"off", true, true}, {"Off", false, true}, {" off ", true, true},
|
||||
{"false", false, false}, {"nonsense", false, false}, {"nonsense", true, false},
|
||||
} {
|
||||
cfg := Default()
|
||||
cfg.Agent.HostVerification = tc.value
|
||||
if got := cfg.WaivesOwedChecks(tc.delivery); got != tc.want {
|
||||
t.Errorf("host_verification = %q, delivery %v -> waived %v, want %v", tc.value, tc.delivery, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserFileTurnsHostVerificationOff(t *testing.T) {
|
||||
cfg, _ := loadScoped(t, "[agent]\nhost_verification = \"off\"\n", "")
|
||||
if !cfg.WaivesOwedChecks(false) {
|
||||
t.Fatalf("a user file's off was not honoured: %q", cfg.Agent.HostVerification)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectCannotChangeHostVerificationEitherWay(t *testing.T) {
|
||||
for _, value := range []string{"on", "off"} {
|
||||
cfg, _ := loadScoped(t, "", "[agent]\nhost_verification = \""+value+"\"\n")
|
||||
if !cfg.WaivesOwedChecks(false) || cfg.WaivesOwedChecks(true) {
|
||||
t.Fatalf("a cloned repo's reasonix.toml (%q) moved what the host asks of a turn: waived %v, under delivery %v", value, cfg.WaivesOwedChecks(false), cfg.WaivesOwedChecks(true))
|
||||
}
|
||||
if !slices.Contains(ignoredKeys(cfg), "agent.host_verification") {
|
||||
t.Fatalf("ignored = %v, want agent.host_verification reported", ignoredKeys(cfg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectCannotTurnBackOnWhatTheUserTurnedOff(t *testing.T) {
|
||||
cfg, _ := loadScoped(t, "[agent]\nhost_verification = \"off\"\n", "[agent]\nhost_verification = \"on\"\n")
|
||||
if !cfg.WaivesOwedChecks(false) {
|
||||
t.Fatal("the project file overrode the user's own host_verification")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostVerificationOffSurvivesASave(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.Agent.HostVerification = "off"
|
||||
if out := RenderTOMLForScope(cfg, RenderScopeUser); !strings.Contains(out, "host_verification = \"off\"") {
|
||||
t.Fatalf("rendered config lost the choice:\n%s", out)
|
||||
}
|
||||
if out := RenderTOMLForScope(Default(), RenderScopeUser); strings.Contains(out, "\nhost_verification = ") {
|
||||
t.Fatalf("the default was written as an active key:\n%s", out)
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ type heldScope struct {
|
||||
limits agentLimits
|
||||
goalAutoRetry int
|
||||
subagentClaims string
|
||||
hostVerif string
|
||||
shell ShellConfig
|
||||
rgPath string
|
||||
lsp map[string]LSPServer
|
||||
@@ -92,7 +91,6 @@ func holdUserScope(c *Config) heldScope {
|
||||
limits: holdAgentLimits(c),
|
||||
goalAutoRetry: c.Agent.GoalAutoRetry,
|
||||
subagentClaims: c.Agent.SubagentClaims,
|
||||
hostVerif: c.Agent.HostVerification,
|
||||
shell: shell,
|
||||
rgPath: c.Tools.Search.RgPath,
|
||||
lsp: maps.Clone(c.LSP.Servers),
|
||||
@@ -259,12 +257,6 @@ func (h heldScope) narrow(c *Config, r Roots, root string, projectMeta toml.Meta
|
||||
c.ignoreProject("agent.subagent_claims", c.Agent.SubagentClaims, ProjectUserOnly)
|
||||
c.Agent.SubagentClaims = h.subagentClaims
|
||||
}
|
||||
// What the host asks of a turn before it may finish is the user's own call,
|
||||
// so a cloned repo can neither switch it off nor undo the user's choice.
|
||||
if c.Agent.HostVerification != h.hostVerif {
|
||||
c.ignoreProject("agent.host_verification", c.Agent.HostVerification, ProjectUserOnly)
|
||||
c.Agent.HostVerification = h.hostVerif
|
||||
}
|
||||
// Keeping paired phones across restarts is the user's alone: it widens how
|
||||
// long a device stays trusted, which is not a clone's to give away.
|
||||
if c.Serve.RememberPairedDevices != h.rememberDevices {
|
||||
|
||||
@@ -211,7 +211,6 @@ func RenderTOMLForScope(c *Config, scope RenderScope) string {
|
||||
agentSetting(&b, "write_lease_max_wait_minutes", c.Agent.WriteLeaseMaxWaitMinutes, defaults.Agent.WriteLeaseMaxWaitMinutes, "minutes a write waits behind another session's claim before it fails with workspace.claim_timeout; 0 = wait as long as the turn lasts (default), 120 is a sensible limit; user file only")
|
||||
agentSetting(&b, "plan_approval", c.Agent.PlanApproval, defaults.Agent.PlanApproval, "auto: the plan card answers itself and execution starts; user file only")
|
||||
agentSetting(&b, "default_mode", c.Agent.DefaultMode, defaults.Agent.DefaultMode, "plan: a new conversation starts in plan mode (the mode picker still switches it); a resumed one keeps its own; user file only")
|
||||
agentSetting(&b, "host_verification", c.Agent.HostVerification, defaults.Agent.HostVerification, "on: the host holds a turn that changed files open until a check, a review and a sign-off exist; unset or off: it does not (unset still holds them under the delivery preset); the receipt always says what was and was not checked; user file only")
|
||||
if c.Agent.OutputStyle != "" {
|
||||
fmt.Fprintf(&b, "output_style = %q # persona/tone folded into the prompt\n", c.Agent.OutputStyle)
|
||||
} else {
|
||||
|
||||
@@ -141,7 +141,6 @@ var table = []Entry{
|
||||
{ID: "agent.plan_approval", Class: Guarded, Effect: OnOpen},
|
||||
{ID: "agent.default_mode", Class: Preference, Effect: OnOpen},
|
||||
{ID: "agent.goal_auto_retry", Class: Guarded, Effect: OnOpen},
|
||||
{ID: "agent.host_verification", Class: Guarded, Effect: OnOpen},
|
||||
{ID: "agent.output_style", Class: Preference, Effect: OnOpen},
|
||||
{ID: "agent.auto_plan", Class: Preference, Effect: OnOpen},
|
||||
{ID: "agent.reasoning_language", Class: Preference, Effect: OnOpen},
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestBoundaryKeysAreGuarded(t *testing.T) {
|
||||
}
|
||||
}
|
||||
for _, id := range []string{
|
||||
"desktop.default_tool_approval_mode", "agent.plan_approval", "agent.host_verification", "agent.subagent_claims",
|
||||
"desktop.default_tool_approval_mode", "agent.plan_approval", "agent.subagent_claims",
|
||||
"agent.max_steps", "agent.max_subagent_depth", "agent.max_subagent_concurrency", "agent.max_parallel_writers",
|
||||
"agent.task_token_budget", "agent.guardian_model", "agent.system_prompt", "agent.screen_external_content",
|
||||
"tools.shell.path", "tools.enabled", "statusline.command", "skills.paths", "providers", "plugins",
|
||||
|
||||
@@ -30,7 +30,6 @@ type cliBuildOverrides struct {
|
||||
WorkspaceRoot string
|
||||
HeadlessApprovalMode string
|
||||
GoalTurnsUnreachable bool
|
||||
HoldOwedChecks bool
|
||||
Stderr io.Writer
|
||||
OnSessionRecovered func(control.SessionRecoveryInfo) error
|
||||
Ablation ablation.Set
|
||||
@@ -78,7 +77,6 @@ func cliProfileBuildOptions(modelName string, maxStepsOverride int, requireKey b
|
||||
AdditionalDirs: overrides.AdditionalDirs,
|
||||
HeadlessApprovalMode: overrides.HeadlessApprovalMode,
|
||||
GoalTurnsUnreachable: overrides.GoalTurnsUnreachable,
|
||||
HoldOwedChecks: overrides.HoldOwedChecks,
|
||||
StatsSource: surface.CLI,
|
||||
FeedbackSurface: feedback.SurfaceTUI,
|
||||
Stderr: overrides.Stderr,
|
||||
@@ -95,7 +93,7 @@ func cliProfileBuildOptions(modelName string, maxStepsOverride int, requireKey b
|
||||
// exits, so nothing in this path can reach SetGoal and goal-only tools are left
|
||||
// out of the provider schema.
|
||||
func runBuildOverrides(effort *string, allow, dirs []string, workspaceRoot, approval string,
|
||||
onRecovered func(control.SessionRecoveryInfo) error, ablated ablation.Set, holdOwedChecks bool) cliBuildOverrides {
|
||||
onRecovered func(control.SessionRecoveryInfo) error, ablated ablation.Set) cliBuildOverrides {
|
||||
return cliBuildOverrides{
|
||||
Effort: effort,
|
||||
PermissionAllow: allow,
|
||||
@@ -105,6 +103,5 @@ func runBuildOverrides(effort *string, allow, dirs []string, workspaceRoot, appr
|
||||
GoalTurnsUnreachable: true,
|
||||
OnSessionRecovered: onRecovered,
|
||||
Ablation: ablated,
|
||||
HoldOwedChecks: holdOwedChecks,
|
||||
}
|
||||
}
|
||||
@@ -501,7 +501,7 @@ func runAgent(args []string, version string) int {
|
||||
// executor, not just the top-level one. Ask fails closed because no UI can
|
||||
// answer; with no mode named, the build's sandbox claim and the folder's
|
||||
// trust decide between that and auto once the controller exists.
|
||||
overrides := runBuildOverrides(effortOverride, allowedTools, f.additionalDirs, workspaceRoot, permissions.approval, cliSessionRecoveredHandler(leases), ablated, *f.failOnUnverified)
|
||||
overrides := runBuildOverrides(effortOverride, allowedTools, f.additionalDirs, workspaceRoot, permissions.approval, cliSessionRecoveredHandler(leases), ablated)
|
||||
overrides.Version = version
|
||||
overrides = withComposition(overrides, *f.agentComposition, resumeSession)
|
||||
ctrl, err := setupProfileWithOverrides(ctx, *f.model, *f.maxSteps, true, sink, profile, overrides)
|
||||
@@ -527,7 +527,7 @@ func runAgent(args []string, version string) int {
|
||||
chain.begin(ctrl, version, prompt)
|
||||
runErr := ctrl.Run(ctx, prompt)
|
||||
reporter.RecordRecovery(ctrl.DrainRecoveryMetrics())
|
||||
completion := classifyRunCompletion(runErr).withFolderRefusal(chain.refusedByFolderTrust(os.Stderr)).withFailOnUnverified(*f.failOnUnverified)
|
||||
completion := classifyRunCompletion(runErr).withFolderRefusal(chain.refusedByFolderTrust(os.Stderr))
|
||||
if cfg != nil {
|
||||
notify.SendEvent(newNotificationSender(), i18n.M, cfg.Notifications, event.Event{
|
||||
Kind: event.TurnDone,
|
||||
|
||||
@@ -27,16 +27,10 @@ func TestClassifyRunCompletionNamesTheFailureClass(t *testing.T) {
|
||||
}
|
||||
|
||||
// An unmet final-readiness judgement is a verdict on a finished run: it keeps
|
||||
// its benchmark class but exits 0 unless --fail-on-unverified asks for 3.
|
||||
// its benchmark class and exits 0.
|
||||
func TestFinalReadinessIsAVerdictNotAFailure(t *testing.T) {
|
||||
readiness := classifyRunCompletion(&agent.FinalReadinessError{Attempts: 3})
|
||||
if readiness.isError || readiness.exitCode != 0 || !readiness.unverified || readiness.subtype != "success" {
|
||||
t.Fatalf("final-readiness completion = %+v, want a successful, unverified run", readiness)
|
||||
}
|
||||
if got := readiness.withFailOnUnverified(true).exitCode; got != runExitUnverified {
|
||||
t.Fatalf("--fail-on-unverified exit = %d, want %d", got, runExitUnverified)
|
||||
}
|
||||
if got := classifyRunCompletion(errors.New("provider 500")).withFailOnUnverified(true).exitCode; got != 1 {
|
||||
t.Fatalf("--fail-on-unverified changed a run error's exit to %d", got)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ type runFlags struct {
|
||||
takeover *bool
|
||||
autoApprove, yolo *bool
|
||||
printOnly, eventsJSONL *bool
|
||||
failOnUnverified *bool
|
||||
additionalDirs, allowedToolValues []string
|
||||
}
|
||||
|
||||
@@ -45,7 +44,6 @@ func newRunFlags() *runFlags {
|
||||
f.permissionMode = fs.String("permission-mode", "", "permission mode: read-only | manual | ask | auto | acceptEdits | dontAsk | bypassPermissions (default: auto where the OS sandbox confines writes and the folder is trusted, otherwise ask)")
|
||||
f.autoApprove, f.yolo = registerRunApprovalFlags(fs)
|
||||
f.printOnly = fs.BoolP("print", "p", false, "print only the final response")
|
||||
f.failOnUnverified = fs.Bool("fail-on-unverified", false, "exit 3 when the model finished but the host could not verify the result (final readiness unmet, or edits and commands refused because the folder is not trusted)")
|
||||
f.eventsJSONL = fs.Bool("events-jsonl", false, "emit a redacted structured event stream as JSONL")
|
||||
f.outputFormat = fs.String("output-format", "text", "output format: text | json | stream-json")
|
||||
fs.StringArrayVar(&f.additionalDirs, "add-dir", nil, "allow tool access to an additional directory (repeatable)")
|
||||
|
||||
@@ -98,14 +98,10 @@ type runResult struct {
|
||||
// refusal's code; a run that wrote nothing says why here, not only in prose.
|
||||
PermissionDenials []runPermissionDenial `json:"permission_denials"`
|
||||
// PermissionMode is the posture the run settled on, named or defaulted.
|
||||
PermissionMode string `json:"permission_mode,omitempty"`
|
||||
// Readiness is the unmet final-readiness judgement, if any; the run still
|
||||
// exits 0 unless --fail-on-unverified asked otherwise.
|
||||
Readiness *eventwire.FinalReadiness `json:"readiness,omitempty"`
|
||||
// UnverifiedBy is the refusal code that left the work undone when the run
|
||||
// still reports success; `--fail-on-unverified` turns it into exit 3.
|
||||
UnverifiedBy string `json:"unverified_by,omitempty"`
|
||||
Completion *eventwire.CompletionSummary `json:"completion,omitempty"`
|
||||
PermissionMode string `json:"permission_mode,omitempty"`
|
||||
Readiness *eventwire.FinalReadiness `json:"readiness,omitempty"`
|
||||
UnverifiedBy string `json:"unverified_by,omitempty"`
|
||||
Completion *eventwire.CompletionSummary `json:"completion,omitempty"`
|
||||
}
|
||||
|
||||
type machineEventUsage struct {
|
||||
|
||||
@@ -76,7 +76,7 @@ func (r *untrustedFolderRun) modelSaw() string {
|
||||
|
||||
// In a folder nobody trusted, a headless run that could not write says so on
|
||||
// stderr with the refusal's identity and the remedy, tells the model the same
|
||||
// cause, and exits 4 (3 under --fail-on-unverified); trusting the folder lets the
|
||||
// cause, and exits 4; trusting the folder lets the
|
||||
// identical command write.
|
||||
func TestRunInUntrustedFolderSaysWhyNothingWasDone(t *testing.T) {
|
||||
r := newUntrustedFolderRun(t)
|
||||
@@ -98,11 +98,6 @@ func TestRunInUntrustedFolderSaysWhyNothingWasDone(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = captureCLIOutput(t, func() { code = Run([]string{"run", "--fail-on-unverified", "write notes.md"}, "test") })
|
||||
if code != runExitUnverified {
|
||||
t.Fatalf("--fail-on-unverified exited %d, want %d: every write was refused by project trust", code, runExitUnverified)
|
||||
}
|
||||
|
||||
stdout, _ := captureCLIOutput(t, func() { code = Run([]string{"-p", "--output-format", "json", "write notes.md"}, "test") })
|
||||
var result struct {
|
||||
Denials []runPermissionDenial `json:"permission_denials"`
|
||||
|
||||
@@ -15,14 +15,9 @@ import (
|
||||
"reasonix/internal/session/control"
|
||||
)
|
||||
|
||||
// runExitUnverified is `run --fail-on-unverified`'s exit status when the model
|
||||
// finished but the host's final-readiness judgement was left unmet. It is
|
||||
// apart from 1 (run error) and 2 (usage) so automation can tell them apart.
|
||||
const runExitUnverified = 3
|
||||
|
||||
// runExitUntrustedFolder is the exit status of a run whose edits or commands
|
||||
// were refused because the folder is untrusted, so the work was not done. It
|
||||
// is apart from 1, 2 and 3; --fail-on-unverified keeps reporting 3 for it.
|
||||
// is apart from 1 and 2.
|
||||
const runExitUntrustedFolder = 4
|
||||
|
||||
// runVerdict is what the host decided about the run beside its answer.
|
||||
@@ -136,15 +131,6 @@ func runReadiness(runErr error) *eventwire.FinalReadiness {
|
||||
return &eventwire.FinalReadiness{Attempts: readiness.Attempts, Missing: append([]string(nil), readiness.Missing...)}
|
||||
}
|
||||
|
||||
// withFailOnUnverified applies --fail-on-unverified to the exit status only;
|
||||
// the machine-readable result reports the same verdict either way.
|
||||
func (c runCompletion) withFailOnUnverified(on bool) runCompletion {
|
||||
if on && c.unverified {
|
||||
c.exitCode = runExitUnverified
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// withFolderRefusal marks a run whose edits and commands were refused because
|
||||
// the folder is untrusted as unverified: whatever the model then said, the work
|
||||
// it was asked for did not happen.
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestRunProseOnlyFinishHasNoReadinessDebt(t *testing.T) {
|
||||
if err := os.Remove(filepath.Join(dir, "note.txt")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
code, result, stderr := runCLIJSONResult(t, "run", "-y", "--fail-on-unverified", "--output-format", "json", "write a note")
|
||||
code, result, stderr := runCLIJSONResult(t, "run", "-y", "--output-format", "json", "write a note")
|
||||
if code != 0 || result["is_error"] != false || result["readiness"] != nil {
|
||||
t.Fatalf("prose finish exited %d with %v; stderr: %s", code, result, stderr)
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ func cliCompletionRootSpec() cliCompletionSpec {
|
||||
effort, permissionMode,
|
||||
completionFlag("--auto -y", cliCompletionNoValue), completionFlag("--dangerously-skip-permissions --yolo", cliCompletionNoValue),
|
||||
completionFlag("--print -p", cliCompletionNoValue),
|
||||
completionFlag("--events-jsonl", cliCompletionNoValue), completionFlag("--fail-on-unverified", cliCompletionNoValue),
|
||||
completionFlag("--events-jsonl", cliCompletionNoValue),
|
||||
completionFlag("--output-format", cliCompletionStaticValue, "text", "json", "stream-json"),
|
||||
completionFlag("--add-dir", cliCompletionPathValue),
|
||||
completionFlag("--allowed-tools --allowedTools", cliCompletionStaticValue),
|
||||
|
||||
@@ -95,15 +95,6 @@ func runTrace(t *testing.T, tc traceCase) traceReading {
|
||||
if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), []byte(toml), 0o600); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
// A turn that changed files owes its check only when the user asks the host to
|
||||
// hold it, and these readings include what that hold emits.
|
||||
userConfig := config.RootsForHome(dir).UserConfigPath()
|
||||
if err := os.MkdirAll(filepath.Dir(userConfig), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(userConfig, []byte("[agent]\nhost_verification = \"on\"\n"), 0o600); err != nil {
|
||||
t.Fatalf("write user config: %v", err)
|
||||
}
|
||||
// The home this run states is where the person's approval would be.
|
||||
if _, err := config.RootsForHome(dir).ApproveWorkspacePrograms(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -167,11 +167,6 @@ type Options struct {
|
||||
// AgentPreset is empty for one compatibility version of direct constructors.
|
||||
DeliveryProfile bool
|
||||
|
||||
// WaiveOwedChecks is the user's choice to drop the check, review and
|
||||
// sign-off a turn that changed files owes before it may finish. It never
|
||||
// comes from a project file.
|
||||
WaiveOwedChecks bool
|
||||
|
||||
// AgentPreset is the session role setting (balanced|delivery). Empty
|
||||
// falls back to balanced unless DeliveryProfile is true (then delivery).
|
||||
// Switching the preset mid-session does not rebuild the agent; the value is
|
||||
|
||||
@@ -16,16 +16,11 @@ type agentRole struct {
|
||||
// non-destructive MCP targets without readOnlyHint. Writers, bash and
|
||||
// destructive MCP stay blocked, and read-only sub-agents leave it false.
|
||||
plannerMCPExecution bool
|
||||
// waiveOwedChecks is the user's choice to drop the check, review and
|
||||
// sign-off a turn that changed files owes before it may finish. The turn's
|
||||
// own unfinished plan and a capability the user required still hold it.
|
||||
waiveOwedChecks bool
|
||||
}
|
||||
|
||||
func newAgentRole(opts Options) agentRole {
|
||||
return agentRole{
|
||||
readOnlyExecution: opts.ReadOnlyExecution,
|
||||
plannerMCPExecution: opts.PlannerMCPExecution,
|
||||
waiveOwedChecks: opts.WaiveOwedChecks,
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,10 @@
|
||||
"doc-prose": 2128,
|
||||
"error-text": 9,
|
||||
"essay": 2579,
|
||||
"file-size": 18429,
|
||||
"file-size": 18427,
|
||||
"flat-view": 0,
|
||||
"frontend-parity": 13,
|
||||
"function-size": 2486,
|
||||
"function-size": 2485,
|
||||
"layer-order": 0,
|
||||
"layering": 0,
|
||||
"marker": 0,
|
||||
@@ -265,7 +265,7 @@
|
||||
},
|
||||
"internal/contract/config/config.go": {
|
||||
"essay": 30,
|
||||
"file-size": 846,
|
||||
"file-size": 845,
|
||||
"process-input": 3
|
||||
},
|
||||
"internal/contract/config/credentials.go": {
|
||||
@@ -343,8 +343,8 @@
|
||||
"internal/contract/config/render.go": {
|
||||
"complexity": 86,
|
||||
"essay": 1,
|
||||
"file-size": 273,
|
||||
"function-size": 375
|
||||
"file-size": 272,
|
||||
"function-size": 374
|
||||
},
|
||||
"internal/contract/config/vision.go": {
|
||||
"essay": 1,
|
||||
|
||||
Reference in new issue
Block a user