Skip to content

R4a (governance): generalize MODIFY decision beyond cli_execute output #209

Description

@naveen-kurra

Requirement (governance framework, MUST)

The policy engine MUST be capable of producing one of five decisions: ALLOW, DENY, MODIFY, STEP_UP, or DEFER.

Focus of this issue: MODIFY.

Current state

MODIFY is implemented but only for one narrow path — cli_execute output redaction (forge-core/runtime/skill_guardrails.go:145-171):

func (s *SkillGuardrailEngine) CheckCommandOutput(toolName, toolOutput string) (string, error) {
    if toolName != "cli_execute" {
        return toolOutput, nil
    }
    // ...
    switch f.action {
    case "block":
        return "", fmt.Errorf("tool output blocked by skill guardrail")
    case "redact":
        toolOutput = f.re.ReplaceAllString(toolOutput, "[BLOCKED BY POLICY]")
        // ...
    }
}

Gap

The MODIFY decision does NOT apply to:

  • LLM responses (guardrails.CheckOutbound — only DENYs)
  • User prompts (guardrails.CheckInbound — only DENYs)
  • MCP tool outputs (adapter path is unchanged output)
  • Builtin tool outputs (http_request, browser, etc.)

To satisfy R4-MODIFY at the framework's grade, MODIFY must be a general decision the policy engine can emit for any evaluated content — not a special-cased cli_execute path.

Proposed implementation

  1. Introduce a PolicyDecision type in forge-core/runtime:

    type PolicyDecision int
    const (
        DecisionAllow PolicyDecision = iota
        DecisionDeny
        DecisionModify
        DecisionStepUp   // see R4b
        DecisionDefer    // see R4c
    )
    type PolicyResult struct {
        Decision  PolicyDecision
        Modified  string   // valid when Decision == Modify
        Reason    string
    }
  2. Change GuardrailChecker.CheckInbound / CheckOutbound signatures to return (PolicyResult, error) instead of error.

  3. Move the redact loop out of SkillGuardrailEngine.CheckCommandOutput into a shared applyOutputPolicy(content, filters) PolicyResult used by every guardrail call site.

  4. Update every audit event emitter to record decision from the PolicyResult.

Effort

~1 day (mostly threading the new return type through call sites; the redact mechanism already exists).

Acceptance criteria

  • PolicyDecision + PolicyResult types introduced
  • CheckInbound/CheckOutbound return PolicyResult
  • MODIFY applies to LLM responses AND user prompts AND all tool outputs (not just cli_execute)
  • Every guardrail audit event carries a decision field
  • Tests: MODIFY case emitted for a non-cli_execute tool output

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestforge-coreAffects the forge-core library (runtime, security, types, llm, mcp, auth)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