fix(capture): name what escalated the classification in an I1 refusal (#91)
infra#265 reported "capture with a tickets array fails": 20 tickets
errored, 5 errored, the caller fell back to 20 individual issue_create
calls and filed a bug against the array.
The array was never the failing dimension. Every ticket in that batch
targeted a client repo, and the I1 sovereignty gate refuses a
confidential capture through a us-nexus origin at ANY length — reproduced
live today against brain-mcp: length 1 succeeds for an internal repo,
length 20 validates clean, and a single ticket targeting
trucker-ecommerce is refused.
What made it look like an array bug is that the refusal named neither the
offending target nor the fact that a policy said no:
capture refused by I1 sovereignty gate: effective classification
confidential through us-nexus origin
With twenty tickets a caller cannot tell which repo tripped it, so
"the array is broken" is a reasonable conclusion from the evidence
available. resolveClassification now also returns the targets carrying
the effective level, and the refusal names them:
... through us-nexus origin (required by: trucker-ecommerce)
Only targets that actually reach the effective level are named — at
twenty tickets, listing the innocent ones hides the one that matters —
and a caller who declared confidential themselves is told exactly that,
because "you asked for this" and "a repo you touched required it" call
for different fixes.
Regression tests at length 1 and 20 pin the array path that was never
broken, and a third pins per-item partial success: infra#265 asked "if
ticket 3 of 5 fails, what happened to 1 and 2?" — they land, and the
receipt says so per item.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
This commit is contained in:
@@ -83,7 +83,7 @@ func (s *Service) Capture(ctx context.Context, in CaptureInput) (CaptureReceipt,
|
||||
declared, _ = classification.ParseLevel(in.Context.Classification)
|
||||
}
|
||||
|
||||
effective, securityEvents := s.resolveClassification(declared, in)
|
||||
effective, securityEvents, escalators := s.resolveClassification(declared, in)
|
||||
|
||||
// Server-derived origin governs the I1 gate; a caller-asserted harness
|
||||
// label that names a different zone is descriptive-only and logged as a
|
||||
@@ -107,8 +107,8 @@ func (s *Service) Capture(ctx context.Context, in CaptureInput) (CaptureReceipt,
|
||||
Items: nil, // refused before any write
|
||||
SecurityEvents: append(securityEvents, "I1 refusal: confidential capture via us-nexus origin"),
|
||||
}, AuditCentral)
|
||||
return CaptureReceipt{}, fmt.Errorf("%w: effective classification confidential through %s origin",
|
||||
ErrSovereigntyRefused, in.Context.Origin)
|
||||
return CaptureReceipt{}, fmt.Errorf("%w: effective classification confidential through %s origin (%s)",
|
||||
ErrSovereigntyRefused, in.Context.Origin, escalationReason(escalators))
|
||||
}
|
||||
|
||||
receipt := CaptureReceipt{
|
||||
@@ -221,15 +221,28 @@ func (s *Service) validate(in CaptureInput) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveClassification computes the effective level (stricter of
|
||||
// declared and every target's derived level) and collects a security
|
||||
// event whenever the caller under-declared relative to a target floor.
|
||||
func (s *Service) resolveClassification(declared classification.Level, in CaptureInput) (classification.Level, []string) {
|
||||
// resolveClassification computes the effective level (stricter of declared
|
||||
// and every target's derived level), collects a security event whenever the
|
||||
// caller under-declared relative to a target floor, and names the targets
|
||||
// that carry the strictest level.
|
||||
//
|
||||
// That third return is what makes an I1 refusal actionable. A caller sending
|
||||
// twenty tickets gets one refusal; without the offending target it cannot
|
||||
// tell which repo tripped the gate, and infra#265 shows what happens next —
|
||||
// the caller concluded the tickets ARRAY was broken, built a fallback of
|
||||
// twenty individual calls, and filed a bug against the wrong thing.
|
||||
func (s *Service) resolveClassification(declared classification.Level, in CaptureInput) (classification.Level, []string, []string) {
|
||||
effective := declared
|
||||
var events []string
|
||||
derivedFor := map[string]classification.Level{}
|
||||
var order []string
|
||||
consider := func(kind classification.TargetKind, name string) {
|
||||
derived := s.policy.Derive(classification.Target{Kind: kind, Name: name})
|
||||
effective = classification.Stricter(effective, derived)
|
||||
if _, seen := derivedFor[name]; !seen {
|
||||
derivedFor[name] = derived
|
||||
order = append(order, name)
|
||||
}
|
||||
if declared < derived {
|
||||
events = append(events, fmt.Sprintf("classification under-declared: declared=%s target=%s(%s) derived=%s",
|
||||
declared, name, kindString(kind), derived))
|
||||
@@ -241,7 +254,28 @@ func (s *Service) resolveClassification(declared classification.Level, in Captur
|
||||
for _, tk := range in.Tickets {
|
||||
consider(classification.RepoTarget, tk.Repo)
|
||||
}
|
||||
return effective, events
|
||||
|
||||
// Only targets that actually reach the effective level are named: at
|
||||
// twenty tickets, listing the innocent ones is noise that hides the one
|
||||
// that matters.
|
||||
var escalators []string
|
||||
for _, name := range order {
|
||||
if derivedFor[name] == effective && effective > declared {
|
||||
escalators = append(escalators, name)
|
||||
}
|
||||
}
|
||||
return effective, events, escalators
|
||||
}
|
||||
|
||||
// escalationReason renders why the effective classification landed where it
|
||||
// did, for an operator reading a refusal. Naming the caller's own declaration
|
||||
// matters as much as naming a target: "you asked for this" and "a repo you
|
||||
// touched required it" call for different fixes.
|
||||
func escalationReason(escalators []string) string {
|
||||
if len(escalators) == 0 {
|
||||
return "declared by the caller; no target required it"
|
||||
}
|
||||
return "required by: " + strings.Join(escalators, ", ")
|
||||
}
|
||||
|
||||
func (s *Service) persistInsight(ctx context.Context, ins Insight) (InsightResult, string, error) {
|
||||
|
||||
Reference in New Issue
Block a user