Skip to content

resolveApprovals

resolveApprovals(config, command): ResolvedApprovals

Defined in: _worktrees/docs-release/gaunt-sloth/packages/core/src/config/shell-policy.ts:827

CFG-27 — resolve the effective ResolvedApprovals for the active command.

There is no defaults matrix: §1.1 makes auto-safe the default in every context, so this resolver neither detects nor accepts a “context”. Precedence is the only thing it decides, and §9.1 splits it in two:

  • The scalars — mode, rater, raterTimeoutMs — are replaced when the per-command value states them and inherited from the root when it does not. So the scalar sugar "code": { "approvals": "bypass" } is exactly { mode: 'bypass' } merged over the root: it sets the rung and nothing else.
  • deny and escalate never replace: they CONCATENATE across every scope. A command-specific deny adds to the root’s. Removing an inherited prohibition for one command is deliberately not expressible.
  • allow is REPLACED when the per-command value states its own, and inherited when it does not. A per-command scope may therefore narrow what runs unprompted, and may never widen what is prohibited.
  • mcp (EXT-70 §4.7) follows allow, not the restrictive lists: replaced when the per-command value states it, inherited when it does not. Believing a hint is a PERMISSIVE act in both directions — it can make an allow hint entry fire and can make a deny hint entry stop firing — so it merges the way the permissive list does, and a per-command scope can narrow the session’s trust ("mcp": {} believes nothing) but never inherits half of it by accident. Deep-merging the two scopes’ servers maps was rejected for the same reason: it would leave a deliberately distrustful per-command block silently carrying the root’s trust.

The two halves differ because the costs differ (§3.1), not for tidiness. A missed allow entry escalates and a missed deny entry falls through to the rater — neither is an execution — while a too-broad allow entry runs, unrated and unprompted. Concatenating the restrictive lists fails toward a prompt; concatenating the permissive one fails toward an execution, and would leave a deliberately restrictive per-command rung with no way to shed the root’s standing grants. Do not “regularize” these three into one policy: the direction each list fails in is the whole design.

On the restrictive side the pressure runs the other way (§11.1f). Were the per-command value to replace the root wholesale, the friendliest spelling of “stop asking me about code” would also delete every deny entry — at the one rung where the deny list and the §8 floor are the only checks left. A prohibition a nested config key can quietly delete is not a hardline.

Concatenation order cannot change any outcome (resolveApprovalRules consults every deny entry before any escalate entry and every escalate entry before any allow entry), so root-first is a convention for readability — matching GthAgentRunner.approvalRuleLists, where the declared entries precede the runtime grants — and never a precedence.

Defaults are applied HERE, at the read site, rather than in DEFAULT_CONFIG — so the effective-config snapshot the /config panel renders never churns (à la GS2-34 injectModelContext / GS2-63 output.header).

This is the per-command half. The cross-LAYER half (a project config’s lists adding to a global config’s rather than replacing them) is the additive-array policy in config/loader.ts; both are needed, since either alone still loses a list silently.

Pick<GthConfig, "commands" | "approvals"> | undefined

GthCommand | undefined

The active command; selects the per-command block.

ResolvedApprovals