- 5 independent flags (MULTI_INTENT_U11_LIVE_REVIEW/FINAL/PROPOSAL, U12_LIVE_SWITCH/RESTART), default OFF; no full rollout - DocGovLiveActionKeys/RunControlLiveActionKeys become flag-driven functions; baseline keys unchanged - DocGovOwner routes review.approve/reject, final.approve/reject, proposal.decide to real engine with slot-driven reason/approve parsing and idempotent receipts - RunControlOwner routes workflow.switch -> ActivateWorkflow and run.restart -> fresh CreateAndStart run - policy: 7 keys promoted from TierFull to TierConfirm (real execution still gated per-key) - tests: default-off isolation, per-key independence, live state advance reviewing->approving, dry-run zero-advance when flag off
161 lines
4.3 KiB
Go
161 lines
4.3 KiB
Go
package intentflow
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"chat-agent-system/internal/intent"
|
|
)
|
|
|
|
const PolicyVersion = "batch-action-policy.v1"
|
|
|
|
// ActionBatchPolicy is the intentflow-owned batch row. It must not copy
|
|
// Risk, RequiredSlots, CapabilityKey, or AllowedStates from ActionRegistry.
|
|
type ActionBatchPolicy struct {
|
|
ActionKey string
|
|
RequiresBatchConfirm bool
|
|
OpenFromTier PlanTier
|
|
AllowedModes []ExecutionMode
|
|
RejectPlan bool
|
|
}
|
|
|
|
// BatchActionPolicy is the versioned fail-closed matrix keyed by ActionSpec.Key.
|
|
type BatchActionPolicy struct {
|
|
Version string
|
|
byKey map[string]ActionBatchPolicy
|
|
}
|
|
|
|
func DefaultBatchActionPolicy() *BatchActionPolicy {
|
|
p, err := NewBatchActionPolicy(defaultBatchRows(), intent.DefaultRegistry())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func NewBatchActionPolicy(rows []ActionBatchPolicy, registry *intent.ActionRegistry) (*BatchActionPolicy, error) {
|
|
if registry == nil {
|
|
return nil, fmt.Errorf("intentflow: action registry is required")
|
|
}
|
|
byKey := make(map[string]ActionBatchPolicy, len(rows))
|
|
for _, row := range rows {
|
|
if row.ActionKey == "" {
|
|
return nil, fmt.Errorf("intentflow: empty policy action key")
|
|
}
|
|
if _, exists := byKey[row.ActionKey]; exists {
|
|
return nil, fmt.Errorf("intentflow: duplicate policy key %q", row.ActionKey)
|
|
}
|
|
if _, ok := registry.Get(row.ActionKey); !ok {
|
|
return nil, fmt.Errorf("intentflow: unknown policy action key %q", row.ActionKey)
|
|
}
|
|
byKey[row.ActionKey] = row
|
|
}
|
|
for _, spec := range registry.All() {
|
|
if spec == nil {
|
|
continue
|
|
}
|
|
if _, ok := byKey[spec.Key]; !ok {
|
|
return nil, fmt.Errorf("intentflow: action %q missing batch policy", spec.Key)
|
|
}
|
|
}
|
|
return &BatchActionPolicy{Version: PolicyVersion, byKey: byKey}, nil
|
|
}
|
|
|
|
func (p *BatchActionPolicy) Lookup(key string) (ActionBatchPolicy, error) {
|
|
if p == nil {
|
|
return ActionBatchPolicy{}, fmt.Errorf("intentflow: policy is nil")
|
|
}
|
|
row, ok := p.byKey[key]
|
|
if !ok {
|
|
return ActionBatchPolicy{}, fmt.Errorf("intentflow: unknown action key %q", key)
|
|
}
|
|
return row, nil
|
|
}
|
|
|
|
func (p *BatchActionPolicy) RequiresBatchConfirm(key string) bool {
|
|
row, err := p.Lookup(key)
|
|
if err != nil {
|
|
return true
|
|
}
|
|
return row.RequiresBatchConfirm || row.RejectPlan
|
|
}
|
|
|
|
func defaultBatchRows() []ActionBatchPolicy {
|
|
autoRead := []string{
|
|
"regulation.query",
|
|
"case.query",
|
|
"permission.query",
|
|
"document.reject_reason.query",
|
|
"workflow.status.query",
|
|
"document.status.query",
|
|
"run.status.query",
|
|
"project.advisory",
|
|
"conversation.help",
|
|
}
|
|
confirmGenerate := []string{"task.run", "task.run.adhoc"}
|
|
rows := make([]ActionBatchPolicy, 0, len(intent.DefaultRegistry().All()))
|
|
seen := map[string]struct{}{}
|
|
add := func(row ActionBatchPolicy) {
|
|
rows = append(rows, row)
|
|
seen[row.ActionKey] = struct{}{}
|
|
}
|
|
for _, key := range autoRead {
|
|
add(ActionBatchPolicy{
|
|
ActionKey: key,
|
|
RequiresBatchConfirm: false,
|
|
OpenFromTier: TierAutoRead,
|
|
AllowedModes: []ExecutionMode{ModeLive, ModeValidatedOnly},
|
|
})
|
|
}
|
|
for _, key := range confirmGenerate {
|
|
add(ActionBatchPolicy{
|
|
ActionKey: key,
|
|
RequiresBatchConfirm: true,
|
|
OpenFromTier: TierConfirm,
|
|
AllowedModes: []ExecutionMode{ModeValidatedOnly, ModeLive},
|
|
})
|
|
}
|
|
for _, key := range []string{
|
|
"project.fact.supplement",
|
|
"document.claim",
|
|
"document.review.submit",
|
|
"workflow.run",
|
|
"run.resume",
|
|
// P8 逐键放量:confirm 档即可进入计划(真实执行仍受 per-key LIVE flag 限制;
|
|
// flag 未开时 Owner 落 dry-run validated_only)。
|
|
"document.review.approve",
|
|
"document.review.reject",
|
|
"document.final.approve",
|
|
"document.final.reject",
|
|
"document.proposal.decide",
|
|
"workflow.switch",
|
|
"run.restart",
|
|
} {
|
|
add(ActionBatchPolicy{
|
|
ActionKey: key,
|
|
RequiresBatchConfirm: true,
|
|
OpenFromTier: TierConfirm,
|
|
AllowedModes: []ExecutionMode{ModeValidatedOnly, ModeLive},
|
|
})
|
|
}
|
|
add(ActionBatchPolicy{
|
|
ActionKey: "security.forbidden",
|
|
RejectPlan: true,
|
|
OpenFromTier: TierFull,
|
|
})
|
|
for _, spec := range intent.DefaultRegistry().All() {
|
|
if spec == nil {
|
|
continue
|
|
}
|
|
if _, ok := seen[spec.Key]; ok {
|
|
continue
|
|
}
|
|
add(ActionBatchPolicy{
|
|
ActionKey: spec.Key,
|
|
RequiresBatchConfirm: true,
|
|
OpenFromTier: TierFull,
|
|
AllowedModes: []ExecutionMode{ModeValidatedOnly},
|
|
})
|
|
}
|
|
return rows
|
|
}
|