From d53e7f3a02c545e24ae79d208b1eb0ea453c3ee3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:42:55 +0000 Subject: [PATCH 1/5] fix(service-analytics)!: a read gate that could not decide answers 503 SERVICE_UNAVAILABLE, not the denial's 403 (WIP) Claude-Session: https://claude.ai/code/session_01CxBKycaiUpA1yGx1XyoGqX Co-authored-by: Claude --- .../build-probes-widget-refusal-fix.test.ts | 31 ++++++++- .../metadata-protocol/src/build-probes.ts | 32 ++++++++- .../src/api-exposure-door.ts | 25 ++----- .../src/field-read-admission.ts | 23 ++----- .../service-analytics/src/read-admission.ts | 69 +++++++++++++++++-- 5 files changed, 138 insertions(+), 42 deletions(-) diff --git a/packages/metadata-protocol/src/build-probes-widget-refusal-fix.test.ts b/packages/metadata-protocol/src/build-probes-widget-refusal-fix.test.ts index f8eb20e8004..206b513636e 100644 --- a/packages/metadata-protocol/src/build-probes-widget-refusal-fix.test.ts +++ b/packages/metadata-protocol/src/build-probes-widget-refusal-fix.test.ts @@ -7,9 +7,11 @@ * The widget plane runs `queryDataset` as the publishing caller, so read * admission can refuse a dataset that is correct. That refusal is told apart * by the thrown error's ADR-0112 `code` (`PERMISSION_DENIED`), and its `fix` - * names the caller's read access — never a dataset edit. Every other throw + * names the caller's read access — never a dataset edit. A read gate that + * could not decide at all answers `SERVICE_UNAVAILABLE`, and its `fix` names + * the server-side fault — neither a grant nor a dataset edit. Every other throw * (a selection the dataset does not declare, an unjudgeable member) keeps the - * dataset fix. `code`, `severity` and `message` are the same on both arms. + * dataset fix. `code`, `severity` and `message` are the same on every arm. * * The errors below are the analytics service's envelopes field for field * (`read-admission.ts`, `field-read-admission.ts`, `dataset-refusal.ts`); the @@ -84,6 +86,31 @@ describe('widget_query_failed — the fix follows the failure', () => { expect(issue.fix).toContain('read access to object "task_item" and its field(s) salary'); }); + it('a read gate that could not decide (SERVICE_UNAVAILABLE) names neither the dataset nor an access grant', async () => { + // The analytics service's fault refusal (`readAdmissionUnresolvedError`): + // a permission or exposure resolver failed, so no verdict was reached. + const issue = await probeThrowing(envelope( + '[Analytics] Read access to "project" could not be determined, so the query was not run. ' + + 'This is a server-side fault, not a permission denial.', + { code: 'SERVICE_UNAVAILABLE', status: 503, object: 'project' }, + )); + expect(issue.fix).toContain('Dataset "ds" is not the cause'); + expect(issue.fix).toContain('could not decide read access to object "project"'); + expect(issue.fix).toContain('server log'); + expect(issue.fix).not.toContain(DATASET_FIX); + // Not the denial's remedy: nobody is told to grant anything. + expect(issue.fix).not.toContain('grant it'); + }); + + it('a SERVICE_UNAVAILABLE that names no object falls back to the dataset\'s object', async () => { + const issue = await probeThrowing(envelope( + 'unavailable', + { code: 'SERVICE_UNAVAILABLE', status: 503 }, + )); + expect(issue.fix).toContain('could not decide read access to object "task_item"'); + expect(issue.fix).not.toContain(DATASET_FIX); + }); + it('a selection the dataset does not declare keeps the dataset fix', async () => { const issue = await probeThrowing(envelope( 'Dataset "ds" does not declare measure "nope".', diff --git a/packages/metadata-protocol/src/build-probes.ts b/packages/metadata-protocol/src/build-probes.ts index b4e89c9068d..dbe91790b60 100644 --- a/packages/metadata-protocol/src/build-probes.ts +++ b/packages/metadata-protocol/src/build-probes.ts @@ -138,6 +138,27 @@ function readAdmissionRefusal(e: unknown): { object?: string; fields: string[] } }; } +/** + * `SERVICE_UNAVAILABLE`, pinned against the STANDARD catalog — the code the + * analytics service throws (ADR-0112 envelope, 503) when a read gate could not + * reach a verdict: a permission or API-exposure resolver failed. + */ +const SERVICE_UNAVAILABLE: StandardErrorCode = 'SERVICE_UNAVAILABLE'; + +/** + * The fault refusal a widget query can be answered with, judged by the thrown + * error's ADR-0112 `code` — never by its message text. + * + * Neither the dataset nor the caller's access is the cause: the server could + * not decide, so the remedy is the operator's. `object` names the object the + * server could not decide read access to, when the error carries one. + */ +function serviceUnavailableRefusal(e: unknown): { object?: string } | undefined { + const err = e && typeof e === 'object' ? (e as Rec) : undefined; + if (!err || err.code !== SERVICE_UNAVAILABLE) return undefined; + return { object: typeof err.object === 'string' && err.object ? err.object : undefined }; +} + /** Result rows of a queryDataset call, tolerant of {rows}/{data}/array shapes. */ function resultRows(result: unknown): unknown[] { if (Array.isArray(result)) return result; @@ -419,15 +440,24 @@ export async function runBuildProbes(opts: RunBuildProbesOptions): Promise