Skip to content

Tool-call guardrails not enforced: skillConstraints skill-identity unset + approvalGates Review decision dropped #289

Description

@initializ-mk

Two related defects in how Forge drives the guardrails tool-call gate (forge-cli/runtime/guardrails_engine.go: CheckToolCall). Both cause a configured control to silently not enforce.

1. skillConstraints — skill identity is never passed (deny/allow lists don't work)

The library's SkillEvaluator (guardrails skill_evaluator.go) keys on the tool-call payload's SkillID (falling back to SkillName). Forge's CheckToolCall builds a ToolCallRequest with only ToolName set — SkillID/SkillName are left empty.

IsSkillAllowed("") therefore:

  • blockedSkills matches nothing — the empty id is never in the denylist, so a skill an operator explicitly blocked still runs.
  • a non-empty allowedSkills blocks every tool call — the empty id isn't in the allowlist → not_allowed → block.

So skillConstraints is a footgun: the denylist is silent-no-op, and the allowlist is a global kill-switch. An operator who sets blockedSkills: [shell-runner] gets no enforcement and no error — a security-relevant silent failure.

Fix: populate SkillName (and/or SkillID) on the ToolCallRequest — e.g. map the invoked tool's owning skill, or at minimum pass ToolName as the skill identity so the lists key on something real. Add a test that a blocked skill is denied and an allowlist admits only its members.

2. approvalGates — the Review decision is dropped (human approval silently doesn't fire)

When an approval condition matches, the library returns DecisionReview ("requires human approval"). Forge's CheckToolCall (and the inbound/outbound checks) switch only on DecisionMask / DecisionBlockDecisionReview falls through to allow. So action: require_human_approval in approvalGates is a no-op in Forge.

(Compounding: approval matching runs inside SkillEvaluator, so it also depends on the empty skill identity from #1; and the library's GetApprovalCondition currently returns the first non-empty condition without parsing the condition expression — an upstream guardrails limitation.)

Fix: map DecisionReview onto Forge's existing human-in-the-loop machinery — the R4c DEFER engine (deferpolicy, #211) is the natural target: a guardrails Review at the tool-call gate should pause-and-resume via POST /tasks/{id}/decisions exactly like a policy-declared defer. Until then, approvalGates should be documented as unsupported so operators don't rely on it.

Impact

Both are silent non-enforcement of a configured security control — the worst failure mode. Neither errors; the operator believes the control is active.

Discovered while evaluating the guardrails sections for #284 / the platform overlay. Related: #211 (DEFER), guardrails skill_evaluator.go.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingforge-cliAffects the forge-cli command-line tool (init, run, build, mcp commands)securitySecurity vulnerability fixes

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions