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
-
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
}
-
Change GuardrailChecker.CheckInbound / CheckOutbound signatures to return (PolicyResult, error) instead of error.
-
Move the redact loop out of SkillGuardrailEngine.CheckCommandOutput into a shared applyOutputPolicy(content, filters) PolicyResult used by every guardrail call site.
-
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
Requirement (governance framework, MUST)
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):Gap
The MODIFY decision does NOT apply to:
guardrails.CheckOutbound— only DENYs)guardrails.CheckInbound— only DENYs)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
Introduce a
PolicyDecisiontype inforge-core/runtime:Change
GuardrailChecker.CheckInbound/CheckOutboundsignatures to return(PolicyResult, error)instead oferror.Move the redact loop out of
SkillGuardrailEngine.CheckCommandOutputinto a sharedapplyOutputPolicy(content, filters) PolicyResultused by every guardrail call site.Update every audit event emitter to record
decisionfrom thePolicyResult.Effort
~1 day (mostly threading the new return type through call sites; the redact mechanism already exists).
Acceptance criteria
PolicyDecision+PolicyResulttypes introducedCheckInbound/CheckOutboundreturnPolicyResultdecisionfield