Skip to content

feat: enforce ai_access before Gram-hosted inference - #5976

Open
danielkov wants to merge 8 commits into
daniel/dno-990-feat-enforce-ai_access-before-litellm-inferencefrom
daniel/dno-991-feat-enforce-ai_access-before-gram-hosted-inference
Open

feat: enforce ai_access before Gram-hosted inference#5976
danielkov wants to merge 8 commits into
daniel/dno-990-feat-enforce-ai_access-before-litellm-inferencefrom
daniel/dno-991-feat-enforce-ai_access-before-gram-hosted-inference

Conversation

@danielkov

@danielkov danielkov commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Enforce active ai_access prescriptions 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_resource contract 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_access prescriptions 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

  • Evaluates the checkpoint before every approved OpenRouter completion, embedding, retry, and governed redirect attempt, and denies fail-closed when the evaluator fails.
  • Uses only validated ordinary Gram session provenance as the acting user, so missing, spoofed, or cross-tenant identity cannot match a prescription.
  • Returns matched denials as 403 ai_access_denied with the administrator-selected note, renders the note verbatim in the chat thread, and keeps it out of logs and traces; infrastructure failures return a generic 503.
  • Classifies every production hosted-inference call site as governed user, internal, background, or unsupported, and rejects unchecked production client composition.

Migration

  • Production OpenRouter clients must now be built with NewUnifiedClient plus a hosted-inference checkpoint; benchmarks and tests use NewUncheckedUnifiedClient.

Written for commit b0f69d6. Summary will update on new commits.

Review in cubic

@danielkov
danielkov requested review from a team as code owners September 2, 2026 11:02
@linear-code

linear-code Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

DNO-991

@changeset-bot

changeset-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b0f69d6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
server Minor
dashboard Minor

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

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

@cubic-dev-ai cubic-dev-ai Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

Key: DefinitionKeyAIAccess,
PrincipalKinds: []killswitches.PrincipalKind{PrincipalKindUser},
ResourceKinds: []killswitches.ResourceKind{ResourceKindMCPServer, ResourceKindHookActivity, ResourceKindLiteLLMInstance},
ResourceKinds: []killswitches.ResourceKind{ResourceKindMCPServer, ResourceKindHookActivity, ResourceKindLiteLLMInstance, hostedinference.ResourceKindGramHostedInference},

@cubic-dev-ai cubic-dev-ai Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic


// 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 {

@cubic-dev-ai cubic-dev-ai Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

var _ AttemptCheckpoint = (*Checkpoint)(nil)

func NewCheckpoint(registry *killswitches.Registry, evaluation evaluator, timeout time.Duration) (*Checkpoint, error) {
if registry == nil || evaluation == nil {

@cubic-dev-ai cubic-dev-ai Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

}

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)

@cubic-dev-ai cubic-dev-ai Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

}

if _, ok := errors.AsType[*InfrastructureUnavailableError](err); ok {
return oops.E(oops.CodeUnavailable, err, "%s", oops.CodeUnavailable.UserMessage()), true

@cubic-dev-ai cubic-dev-ai Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

if definitionKey == "" {
definitionKey = DefinitionKeyMCPToolExecution
}
resourceKind := fixture.ResourceKind

@cubic-dev-ai cubic-dev-ai Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

//
// 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

@cubic-dev-ai cubic-dev-ai Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

return io.NopCloser(strings.NewReader("")), nil
}

func (c *checkpointCompletionClient) GetObjectCompletion(ctx context.Context, request openrouter.ObjectCompletionRequest) (*openrouter.CompletionResponse, error) {

@cubic-dev-ai cubic-dev-ai Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

"assistant/chat attribution": {ActiveOrganizationID: "org", UserID: "owner", ExternalUserID: "assistant"},
"anonymous organization": {ActiveOrganizationID: "org"},
} {
_, ok = ValidatedGramSessionActingUser(SetAuthContext(t.Context(), authCtx))

@cubic-dev-ai cubic-dev-ai Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
_, ok = ValidatedGramSessionActingUser(SetAuthContext(t.Context(), authCtx))
_, ok = ValidatedGramSessionActingUser(WithValidatedGramSession(t.Context(), authCtx, false))
Fix with cubic

@danielkov

Copy link
Copy Markdown
Contributor Author

Demo

Hosted inference denial

What it shows:

  1. An authenticated Playground user submits a governed model request.
  2. The mocked production-shaped pre-egress denial returns HTTP 403 without provider inference.
  3. The chat renders the administrator-selected safe note.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant