Skip to content

DESIGN QUESTION: wildcard non-deny decisions on compress still lose to ACP appended default (silent ask→allow) #472

Open

Description

@ranxianglei

来源: #465 分析 wildcard 覆盖 acp_status 问题时发现。同一家族的根因(key-presence vs rule-resolution)在 compress defer 守卫上还有一个残留实例;#457/#465 的修复按 owner 范围只改了 acp_status 默认写入,此处是设计决策而非机械 bug 修复,故立项待 owner 拍板。

Observation

After #457/#465 (PR #466), the acp_status default write is gated on rule resolution (anyRuleResolves). The sibling guard for the compress key — hasExplicitToolPermission(permissionConfig, "compress") inside applyAcpToolPermissions (lib/host-permissions.ts) — is still plain key presence. Consequence: a user wildcard decision that resolves compress but is not a deny does not defer:

// user opencode.json
{ "permission": { "*": "ask" } }   // no explicit compress key
// ACP config: default (compress.permission: "allow")

Flow on current code:

  1. compressDisabledByOpencode({ "*": "ask" }) → false (only pattern==="*" && action==="deny" self-disables) → no self-disable.
  2. applyAcpToolPermissions: no own compress key → appends compress: "allow" after the user's "*": "ask".
  3. Host last-match resolution: compress matches both "*": "ask" and compress: "allow"; the appended rule is later → allow wins.

Net effect: the user's ask on compress is silently upgraded to allow.

Severity: LOW — design question, not a regression

  • The safety-critical direction is already covered: wildcard or exact deny on compress triggers the self-disable path (compressDisabledByOpencode, lib/host-permissions.ts:71, which itself uses rule resolution) and ACP stops offering the compress tool entirely.
  • Only ask (and redundant allow) resolutions are affected; practical impact is one fewer confirmation prompt on the compress tool.
  • Not introduced by PR fix: respect user decisions on permission.acp_status in host config write (#457, #465) #466 — behavior is identical before and after it.

Why filed rather than fixed in PR #466

Extending the defer guard to rule resolution (anyRuleResolves(…, "compress")) would change defer-by-design semantics beyond the owner-scoped #457/#465 fix: every config where a wildcard merely happens to cover compress would stop receiving ACP's own compress permission from ACP config. That is a semantic choice, not a mechanical fix.

Options

  1. Keep current semantics (defer = exact key only). Rationale: ACP's own compress.permission config is the authoritative knob for the compress tool; a blanket wildcard is a general catch-all, unlikely to express fine-grained intent for ACP's internal tool. Cost: the ask→allow silent upgrade remains.
  2. Extend defer to any rule resolving compress (reuse anyRuleResolves). Consistent with the [BLOCKER][V1] Явный permission.acp_status: "deny" молча переписывается в "allow" #457/MINOR: the #457 fix only covers the exact own-key case — a user wildcard decision on acp_status is still overwritten #465 principle — ACP never writes a decision over a user rule that resolves the tool. Cost: behavior change for existing users, e.g. { "*": "ask" } + ACP default allow → compress becomes ask instead of allow (a tightening, matching stated user intent).
  3. Asymmetric (defer on ask/deny, not on allow) — rejected: inconsistent and hard to explain.

Recommendation: option 2. The same root-cause family (key presence vs rule resolution) was judged wrong for acp_status in #457/#465; leaving the sister guard key-based keeps an incoherent split inside the same function. Since it widens defer behavior for existing users, it needs an explicit owner decision.

No PR attached — design question for the owner.

Activity

  1. ranxianglei commented on Sep 29, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 Verified the observation against the current PR #466 head — it holds exactly as described. Assessment and recommendation below; no code changes made, waiting for your call.

    Verification (PR #466 head 087e25cf, still open / unmerged)

    The split is confirmed at lib/host-permissions.ts on the branch tip, adjacent lines:

    // defer guard — key presence
    if (permissionConfig && hasExplicitToolPermission(permissionConfig, "compress")) {
        return permissionConfig
    }
    ...
    // acp_status default — rule resolution (#457/#465 fix)
    if (!anyRuleResolves(permissionConfig, "acp_status")) {
        next.acp_status = "allow"
    }

    Traced { "permission": { "*": "ask" } } end-to-end through the config hook (index.ts):

    1. Self-disable (compressDisabledByOpencode, index.ts:226) → false (action is ask, not deny) → no self-disable. ✓
    2. No own compress key → applyAcpToolPermissions appends compress: "allow" after the user's "*": "ask". ✓
    3. Last-match resolution → the appended rule wins → ask silently upgraded to allow. ✓

    Also confirmed: identical before and after PR #466 (not a regression), and deny remains safe via the self-disable path, which uses rule resolution.

    One addition beyond the issue body: ACP never writes host permission entries for decompress, search_context, or acp_context_recap — user wildcards already govern those three today. Under current semantics compress is the only ACP tool where a user wildcard is ignored, purely because ACP has a configurable knob for it. That asymmetry cuts toward option 2.

    Duplicate screen: no other open issue/PR covers this. The family is now complete and accounted for: #457 (exact key, acp_status) + #465 (wildcard, acp_status) → both fixed in PR #466; PR #469 (fix(v2)) handles the same root cause on the retired V2 path; #472 is the last instance on the live 1.x path.

    Assessment

    Option 1's core rationale — "a blanket wildcard is unlikely to express fine-grained intent for ACP's internal tool" — applies with equal force to acp_status, yet #457/#465 decided the wildcard wins there. Accepting the principle for one tool and rejecting it for its sibling is exactly the incoherent split this issue flags.

    The deeper point: the host resolver treats "compress": "ask" and "*": "ask" identically — both are "a user rule resolves compress". ACP's defer guard asks a different question ("is there an exact key?") than the system it writes into ("does any rule resolve?"). That divergence is the very root-cause family we just judged wrong twice.

    Blast radius is provably minimal — every cell checked:

    User config Today Option 2
    own compress key (any value form) defer defer — unchanged (anyRuleResolves short-circuits on key presence first)
    no rule resolves compress ACP writes knob value (+ acp_status default) unchanged
    wildcard deny resolving compress self-disable flips knob to deny, tool unregistered; appended entry inert defer + self-disable — same outcome
    wildcard allow resolving compress appended allow wins last-match → allow nothing written; host resolves allow — same effective result
    wildcard ask resolving compress (e.g. "*": "ask") appended allow wins → silent ask→allow ❌ nothing written; host resolves ask ✅

    The only effective behavior change: users with a wildcard ask covering compress start getting prompted — a tightening that matches their stated posture. Anyone wanting today's behavior can pin an explicit compress: "allow" key, which defers under both options. On prompt fatigue: a user who set "*": "ask" is already confirmed on every bash/edit/write call; one more confirmation class is consistent with their choice, not a new kind of friction.

    Recommendation: option 2

    Agree with the issue's recommendation. It completes the #457/#465 principle (ACP never writes a decision over a user rule that resolves the tool) instead of carving out an exception for one tool, and aligns compress with how all of ACP's other tools already behave under wildcards.

    Implementation plan if you approve (small, surgical):

    1. lib/host-permissions.ts — one-line guard swap: hasExplicitToolPermission(…, "compress") → anyRuleResolves(…, "compress"); update the doc comment (it currently claims "An explicit user compress rule means the user owns the map").
    2. tests/host-permissions.test.ts — RED→GREEN per project convention: wildcard-ask defer, wildcard-allow defer, non-matching wildcard still gets the write, own-key cases unchanged.
    3. Behavior-change disclosure (old→new table above) in the PR body + devlog update.
    4. Home: fold into PR fix: respect user decisions on permission.acp_status in host config write (#457, #465) #466 — it's still open, same branch, same function, same root-cause family; keeps the family in one review cycle (one-root-cause-one-PR). Alternative: separate PR after fix: respect user decisions on permission.acp_status in host config write (#457, #465) #466 merges — folding is cleaner, but yours to choose.

    Waiting on two decisions: (a) option 1 vs 2, (b) PR #466 vs a separate PR.

    中文摘要:已在 PR #466 当前 head(087e25cf,未合并)上核实残留属实——compress 的 defer 守卫仍是 key-presence,而 acp_status 已改为规则解析;建议采纳方案 2(defer 扩展到任何能 resolve compress 的用户规则),唯一实际行为变化是通配符 ask 不再被静默升级为 allow(deny/allow 路径均不变),实现为一行守卫替换 + 测试,建议并入尚未合并的 PR #466;尚未改任何代码,等你拍板。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions