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:
esengine committed 2026-11-02 05:27:28 -08:00
1 parent b364c1693c
commit 8c785fa4b0
30 files changed
+28 -190

No files matched your search

+2 -3
View File
@@ -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
View File
@@ -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`。 |
### 脱敏机器接口
-1
View File
@@ -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
-1
View File
@@ -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
View File
@@ -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.
-1
View File
@@ -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{
-1
View File
@@ -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.
+5 -6
View File
@@ -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 {
-1
View File
@@ -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 {
-1
View File
@@ -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},
+1 -1
View File
@@ -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",
+1 -4
View File
@@ -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,
}
}
+2 -2
View File
@@ -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,
+1 -7
View File
@@ -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)
}
}
-2
View File
@@ -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)")
+4 -8
View File
@@ -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"`
+1 -15
View File
@@ -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.
+1 -1
View File
@@ -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)
}
+1 -1
View File
@@ -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)
-5
View File
@@ -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
-5
View File
@@ -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,
}
}
+5 -5
View File
@@ -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,