Documentation
¶
Overview ¶
Package enforce is the harness's refusal seam: it sits on the proxy's interception hook, decides each decidable client request against the policy engine, and — on a denial — synthesizes the refusal in the exact shape the method requires, so the governed server never sees a request the harness refused.
This is the enforcing half of moving adjudication out of the server. The verdict is reached in the harness process, on the wire, using the tool index the observer built from the manifest; the server is not consulted and cannot override it. Two shape rules are load-bearing and match the in-process enforcer exactly: a refused tools/call is an IsError result (a JSON-RPC success frame carrying isError), so the model can read the reason and adapt; a refused resources/read, prompts/get, completion/complete or subscriptions/listen is a JSON-RPC error, because those methods have no IsError envelope and a CallToolResult would be the wrong shape on the wire.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AllowAll ¶ added in v0.4.0
type AllowAll struct{}
AllowAll forwards every decidable request. It is the inner decider behind a manifest gate when no policy layers are present: the manifest bounds the session while the policy stack has nothing to say.
type Decider ¶
type Decider interface {
Decide(ctx context.Context, method string, params json.RawMessage) Decision
}
Decider reaches a verdict on one decidable request. It is the policy engine (behind the manifest gate) in production and a fake in tests.
func NewManifestGate ¶ added in v0.4.0
func NewManifestGate( s *manifest.Session, inner Decider, a *audit.AuditLog, logger *slog.Logger, now func() time.Time, ) Decider
NewManifestGate wraps the inner decider with the session's bounds. A nil session gates nothing and returns the inner decider unchanged, so callers can compose unconditionally.
type Decision ¶ added in v0.4.0
Decision is a decider's answer for one decidable request. Code, when set, is a typed refusal code from the wire package — the machine-readable half of a manifest or session-lifetime refusal. Policy-rule denials carry only a reason (the signal and rule that failed explain them); the empty code keeps their refusal text exactly as the in-process enforcer renders it.
type DenyAll ¶
type DenyAll struct{ Reason string }
DenyAll refuses every decidable request with a fixed reason. It is the fail-closed decider installed while the policy engine is still initializing (before the servant has connected and its signals are reachable): an operator who configured enforcement must not get an unadjudicated window, so decidable calls are refused until the real decider is ready. Non-decidable traffic — initialize, tools/list, notifications — is unaffected, because the Interceptor never consults the decider for those.
type Interceptor ¶
type Interceptor struct {
// contains filtered or unexported fields
}
Interceptor implements proxy.Interceptor over a Decider. It only ever refuses the five decidable request methods; everything else it forwards untouched by returning nil.
func NewInterceptor ¶
func NewInterceptor(d Decider, logger *slog.Logger) *Interceptor
NewInterceptor builds the refusal seam.
func (*Interceptor) Intercept ¶
func (i *Interceptor) Intercept(raw []byte) []byte
Intercept decides a client frame. A decidable request with a readable id is put to the decider; a denial is answered in the method's shape and the request is dropped. Everything else returns nil (forward).
type ManifestGate ¶ added in v0.4.0
type ManifestGate struct {
// contains filtered or unexported fields
}
ManifestGate enforces the session manifest ahead of the inner decider: the two session clocks first (an out-of-time session drains with a typed refusal, whatever the request), then the grant lists for the method, then the wrapped policy decision. The idle clock is touched only for requests the whole chain allowed — a refused call must not keep a session alive.
The gate enforces whenever a manifest is present, regardless of policy mode: a manifest has no audit mode, it is a grant, and a grant that only logged its bounds would not bound anything.
func (*ManifestGate) Decide ¶ added in v0.4.0
func (g *ManifestGate) Decide(ctx context.Context, method string, params json.RawMessage) Decision
Decide applies clocks, grant, then the inner decider.
type PolicyDecider ¶
type PolicyDecider struct {
// contains filtered or unexported fields
}
PolicyDecider adapts the rule-layer engines to the Decider interface. It builds the subject for the method from the request params — using the tool index for a tools/call, and the read-only data-egress subject for the other four, matching the in-process enforcer's subjectFor — evaluates every layer, and takes the strictest verdict (manifest.MaxVerdict): rules are never merged into one attribution space, per the layering semantics. In audit mode the engines cap severity below deny, so Allowed() is true and the call forwards while the intended verdict is still recorded: the harness observes without refusing, exactly as a standalone server does under an audit policy.
func NewPolicyDecider ¶
func NewPolicyDecider(engines []*policy.Engine, a *audit.AuditLog, logger *slog.Logger) *PolicyDecider
NewPolicyDecider builds the engine-backed decider over the rule layers, in composition order. With no engines every request is allowed: no layers, no rules.
func (*PolicyDecider) Decide ¶
func (d *PolicyDecider) Decide(ctx context.Context, method string, params json.RawMessage) Decision
Decide evaluates the request against every layer and records the strictest verdict.