Splits the audit port into Reserve (before any write) + Record (after), so "confidential + sink-down → refuse before any write" is literally true even though the audit record — which lists what landed — can only be written afterwards. - AuditSink.Reserve(ctx, level) → AuditOutcome | error. The error path refuses the capture before writing: confidential + central sink down, or the all-tiers floor (nothing can record). - AuditSink.Record(ctx, entry, outcome) persists per the reserved outcome. - Service: I5 gate runs after the I1 gate and after the dry-run short-circuit (dry-run never probes the sink). AuditBuffered surfaces on the receipt. New ErrAuditUnavailable sentinel (→ HTTP 503). The tier→behaviour decision lives in the sink impl (#54's DegradingSink), not the service — the service just honours Reserve's verdict. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
149 lines
5.4 KiB
Go
149 lines
5.4 KiB
Go
// Package capture is the Clean-Architecture use-case for the uniform
|
|
// capture capability (issue #49/#51): persist a finished session's
|
|
// valuable output — insights → brain, action items → Gitea tickets,
|
|
// optional summary → ai-sessions — with one invocation, identical core
|
|
// behaviour across every harness.
|
|
//
|
|
// This package is pure orchestration. It depends only on ports
|
|
// (interfaces) and plain entities — no HTTP, no live Gitea, no embedding
|
|
// or audit I/O. The real adapters are wired in #52 (Gitea tracker), #53
|
|
// (REST + I1 origin gate), and #54/#55 (audit path + relay). The I1
|
|
// sovereignty refusal and the classification-aware audit degradation are
|
|
// deliberately NOT here — those need the server-derived principal origin
|
|
// (#53) and the loki/buffer machinery (#54). What lives here is everything
|
|
// testable against fakes: validation, effective-classification resolution
|
|
// (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 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
|
|
Fidelity string
|
|
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
|
|
// non-empty SupersedeSlug routes to Update (revise in place); otherwise
|
|
// Write (create).
|
|
type Insight struct {
|
|
Text string
|
|
Wing string
|
|
Hall string
|
|
SupersedeSlug string
|
|
}
|
|
|
|
// Ticket is one action item bound for a Gitea repo. Owner is always the
|
|
// operator (set by the tracker adapter), never carried here.
|
|
type Ticket struct {
|
|
Repo string
|
|
Action string // create | close | comment
|
|
Number int // required for close/comment
|
|
Title string // required for create
|
|
Body string
|
|
}
|
|
|
|
// Summary is an optional session summary bound for ai-sessions.
|
|
type Summary struct {
|
|
Title string
|
|
Body string
|
|
ReposTouched []string
|
|
}
|
|
|
|
// CaptureInput is the whole capture request.
|
|
type CaptureInput struct {
|
|
Context CaptureContext
|
|
Insights []Insight
|
|
Tickets []Ticket
|
|
Summary *Summary
|
|
DryRun bool
|
|
}
|
|
|
|
// InsightResult is the per-insight outcome in the receipt.
|
|
type InsightResult struct {
|
|
ID string `json:"id,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
ContentHash string `json:"content_hash,omitempty"`
|
|
Superseded bool `json:"superseded"`
|
|
OK bool `json:"ok"`
|
|
}
|
|
|
|
// TicketResult is the per-ticket outcome in the receipt.
|
|
type TicketResult struct {
|
|
Repo string `json:"repo"`
|
|
Number int `json:"number,omitempty"`
|
|
Action string `json:"action"`
|
|
URL string `json:"url,omitempty"`
|
|
OK bool `json:"ok"`
|
|
}
|
|
|
|
// SummaryResult is the summary outcome in the receipt.
|
|
type SummaryResult struct {
|
|
Path string `json:"path,omitempty"`
|
|
OK bool `json:"ok"`
|
|
}
|
|
|
|
// ItemError pins a failure to a specific request item for the partial
|
|
// receipt. Item is a stable locator like "insight[1]" or "ticket[0]".
|
|
type ItemError struct {
|
|
Item string `json:"item"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// CaptureReceipt is the structured, partial-aware result. Per-item ok
|
|
// flags plus a flat Errors list make partial success explicit; the
|
|
// caller never has to infer what landed.
|
|
type CaptureReceipt struct {
|
|
Insights []InsightResult `json:"insights"`
|
|
Tickets []TicketResult `json:"tickets"`
|
|
Summary *SummaryResult `json:"summary,omitempty"`
|
|
Errors []ItemError `json:"errors"`
|
|
EffectiveClassification string `json:"effective_classification,omitempty"`
|
|
DryRun bool `json:"dry_run"`
|
|
// AuditBuffered is true when the central audit sink was unreachable and
|
|
// this capture's audit record was written to the durable local buffer
|
|
// instead (internal/public tier). Surfaces the degraded state to the
|
|
// caller per §4.4.
|
|
AuditBuffered bool `json:"audit_buffered,omitempty"`
|
|
}
|