feat(capture): I1 sovereignty gate + server-derived origin (#53)
Adds the trust-zone Origin to CaptureContext and the I1 gate to the use-case: a confidential effective classification through a us-nexus origin is refused before ANY write (ErrSovereigntyRefused), and the refusal is itself audited. A caller-asserted harness label that names a different zone than the server-derived origin is logged as a security event — context.Harness is descriptive-only, never a gate input. The gate triggers only on an explicit ZoneUSNexus, so the unset default (ZoneUnknown) can never make it fire on caller-controllable input; the REST adapter always sets a concrete zone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,13 +15,44 @@
|
||||
// (stricter wins), best-effort orchestration, and the partial receipt.
|
||||
package capture
|
||||
|
||||
// Zone is the trust zone a capture originates from, server-derived from
|
||||
// the authenticated principal (spec §4.2 / I1). It is NEVER taken from
|
||||
// caller input — context.Harness is descriptive telemetry only.
|
||||
type Zone int
|
||||
|
||||
const (
|
||||
// ZoneUnknown means the origin was not set. The REST adapter always
|
||||
// sets a concrete zone; the service treats Unknown as "not gated" (only
|
||||
// an explicit ZoneUSNexus triggers the I1 refusal) so the gate can
|
||||
// never fire on a caller-controllable default.
|
||||
ZoneUnknown Zone = iota
|
||||
// ZoneSovereign is sovereign soil (homelab / Tailscale CLI callers).
|
||||
ZoneSovereign
|
||||
// ZoneUSNexus is a non-sovereign US-jurisdiction surface (e.g.
|
||||
// claude.ai). Confidential captures through it are refused (I1).
|
||||
ZoneUSNexus
|
||||
)
|
||||
|
||||
// String renders the zone for audit/refusal messages.
|
||||
func (z Zone) String() string {
|
||||
switch z {
|
||||
case ZoneSovereign:
|
||||
return "sovereign-soil"
|
||||
case ZoneUSNexus:
|
||||
return "us-nexus"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// CaptureContext is the per-session metadata accompanying a capture.
|
||||
//
|
||||
// Classification is the caller-declared sensitivity (model C, spec §4.1):
|
||||
// the server independently derives the target's classification and gates
|
||||
// on the stricter of the two. Principal is server-derived from the
|
||||
// authenticated identity (#53 populates it); it is never caller-asserted.
|
||||
// Harness is descriptive telemetry only — never a gate input.
|
||||
// on the stricter of the two. Principal and Origin are server-derived from
|
||||
// the authenticated identity (the REST adapter populates them); they are
|
||||
// never caller-asserted. Harness is descriptive telemetry only — never a
|
||||
// gate input.
|
||||
type CaptureContext struct {
|
||||
Harness string
|
||||
SessionRef string
|
||||
@@ -29,6 +60,7 @@ type CaptureContext struct {
|
||||
Actor string
|
||||
Classification string // caller-declared level token ("" = unspecified)
|
||||
Principal string // server-derived (auth); audit identity
|
||||
Origin Zone // server-derived trust zone; the I1 gate input
|
||||
}
|
||||
|
||||
// Insight is one piece of session knowledge bound for the brain. A
|
||||
|
||||
@@ -34,6 +34,33 @@ func NewService(b BrainStore, tr IssueTracker, sw SummaryWriter, p Classificatio
|
||||
|
||||
var validActions = map[string]bool{"create": true, "close": true, "comment": true}
|
||||
|
||||
// ErrSovereigntyRefused is returned when the I1 gate refuses a capture
|
||||
// (confidential effective classification through a us-nexus origin). The
|
||||
// REST adapter maps it to HTTP 403. Callers test with errors.Is.
|
||||
var ErrSovereigntyRefused = fmt.Errorf("capture refused by I1 sovereignty gate")
|
||||
|
||||
// assertedZoneMismatch returns a security-event string when the caller's
|
||||
// harness label asserts a trust zone that contradicts the server-derived
|
||||
// origin. A harness label that names no zone (the normal case, e.g.
|
||||
// "claude-code") returns "". The label is never used as a gate input —
|
||||
// this only flags the discrepancy for the audit trail.
|
||||
func assertedZoneMismatch(harness string, derived Zone) string {
|
||||
var asserted Zone
|
||||
switch strings.ToLower(strings.TrimSpace(harness)) {
|
||||
case "sovereign-soil", "sovereign":
|
||||
asserted = ZoneSovereign
|
||||
case "us-nexus", "usnexus":
|
||||
asserted = ZoneUSNexus
|
||||
default:
|
||||
return "" // no zone claim
|
||||
}
|
||||
if asserted != derived {
|
||||
return fmt.Sprintf("asserted-vs-derived origin mismatch: harness asserted %s, principal resolves to %s",
|
||||
asserted, derived)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Capture runs the use-case: validate (fail-closed), resolve effective
|
||||
// classification (stricter of declared vs target-derived), then persist
|
||||
// insights → tickets → summary best-effort, emit an audit record, and
|
||||
@@ -57,6 +84,32 @@ func (s *Service) Capture(ctx context.Context, in CaptureInput) (CaptureReceipt,
|
||||
|
||||
effective, securityEvents := 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
|
||||
// security event (spec §4.2: a control keyed on attacker-suppliable
|
||||
// input is not a control).
|
||||
if ev := assertedZoneMismatch(in.Context.Harness, in.Context.Origin); ev != "" {
|
||||
securityEvents = append(securityEvents, ev)
|
||||
}
|
||||
|
||||
// I1 sovereignty gate: a confidential capture through a us-nexus origin
|
||||
// is refused before ANY write. The refusal itself is audited (best
|
||||
// effort) — refusals must be reconstructable too.
|
||||
if effective == classification.Confidential && in.Context.Origin == ZoneUSNexus {
|
||||
_ = s.audit.Record(ctx, AuditEntry{
|
||||
Timestamp: s.now().UTC(),
|
||||
Principal: in.Context.Principal,
|
||||
Actor: in.Context.Actor,
|
||||
Harness: in.Context.Harness,
|
||||
SessionRef: in.Context.SessionRef,
|
||||
EffectiveClassification: effective.String(),
|
||||
Items: nil, // refused before any write
|
||||
SecurityEvents: append(securityEvents, "I1 refusal: confidential capture via us-nexus origin"),
|
||||
})
|
||||
return CaptureReceipt{}, fmt.Errorf("%w: effective classification confidential through %s origin",
|
||||
ErrSovereigntyRefused, in.Context.Origin)
|
||||
}
|
||||
|
||||
receipt := CaptureReceipt{
|
||||
Errors: []ItemError{},
|
||||
EffectiveClassification: effective.String(),
|
||||
|
||||
@@ -329,3 +329,82 @@ func TestCaptureSummaryPathAndFidelity(t *testing.T) {
|
||||
assert.Contains(t, sw.paths[0], "session-wrap")
|
||||
assert.Contains(t, sw.content[0], "fidelity: transcript-parse", "fidelity stamped in frontmatter")
|
||||
}
|
||||
|
||||
// --- I1 sovereignty gate (#53) ---
|
||||
|
||||
func TestCaptureRefusesConfidentialViaUSNexus(t *testing.T) {
|
||||
b := &fakeBrain{}
|
||||
tr := &fakeTracker{}
|
||||
au := &fakeAudit{}
|
||||
pol := fakePolicy{tags: map[string]classification.Level{"client-seb": classification.Confidential}}
|
||||
svc := newSvc(b, tr, nil, pol, au)
|
||||
|
||||
ctx := baseCtx()
|
||||
ctx.Classification = "confidential"
|
||||
ctx.Origin = ZoneUSNexus
|
||||
_, err := svc.Capture(context.Background(), CaptureInput{
|
||||
Context: ctx,
|
||||
Insights: []Insight{{Text: "x", Wing: "client-seb", Hall: "facts"}},
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrSovereigntyRefused)
|
||||
// Refused before any write.
|
||||
assert.Empty(t, b.writes)
|
||||
assert.Empty(t, tr.created)
|
||||
// Refusal is audited.
|
||||
require.Len(t, au.entries, 1)
|
||||
assert.Empty(t, au.entries[0].Items, "no items landed on refusal")
|
||||
}
|
||||
|
||||
func TestCaptureAllowsConfidentialViaSovereign(t *testing.T) {
|
||||
b := &fakeBrain{}
|
||||
pol := fakePolicy{tags: map[string]classification.Level{"client-seb": classification.Confidential}}
|
||||
svc := newSvc(b, &fakeTracker{}, nil, pol, &fakeAudit{})
|
||||
|
||||
ctx := baseCtx()
|
||||
ctx.Classification = "confidential"
|
||||
ctx.Origin = ZoneSovereign
|
||||
rec, err := svc.Capture(context.Background(), CaptureInput{
|
||||
Context: ctx,
|
||||
Insights: []Insight{{Text: "x", Wing: "client-seb", Hall: "facts"}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, rec.Insights[0].OK)
|
||||
assert.Len(t, b.writes, 1)
|
||||
}
|
||||
|
||||
func TestCaptureAssertedLabelIgnoredAndLogged(t *testing.T) {
|
||||
// Caller asserts harness "sovereign-soil" but principal resolves to
|
||||
// us-nexus; confidential ⇒ refused, and the discrepancy is a security event.
|
||||
au := &fakeAudit{}
|
||||
pol := fakePolicy{tags: map[string]classification.Level{"client-seb": classification.Confidential}}
|
||||
svc := newSvc(&fakeBrain{}, &fakeTracker{}, nil, pol, au)
|
||||
|
||||
ctx := baseCtx()
|
||||
ctx.Harness = "sovereign-soil" // asserted
|
||||
ctx.Origin = ZoneUSNexus // server-derived
|
||||
ctx.Classification = "confidential"
|
||||
_, err := svc.Capture(context.Background(), CaptureInput{
|
||||
Context: ctx,
|
||||
Insights: []Insight{{Text: "x", Wing: "client-seb", Hall: "facts"}},
|
||||
})
|
||||
require.ErrorIs(t, err, ErrSovereigntyRefused)
|
||||
require.Len(t, au.entries, 1)
|
||||
joined := strings.Join(au.entries[0].SecurityEvents, " | ")
|
||||
assert.Contains(t, joined, "asserted-vs-derived origin mismatch")
|
||||
assert.Contains(t, joined, "I1 refusal")
|
||||
}
|
||||
|
||||
func TestCaptureInternalViaUSNexusAllowed(t *testing.T) {
|
||||
// us-nexus origin is fine for non-confidential data.
|
||||
b := &fakeBrain{}
|
||||
svc := newSvc(b, &fakeTracker{}, nil, fakePolicy{}, &fakeAudit{})
|
||||
ctx := baseCtx()
|
||||
ctx.Origin = ZoneUSNexus // internal classification, so gate doesn't fire
|
||||
rec, err := svc.Capture(context.Background(), CaptureInput{
|
||||
Context: ctx,
|
||||
Insights: []Insight{{Text: "x", Wing: "hyperguild", Hall: "facts"}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, rec.Insights[0].OK)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user