feat: enforce ai_access before Gram-hosted inference - #5976
Conversation
🦋 Changeset detectedLatest commit: b0f69d6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Running ultrareview automatically — This change enforces ai_access authorization at the Gram-hosted provider boundary across 105 files, introducing fail-closed checkpoints, client-construction invariants, and error-telemetry sanitization; a subtle bug could wrongly allow or block model egress, so it warrants a deep multi-pass review.. I'll post findings when complete. |
There was a problem hiding this comment.
Ultrareview completed in 15m 37s
13 issues found across 105 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="server/internal/killswitches/mcptoolexecution/setup_test.go">
<violation number="1" location="server/internal/killswitches/mcptoolexecution/setup_test.go:148">
P3: The doc comment on insertPrescription still claims it always creates an "mcp_server resource kind" prescription, but the new ResourceKind fixture field makes the kind configurable (hosted_inference_test.go passes ResourceKindGramHostedInference). Update the comment to reflect that the resource kind is parameterized and defaults to mcp_server.</violation>
</file>
<file name="server/cmd/riskjudgebench/main.go">
<violation number="1" location="server/cmd/riskjudgebench/main.go:5">
P3: `NewUncheckedUnifiedClient` omits the hosted-inference checkpoint, so this command does not enforce `ai_access` or exercise the provider-boundary behavior implied by the surrounding “prod-equivalent” claim. Since the inventory intentionally permits this standalone path, label it as non-governed here and update `server/cmd/riskjudgebench/README.md`, which still claims `NewUnifiedClient`.</violation>
</file>
<file name="server/internal/killswitches/hostedinference/checkpoint.go">
<violation number="1" location="server/internal/killswitches/hostedinference/checkpoint.go:43">
P2: When a typed-nil evaluator is injected, this guard succeeds and the returned checkpoint panics on its first governed call instead of failing closed. Use a typed-nil-aware dependency check before constructing the checkpoint.</violation>
</file>
<file name="server/internal/risk/impl.go">
<violation number="1" location="server/internal/risk/impl.go:2686">
P2: When an ordinary session auto-generates a policy name, this new governed path still sends empty user attribution. According to linked Linear issue DNO-991, preserve the validated acting user; pass that identity into both naming helpers and their completion requests.</violation>
</file>
<file name="server/internal/contextvalues/context.go">
<violation number="1" location="server/internal/contextvalues/context.go:161">
P1: According to linked Linear issue DNO-991, only authoritative validated session provenance may reach the governed checkpoint. This helper accepts arbitrary nonempty strings and stamps them as trusted acting-user provenance, so another internal path can turn an unvalidated or cross-tenant identity into a governed inference; mint the value through an opaque capability or validated-claim type owned by the JWT verifier instead.</violation>
</file>
<file name="server/internal/killswitches/mcptoolexecution/registration.go">
<violation number="1" location="server/internal/killswitches/mcptoolexecution/registration.go:115">
P1: When a selected-resource `ai_access` prescription targets `gram_hosted_inference`, lifecycle validation rejects it as an unsupported resource, so category-specific hosted-inference controls cannot be managed. According to linked Linear issue DNO-991, active prescriptions must govern Gram-hosted inference; add static-key validation for this resource to the lifecycle validator before registering it here.</violation>
</file>
<file name="server/internal/chat/hosted_inference_service_test.go">
<violation number="1" location="server/internal/chat/hosted_inference_service_test.go:89">
P3: GetObjectCompletion and CreateEmbeddings call the checkpoint but never increment providerAttempts, while GetCompletion/GetCompletionStream do. A test asserting "no provider egress" via providerAttempts on the object-completion or embeddings path would report zero even after a successful delegate call, making the guard silently incomplete. Bump providerAttempts after the check in both methods (and count object calls if completionCalls is meant to be total).</violation>
</file>
<file name="server/internal/contextvalues/context_test.go">
<violation number="1" location="server/internal/contextvalues/context_test.go:106">
P3: The three attribution-class cases in TestValidatedGramSessionActingUser only call SetAuthContext, which never marks the session validated, so ValidatedGramSessionActingUser returns false simply because the context is unvalidated. They never run through WithValidatedGramSession, so the API-key, assistant/chat, and anonymous-organization rejection paths the case names claim to cover are not actually exercised, and a regression in IsOrdinaryGramUserSession's handling of APIKeyID, ExternalUserID, or missing UserID would go undetected. Build the validated context first (WithValidatedGramSession(t.Context(), authCtx, false)) so these cases test the real rejection logic.</violation>
</file>
<file name="server/internal/guardian/policy.go">
<violation number="1" location="server/internal/guardian/policy.go:194">
P2: When an attempt check rejects a request with a non-nil body, this transport returns without closing it. Close `req.Body` before returning the rejection so guarded POST or streaming requests do not leak resources.</violation>
</file>
<file name="server/internal/killswitches/hostedinference/contracts.go">
<violation number="1" location="server/internal/killswitches/hostedinference/contracts.go:51">
P2: Because the server lint configuration runs `gosec`, this unsuppressed `api_key_*` declaration is reported as a hardcoded credential. Add the scoped justification used by the sibling category declarations so the server lint job passes.</violation>
</file>
<file name="server/internal/thirdparty/openrouter/unified_client.go">
<violation number="1" location="server/internal/thirdparty/openrouter/unified_client.go:455">
P1: Custom agent: **Flag Security Vulnerabilities**
When an `ai_access` prescription becomes active after this check returns, `GetCompletion` still sends the request because `makeHTTPRequest` calls `c.httpClient.Do` directly. The checkpointing wrapper is installed only for embeddings, leaving completion and streaming egress outside the immediate provider boundary. According to linked Linear issue DNO-991, route these attempts through a checkpointing `RoundTripper` or `guardian.WithAttemptCheck` so every network attempt is denied before egress.</violation>
<violation number="2" location="server/internal/thirdparty/openrouter/unified_client.go:1011">
P2: When an embedding caller has no context deadline, this new SDK client can wait indefinitely because `WithClient` replaces the SDK's 60-second client timeout with Guardian's zero-timeout client. According to linked Linear issue DNO-991, existing inference behavior must remain unchanged; add the SDK timeout back.</violation>
</file>
<file name="server/internal/killswitches/hostedinference/transport.go">
<violation number="1" location="server/internal/killswitches/hostedinference/transport.go:31">
P2: Evaluator and database failures lose their diagnostic cause from internal reporting. Pass the infrastructure error's underlying `cause` (or log it explicitly) while keeping the public `CodeUnavailable` message generic.</violation>
</file>
Linked issue analysis
Linked issue: DNO-991: feat: enforce ai_access before Gram-hosted inference
| Status | Acceptance criteria | Notes |
|---|---|---|
| ✅ | Classify hosted inference into governed user activity versus internal, background, and unsupported classes. | The PR adds explicit hosted-inference call categories, classification wrappers, an inventory, and coverage checks for unsupported/internal/background paths. |
| ✅ | Propagate an authoritative acting user and tenant without inferring identity from conversation ownership or creator fields. | Only validated ordinary Gram session provenance is accepted, with matching organization, user, and session claims required before attribution is forwarded. |
| ✅ | Evaluate ai_access at a common pre-provider checkpoint for every approved hosted provider call. | Production clients receive an injected checkpoint, and the checkpoint is re-run immediately before provider attempts, including retries and governed redirects. |
| ✅ | Ensure a matching ai_access denial prevents provider egress. | The checkpoint and retry transport hooks reject before network I/O, with tests covering denial and retry behavior. |
| ✅ | Keep missing, spoofed, cross-tenant, and unsupported identity from matching a prescription. | Validated provenance checks require consistent organization, user, and session values, while unsupported attribution classes bypass governed matching. |
| ✅ | Expose surface-specific denial and failure contracts, including safe administrator notes and fail-closed infrastructure errors. | The PR adds ai_access_denied and unavailable contracts, maps them to 403 and 503 responses, preserves supported notes, and sanitizes telemetry. |
| ✅ | Prove coverage of approved provider call sites and leave unsupported/internal inference behavior outside the claims. | A static inventory analyzes production call sites and constructor composition, while explicit wrappers classify assistant, internal, background, and other excluded paths. |
| Preserve existing non-governed inference behavior. | The PR explicitly migrates tests and benchmarks to an unchecked client and adds unchanged unsupported classifications, but the supplied diff does not provide complete evidence for all existing non-governed behavior. |
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| // WithValidatedChatSessionActingUser records provenance from a validated chat | ||
| // JWT whose signed ordinary-session claim matches its nonempty tenant, user, and | ||
| // session claims. Only auth/chatsessions may call this after token validation. | ||
| func WithValidatedChatSessionActingUser(ctx context.Context, organizationID, userID, sessionID string) context.Context { |
There was a problem hiding this comment.
P1: According to linked Linear issue DNO-991, only authoritative validated session provenance may reach the governed checkpoint. This helper accepts arbitrary nonempty strings and stamps them as trusted acting-user provenance, so another internal path can turn an unvalidated or cross-tenant identity into a governed inference; mint the value through an opaque capability or validated-claim type owned by the JWT verifier instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/contextvalues/context.go, line 161:
<comment>According to linked Linear issue DNO-991, only authoritative validated session provenance may reach the governed checkpoint. This helper accepts arbitrary nonempty strings and stamps them as trusted acting-user provenance, so another internal path can turn an unvalidated or cross-tenant identity into a governed inference; mint the value through an opaque capability or validated-claim type owned by the JWT verifier instead.</comment>
<file context>
@@ -81,6 +90,102 @@ func IsLegacyImpersonatedSession(ctx context.Context) bool {
+// WithValidatedChatSessionActingUser records provenance from a validated chat
+// JWT whose signed ordinary-session claim matches its nonempty tenant, user, and
+// session claims. Only auth/chatsessions may call this after token validation.
+func WithValidatedChatSessionActingUser(ctx context.Context, organizationID, userID, sessionID string) context.Context {
+ if organizationID == "" || organizationID == constants.DemoOrganizationID || userID == "" || sessionID == "" {
+ return ctx
</file context>
| Key: DefinitionKeyAIAccess, | ||
| PrincipalKinds: []killswitches.PrincipalKind{PrincipalKindUser}, | ||
| ResourceKinds: []killswitches.ResourceKind{ResourceKindMCPServer, ResourceKindHookActivity, ResourceKindLiteLLMInstance}, | ||
| ResourceKinds: []killswitches.ResourceKind{ResourceKindMCPServer, ResourceKindHookActivity, ResourceKindLiteLLMInstance, hostedinference.ResourceKindGramHostedInference}, |
There was a problem hiding this comment.
P1: When a selected-resource ai_access prescription targets gram_hosted_inference, lifecycle validation rejects it as an unsupported resource, so category-specific hosted-inference controls cannot be managed. According to linked Linear issue DNO-991, active prescriptions must govern Gram-hosted inference; add static-key validation for this resource to the lifecycle validator before registering it here.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/killswitches/mcptoolexecution/registration.go, line 115:
<comment>When a selected-resource `ai_access` prescription targets `gram_hosted_inference`, lifecycle validation rejects it as an unsupported resource, so category-specific hosted-inference controls cannot be managed. According to linked Linear issue DNO-991, active prescriptions must govern Gram-hosted inference; add static-key validation for this resource to the lifecycle validator before registering it here.</comment>
<file context>
@@ -111,13 +112,13 @@ func NewRegistration(db *pgxpool.Pool) (killswitches.Registration, error) {
Key: DefinitionKeyAIAccess,
PrincipalKinds: []killswitches.PrincipalKind{PrincipalKindUser},
- ResourceKinds: []killswitches.ResourceKind{ResourceKindMCPServer, ResourceKindHookActivity, ResourceKindLiteLLMInstance},
+ ResourceKinds: []killswitches.ResourceKind{ResourceKindMCPServer, ResourceKindHookActivity, ResourceKindLiteLLMInstance, hostedinference.ResourceKindGramHostedInference},
FailurePolicy: killswitches.FailurePolicyFailClosed,
DefaultExternalNote: DefaultAIAccessExternalNote,
</file context>
|
|
||
| // GetCompletion makes a non-streaming completion request to OpenRouter and applies capture/tracking strategies. | ||
| func (c *ChatClient) GetCompletion(ctx context.Context, req CompletionRequest) (*CompletionResponse, error) { | ||
| if err := c.checkHostedInference(ctx, req.OrgID); err != nil { |
There was a problem hiding this comment.
P1: Custom agent: Flag Security Vulnerabilities
When an ai_access prescription becomes active after this check returns, GetCompletion still sends the request because makeHTTPRequest calls c.httpClient.Do directly. The checkpointing wrapper is installed only for embeddings, leaving completion and streaming egress outside the immediate provider boundary. According to linked Linear issue DNO-991, route these attempts through a checkpointing RoundTripper or guardian.WithAttemptCheck so every network attempt is denied before egress.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/thirdparty/openrouter/unified_client.go, line 455:
<comment>When an `ai_access` prescription becomes active after this check returns, `GetCompletion` still sends the request because `makeHTTPRequest` calls `c.httpClient.Do` directly. The checkpointing wrapper is installed only for embeddings, leaving completion and streaming egress outside the immediate provider boundary. According to linked Linear issue DNO-991, route these attempts through a checkpointing `RoundTripper` or `guardian.WithAttemptCheck` so every network attempt is denied before egress.</comment>
<file context>
@@ -364,6 +452,9 @@ func (c *ChatClient) requestCompletion(ctx context.Context, apiKey string, reqBo
// GetCompletion makes a non-streaming completion request to OpenRouter and applies capture/tracking strategies.
func (c *ChatClient) GetCompletion(ctx context.Context, req CompletionRequest) (*CompletionResponse, error) {
+ if err := c.checkHostedInference(ctx, req.OrgID); err != nil {
+ return nil, err
+ }
</file context>
| var _ AttemptCheckpoint = (*Checkpoint)(nil) | ||
|
|
||
| func NewCheckpoint(registry *killswitches.Registry, evaluation evaluator, timeout time.Duration) (*Checkpoint, error) { | ||
| if registry == nil || evaluation == nil { |
There was a problem hiding this comment.
P2: When a typed-nil evaluator is injected, this guard succeeds and the returned checkpoint panics on its first governed call instead of failing closed. Use a typed-nil-aware dependency check before constructing the checkpoint.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/killswitches/hostedinference/checkpoint.go, line 43:
<comment>When a typed-nil evaluator is injected, this guard succeeds and the returned checkpoint panics on its first governed call instead of failing closed. Use a typed-nil-aware dependency check before constructing the checkpoint.</comment>
<file context>
@@ -0,0 +1,167 @@
+var _ AttemptCheckpoint = (*Checkpoint)(nil)
+
+func NewCheckpoint(registry *killswitches.Registry, evaluation evaluator, timeout time.Duration) (*Checkpoint, error) {
+ if registry == nil || evaluation == nil {
+ return nil, errors.New("hosted-inference registry and evaluator are required")
+ }
</file context>
| } | ||
|
|
||
| func (s *Service) suggestCustomRuleViaLLM(ctx context.Context, orgID, projectID, userID, userEmail, userPrompt string, existingIDs []string) (*gen.SuggestCustomDetectionRuleResult, error) { | ||
| ctx, err := hostedinference.WithGovernedUserOrUnsupported(ctx, hostedinference.CallCategoryRiskAuthoring, hostedinference.CallCategoryAPIKeyRiskAuthoring, hostedinference.CallCategoryNonOrdinarySessionRiskAuthoring) |
There was a problem hiding this comment.
P2: When an ordinary session auto-generates a policy name, this new governed path still sends empty user attribution. According to linked Linear issue DNO-991, preserve the validated acting user; pass that identity into both naming helpers and their completion requests.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/risk/impl.go, line 2686:
<comment>When an ordinary session auto-generates a policy name, this new governed path still sends empty user attribution. According to linked Linear issue DNO-991, preserve the validated acting user; pass that identity into both naming helpers and their completion requests.</comment>
<file context>
@@ -2668,6 +2683,10 @@ func validateScopeExpr(eng *celenv.Engine, expr *string) error {
}
func (s *Service) suggestCustomRuleViaLLM(ctx context.Context, orgID, projectID, userID, userEmail, userPrompt string, existingIDs []string) (*gen.SuggestCustomDetectionRuleResult, error) {
+ ctx, err := hostedinference.WithGovernedUserOrUnsupported(ctx, hostedinference.CallCategoryRiskAuthoring, hostedinference.CallCategoryAPIKeyRiskAuthoring, hostedinference.CallCategoryNonOrdinarySessionRiskAuthoring)
+ if err != nil {
+ return nil, fmt.Errorf("classify custom risk authoring inference: %w", err)
</file context>
| } | ||
|
|
||
| if _, ok := errors.AsType[*InfrastructureUnavailableError](err); ok { | ||
| return oops.E(oops.CodeUnavailable, err, "%s", oops.CodeUnavailable.UserMessage()), true |
There was a problem hiding this comment.
P2: Evaluator and database failures lose their diagnostic cause from internal reporting. Pass the infrastructure error's underlying cause (or log it explicitly) while keeping the public CodeUnavailable message generic.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/killswitches/hostedinference/transport.go, line 31:
<comment>Evaluator and database failures lose their diagnostic cause from internal reporting. Pass the infrastructure error's underlying `cause` (or log it explicitly) while keeping the public `CodeUnavailable` message generic.</comment>
<file context>
@@ -0,0 +1,49 @@
+ }
+
+ if _, ok := errors.AsType[*InfrastructureUnavailableError](err); ok {
+ return oops.E(oops.CodeUnavailable, err, "%s", oops.CodeUnavailable.UserMessage()), true
+ }
+
</file context>
| if definitionKey == "" { | ||
| definitionKey = DefinitionKeyMCPToolExecution | ||
| } | ||
| resourceKind := fixture.ResourceKind |
There was a problem hiding this comment.
P3: The doc comment on insertPrescription still claims it always creates an "mcp_server resource kind" prescription, but the new ResourceKind fixture field makes the kind configurable (hosted_inference_test.go passes ResourceKindGramHostedInference). Update the comment to reflect that the resource kind is parameterized and defaults to mcp_server.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/killswitches/mcptoolexecution/setup_test.go, line 148:
<comment>The doc comment on insertPrescription still claims it always creates an "mcp_server resource kind" prescription, but the new ResourceKind fixture field makes the kind configurable (hosted_inference_test.go passes ResourceKindGramHostedInference). Update the comment to reflect that the resource kind is parameterized and defaults to mcp_server.</comment>
<file context>
@@ -144,13 +145,17 @@ func insertPrescription(t *testing.T, conn *pgxpool.Pool, organizationID string,
if definitionKey == "" {
definitionKey = DefinitionKeyMCPToolExecution
}
+ resourceKind := fixture.ResourceKind
+ if resourceKind == "" {
+ resourceKind = ResourceKindMCPServer
</file context>
| // | ||
| // Unlike a hand-rolled HTTP client, this drives the REAL production | ||
| // openrouter.ChatClient (NewUnifiedClient to GetObjectCompletion), so every | ||
| // openrouter.ChatClient (NewUncheckedUnifiedClient to GetObjectCompletion), so every |
There was a problem hiding this comment.
P3: NewUncheckedUnifiedClient omits the hosted-inference checkpoint, so this command does not enforce ai_access or exercise the provider-boundary behavior implied by the surrounding “prod-equivalent” claim. Since the inventory intentionally permits this standalone path, label it as non-governed here and update server/cmd/riskjudgebench/README.md, which still claims NewUnifiedClient.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/cmd/riskjudgebench/main.go, line 5:
<comment>`NewUncheckedUnifiedClient` omits the hosted-inference checkpoint, so this command does not enforce `ai_access` or exercise the provider-boundary behavior implied by the surrounding “prod-equivalent” claim. Since the inventory intentionally permits this standalone path, label it as non-governed here and update `server/cmd/riskjudgebench/README.md`, which still claims `NewUnifiedClient`.</comment>
<file context>
@@ -2,7 +2,7 @@
//
// Unlike a hand-rolled HTTP client, this drives the REAL production
-// openrouter.ChatClient (NewUnifiedClient to GetObjectCompletion), so every
+// openrouter.ChatClient (NewUncheckedUnifiedClient to GetObjectCompletion), so every
// model runs under prod-equivalent conditions:
// - reasoning disabled (Effort:"none"), as the object-completion path forces,
</file context>
| return io.NopCloser(strings.NewReader("")), nil | ||
| } | ||
|
|
||
| func (c *checkpointCompletionClient) GetObjectCompletion(ctx context.Context, request openrouter.ObjectCompletionRequest) (*openrouter.CompletionResponse, error) { |
There was a problem hiding this comment.
P3: GetObjectCompletion and CreateEmbeddings call the checkpoint but never increment providerAttempts, while GetCompletion/GetCompletionStream do. A test asserting "no provider egress" via providerAttempts on the object-completion or embeddings path would report zero even after a successful delegate call, making the guard silently incomplete. Bump providerAttempts after the check in both methods (and count object calls if completionCalls is meant to be total).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/chat/hosted_inference_service_test.go, line 89:
<comment>GetObjectCompletion and CreateEmbeddings call the checkpoint but never increment providerAttempts, while GetCompletion/GetCompletionStream do. A test asserting "no provider egress" via providerAttempts on the object-completion or embeddings path would report zero even after a successful delegate call, making the guard silently incomplete. Bump providerAttempts after the check in both methods (and count object calls if completionCalls is meant to be total).</comment>
<file context>
@@ -0,0 +1,380 @@
+ return io.NopCloser(strings.NewReader("")), nil
+}
+
+func (c *checkpointCompletionClient) GetObjectCompletion(ctx context.Context, request openrouter.ObjectCompletionRequest) (*openrouter.CompletionResponse, error) {
+ if err := c.check(ctx, request.OrgID); err != nil {
+ return nil, err
</file context>
| "assistant/chat attribution": {ActiveOrganizationID: "org", UserID: "owner", ExternalUserID: "assistant"}, | ||
| "anonymous organization": {ActiveOrganizationID: "org"}, | ||
| } { | ||
| _, ok = ValidatedGramSessionActingUser(SetAuthContext(t.Context(), authCtx)) |
There was a problem hiding this comment.
P3: The three attribution-class cases in TestValidatedGramSessionActingUser only call SetAuthContext, which never marks the session validated, so ValidatedGramSessionActingUser returns false simply because the context is unvalidated. They never run through WithValidatedGramSession, so the API-key, assistant/chat, and anonymous-organization rejection paths the case names claim to cover are not actually exercised, and a regression in IsOrdinaryGramUserSession's handling of APIKeyID, ExternalUserID, or missing UserID would go undetected. Build the validated context first (WithValidatedGramSession(t.Context(), authCtx, false)) so these cases test the real rejection logic.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/contextvalues/context_test.go, line 106:
<comment>The three attribution-class cases in TestValidatedGramSessionActingUser only call SetAuthContext, which never marks the session validated, so ValidatedGramSessionActingUser returns false simply because the context is unvalidated. They never run through WithValidatedGramSession, so the API-key, assistant/chat, and anonymous-organization rejection paths the case names claim to cover are not actually exercised, and a regression in IsOrdinaryGramUserSession's handling of APIKeyID, ExternalUserID, or missing UserID would go undetected. Build the validated context first (WithValidatedGramSession(t.Context(), authCtx, false)) so these cases test the real rejection logic.</comment>
<file context>
@@ -61,6 +63,123 @@ func TestIsOrdinaryGramUserSessionRejectsAlternateProvenance(t *testing.T) {
+ "assistant/chat attribution": {ActiveOrganizationID: "org", UserID: "owner", ExternalUserID: "assistant"},
+ "anonymous organization": {ActiveOrganizationID: "org"},
+ } {
+ _, ok = ValidatedGramSessionActingUser(SetAuthContext(t.Context(), authCtx))
+ require.False(t, ok, name)
+ }
</file context>
| _, ok = ValidatedGramSessionActingUser(SetAuthContext(t.Context(), authCtx)) | |
| _, ok = ValidatedGramSessionActingUser(WithValidatedGramSession(t.Context(), authCtx, false)) |

Summary
Enforce active
ai_accessprescriptions before governed Gram-hosted provider egress. Preserve authoritative acting-user and tenant attribution, deny fail-closed before every provider attempt, and transport administrator-selected notes safely to supported chat surfaces.Motivation
DNO-991 requires hosted inference to honor the same access controls as governed hooks and managed LiteLLM traffic.
Impact
Validated current-user model requests can now be denied before provider egress when an active prescription matches. Assistant, internal, background, API-key, chat-session, and other explicitly unsupported attribution classes retain their existing behavior and are excluded from coverage claims.
Technical details
Fail-closed provider boundary
A shared checkpoint evaluates the canonical
authenticated_user_ai_resourcecontract immediately before every approved OpenRouter completion, embedding, metadata, retry, and governed redirect attempt. Production client construction requires checkpoint injection.Identity and transport safety
Only frozen, validated ordinary Gram user-session provenance is eligible. Missing, spoofed, cross-tenant, or unsupported identity cannot become a candidate; evaluator failures deny before egress; selected notes are encoded for supported user-facing errors without entering telemetry.
Auditable coverage
Typed inventory analysis classifies hosted-provider call sites, validates context propagation, and rejects unchecked production client composition or unclassified provider transports.
Summary by cubic
Implements DNO-991 by enforcing
ai_accessprescriptions before Gram-hosted provider egress. Hosted inference previously bypassed the killswitch checks applied to governed hooks and managed LiteLLM traffic; now a matching prescription denies validated current-user model calls before any provider attempt. Assistant, internal, background, API-key, and chat-session traffic is unchanged and outside coverage.New Features
403 ai_access_deniedwith the administrator-selected note, renders the note verbatim in the chat thread, and keeps it out of logs and traces; infrastructure failures return a generic503.Migration
NewUnifiedClientplus a hosted-inference checkpoint; benchmarks and tests useNewUncheckedUnifiedClient.Written for commit b0f69d6. Summary will update on new commits.