@ggui-ai/protocol-conformance 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -15
- package/dist/cli.d.ts +23 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +223 -23
- package/dist/fixtures/index.d.ts +6 -4
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +6 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/json-equal.d.ts +6 -0
- package/dist/json-equal.d.ts.map +1 -0
- package/dist/json-equal.js +23 -0
- package/dist/refusal-envelope-conformance/index.d.ts.map +1 -1
- package/dist/refusal-envelope-conformance/index.js +1 -20
- package/dist/run-conformance.d.ts +10 -1
- package/dist/run-conformance.d.ts.map +1 -1
- package/dist/run-conformance.js +26 -3
- package/dist/transport-refusal-conformance/cases/refuse-deprovisioned-endpoint.json +25 -0
- package/dist/transport-refusal-conformance/cases/refuse-render-only-code.json +11 -0
- package/dist/transport-refusal-conformance/index.d.ts +54 -0
- package/dist/transport-refusal-conformance/index.d.ts.map +1 -0
- package/dist/transport-refusal-conformance/index.js +52 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ One declared grading gap on the action loop: `action-ack` proves the append half
|
|
|
51
51
|
|
|
52
52
|
The behavioral fixture catalog above asserts what an implementation _does_ over the wire. Some protocol obligations aren't transport-observable at all — they are deterministic _validation functions_: given an input, accept or reject it. SPEC §7.7.2's gadget obligations are exactly this. Modeling them as WebSocket fixtures would mean faking wire frames the protocol never emits; instead the kit ships **pure-function catalogs** — accept/reject cases graded against a caller-supplied function, with no host, render, or transport.
|
|
53
53
|
|
|
54
|
-
Each catalog ships its cases as raw JSON so a non-TypeScript implementer can grade their own implementation, and each runner takes the implementation as a callback — the kit never hard-binds a concrete one.
|
|
54
|
+
Each catalog ships its cases as raw JSON so a non-TypeScript implementer can grade their own implementation, and each runner takes the implementation as a callback — the kit never hard-binds a concrete one. Nine catalogs ship today:
|
|
55
55
|
|
|
56
56
|
- **`@ggui-ai/protocol-conformance/schema-conformance`** — which `DataContract.clientCapabilities` payloads a conformant parser MUST accept / reject (the gadget wire shape).
|
|
57
57
|
- **`@ggui-ai/protocol-conformance/registration-conformance`** — which `(contract, appGadgets, appPublicEnv)` triples the push-time gadget gate stack MUST accept / reject, and with which precise SPEC §7.9 reject code (`gadget_not_registered` / `gadget_package_mismatch` / `gadget_public_env_missing` / `duplicate_gadget_hook`).
|
|
@@ -61,6 +61,7 @@ Each catalog ships its cases as raw JSON so a non-TypeScript implementer can gra
|
|
|
61
61
|
- **`@ggui-ai/protocol-conformance/host-helper-conformance`** — which render coordinates a HOST-HELPER library MUST extract from a `tools/call` result, and which malformed results it MUST decline rather than guess at.
|
|
62
62
|
- **`@ggui-ai/protocol-conformance/refusal-envelope-conformance`** — the tool result a server MUST project for a PRE-GENERATION refusal (SPEC §7.1's refused arm): an in-result `isError` (never a throw), `content[0].text` leading with the code, `structuredContent` carrying `{outcome: 'refused', refusal}` and nothing else, no `_meta` — and no envelope at all for a registered code whose surfaces exclude the render gate.
|
|
63
63
|
- **`@ggui-ai/protocol-conformance/registry-completeness`** — the structural obligations a deployment's closed refusal-code registry MUST satisfy: `surfaces` non-empty, `code === key`, every `after-fix` entry naming who acts (`fixBy`), and `retry` inside the closed four-value set. Grades DATA rather than a function — the caller passes their registry.
|
|
64
|
+
- **`@ggui-ai/protocol-conformance/transport-refusal-conformance`** — the JSON-RPC error a per-app MCP endpoint MUST answer with when it refuses a request for a typed reason (SPEC §7.1's endpoint-level refusal, ggui#825): HTTP 403 with `{ code: -32003, message: 'App not found', data: { refusal } }`, where `refusal` is the registry projection without the render-only fields (`code`, `message`, `fix`, `retry`) — and no envelope at all for a registered code whose surfaces exclude `mcp-endpoint` (an untyped authorization failure answers 403 with `-32001` and no `data`, by contract — ggui#836). Runner: `runTransportRefusalConformance(project)`; folded into `runConformance()` via `transportRefusalProjector`.
|
|
64
65
|
|
|
65
66
|
```ts
|
|
66
67
|
import { runSchemaConformance } from "@ggui-ai/protocol-conformance/schema-conformance";
|
|
@@ -119,20 +120,21 @@ It deliberately does **not** grade: the order in which a substrate-less server a
|
|
|
119
120
|
|
|
120
121
|
What is actually graded against shipping code today, and what is still awaiting a driver. This is an inventory, not a roadmap — a mechanism is listed as proven only if a test in the repo drives it against the real implementation on every push.
|
|
121
122
|
|
|
122
|
-
Every test below is a plain `*.test.ts` inside a package's `src/`, so it runs in that package's ordinary vitest suite. The ggui repo's per-push CI runs `turbo run test` across all non-e2e packages, so **all
|
|
123
|
-
|
|
124
|
-
| Obligation | Graded against
|
|
125
|
-
| ----------------------------------------------------------------------- |
|
|
126
|
-
| Gadget wire schema (§7.7.2) | the shipping `@ggui-ai/protocol` `clientCapabilitiesSpecSchema`
|
|
127
|
-
| Registration gate (§7.7.3, §7.9) | the shipping gate trio in `@ggui-ai/mcp-server-handlers`
|
|
128
|
-
| Bundle-URL resolution (§7.7.2) | the shipping `resolveGadgetUrls` in `@ggui-ai/mcp-server-handlers`
|
|
129
|
-
| Path-A WS fixtures — first-party | the real `@ggui-ai/mcp-server` GguiSession channel, booted in-process on an ephemeral port
|
|
130
|
-
| Path-A WS fixtures — vendor-neutral | `@ggui-ai/protocol-reference-server`
|
|
131
|
-
| `resources/read` typed failures + mount invariant | the shipping `registerGguiRenderResourceTemplate` in `@ggui-ai/mcp-server`
|
|
132
|
-
| Tool-binding resolution — declared wins, contract derivation (§7.7.4.1) | the shipping `resolveMcpToolBindings` in `@ggui-ai/artifact-manifest`
|
|
133
|
-
| Tool-binding search filters — `tool=` / `server=` semantics (§7.7.4.1) | the shipping `matchesMcpToolFilters` in `@ggui-ai/registry-core`
|
|
134
|
-
| Pre-generation refusal envelope (§7.1, refused arm) | the shipping `ggui_render`
|
|
135
|
-
| Refusal-registry completeness (ggui#786 ruling 5b) | the shipping `PRE_GENERATION_REFUSAL_CODES` in `@ggui-ai/protocol`
|
|
123
|
+
Every test below is a plain `*.test.ts` inside a package's `src/`, so it runs in that package's ordinary vitest suite. The ggui repo's per-push CI runs `turbo run test` across all non-e2e packages, so **all twelve rows below are gated on every push** — the kit needs no dedicated workflow, and adopters wiring it into their own CI need nothing more than their existing unit-test task.
|
|
124
|
+
|
|
125
|
+
| Obligation | Graded against | Driver |
|
|
126
|
+
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| Gadget wire schema (§7.7.2) | the shipping `@ggui-ai/protocol` `clientCapabilitiesSpecSchema` | this kit's own `src/schema-conformance/schema-conformance.test.ts` |
|
|
128
|
+
| Registration gate (§7.7.3, §7.9) | the shipping gate trio in `@ggui-ai/mcp-server-handlers` | `mcp-server-handlers/src/renders/assert-gadgets.conformance.test.ts` |
|
|
129
|
+
| Bundle-URL resolution (§7.7.2) | the shipping `resolveGadgetUrls` in `@ggui-ai/mcp-server-handlers` | `mcp-server-handlers/src/renders/resolve-gadget-urls.conformance.test.ts` |
|
|
130
|
+
| Path-A WS fixtures — first-party | the real `@ggui-ai/mcp-server` GguiSession channel, booted in-process on an ephemeral port | `mcp-server/src/ggui-session-channel.conformance.test.ts` — **24 pass / 12 skip / 0 fail** |
|
|
131
|
+
| Path-A WS fixtures — vendor-neutral | `@ggui-ai/protocol-reference-server` | `protocol-reference-server/src/conformance.test.ts` — **19 pass / 5 skip / 0 fail** |
|
|
132
|
+
| `resources/read` typed failures + mount invariant | the shipping `registerGguiRenderResourceTemplate` in `@ggui-ai/mcp-server` | `mcp-server/src/mcp-apps-outbound.resource-read.conformance.test.ts` — **12 pass / 0 skip / 0 fail** |
|
|
133
|
+
| Tool-binding resolution — declared wins, contract derivation (§7.7.4.1) | the shipping `resolveMcpToolBindings` in `@ggui-ai/artifact-manifest` | `artifact-manifest/src/mcp-tool-bindings.conformance.test.ts` |
|
|
134
|
+
| Tool-binding search filters — `tool=` / `server=` semantics (§7.7.4.1) | the shipping `matchesMcpToolFilters` in `@ggui-ai/registry-core` | `registry-core/src/mcp-tool-filters.conformance.test.ts` |
|
|
135
|
+
| Pre-generation refusal envelope (§7.1, refused arm) | `projectRenderRefusal` in `@ggui-ai/protocol` — the one source both the shipping `ggui_render` handler (`@ggui-ai/mcp-server-handlers`) and `@ggui-ai/protocol-reference-server` answer through | `mcp-server-handlers/src/renders/render-refusal-projection.conformance.test.ts` |
|
|
136
|
+
| Refusal-registry completeness (ggui#786 ruling 5b) | the shipping `PRE_GENERATION_REFUSAL_CODES` in `@ggui-ai/protocol` | this kit's own `src/registry-completeness/registry-completeness.test.ts` |
|
|
137
|
+
| Endpoint-level refusal (§7.1, `mcp-endpoint` surface, ggui#825) | the catalog's kit-local reference projector only — no shipping deployment projector is graded yet: the OSS route is generic (`errorMapper`), the first-party server and the reference server supply no projector (SKIPPED, named), and the pod's mapper lands after this slice | this kit's own `src/transport-refusal-conformance/transport-refusal-conformance.test.ts` (coherence + discrimination) |
|
|
136
138
|
|
|
137
139
|
Both WS drivers pin their pass set _and_ their skip set as exact sets, and the resource-read driver pins its skip set as exactly empty. A fixture silently degrading to a skip fails the build, and so does a skipped fixture that quietly starts passing — a skip set that can grow unnoticed is a false gate, and re-pinning it is meant to be a deliberate act.
|
|
138
140
|
|
|
@@ -170,6 +172,13 @@ npx @ggui-ai/protocol-conformance --url ws://localhost:3000/ws --auth bearer:$TO
|
|
|
170
172
|
|
|
171
173
|
The CLI exits `0` only when at least one fixture executed and none failed; `1` on any fixture failure; `2` on invocation errors **or** when every fixture skipped (a zero-executed run never reads as success in CI).
|
|
172
174
|
|
|
175
|
+
The three pure-function catalogs (`refusal-envelope`, `registry-completeness`, `transport-refusal`) grade an in-process function and a data table, not the wire, so a URL alone cannot reach them. The CLI hands them their inputs by flag — `--registry <file.json>` (the deployment's refusal registry, a JSON object keyed by code), `--projector <module>` (an ES module whose `project(refusal)` — named or default export — returns the SPEC §7.1 tool result, or `null` for a code with no render envelope) and `--transport-projector <module>` (same shape, returning the endpoint's `{ httpStatus, error }` or `null`). A flag left out leaves its catalog on the scorecard as SKIPPED with the flag named. Catalog rows count as executed fixtures, so a run given a catalog flag can exit 0 while every wire fixture is SKIPPED — read the scorecard for the wire signal. Minimal spec-correct samples of each input live in the repository at [`src/cli-samples/`](https://github.com/ggui-ai/ggui/tree/main/packages/protocol-conformance/src/cli-samples) (not in the npm tarball); the test suite pins that they pass their catalogs.
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
npx @ggui-ai/protocol-conformance --url ws://localhost:3000/ws --auth bearer:$TOKEN \
|
|
179
|
+
--registry ./refusal-registry.json --projector ./project.mjs --transport-projector ./endpoint.mjs
|
|
180
|
+
```
|
|
181
|
+
|
|
173
182
|
To grade setup-dependent fixtures (and any `session-state` fixture), pass a `host` implementing the `ConformanceHost` adapter — `dispatchSetup` / `dispatchTeardown` for the directive vocabulary, plus the optional `readSessionField(sessionId, field)` introspection seam. Fixtures whose requirements the host doesn't meet skip with a precise reason; they never silently pass.
|
|
174
183
|
|
|
175
184
|
## License
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { type ConformanceResult } from './run-conformance.js';
|
|
2
|
+
import { type ConformanceResult, type RunConformanceConfig } from './run-conformance.js';
|
|
3
3
|
import type { AuthConfig } from './types.js';
|
|
4
4
|
interface ParsedArgs {
|
|
5
5
|
readonly url?: string;
|
|
6
6
|
readonly auth?: string;
|
|
7
7
|
readonly only?: readonly string[];
|
|
8
8
|
readonly timeoutMs?: number;
|
|
9
|
+
/** Path to a JSON file holding the deployment's refusal registry (code → row). */
|
|
10
|
+
readonly registry?: string;
|
|
11
|
+
/** Path to an ES module exporting the SPEC §7.1 refusal projector. */
|
|
12
|
+
readonly projector?: string;
|
|
13
|
+
/** Path to an ES module exporting the endpoint-level refusal projector. */
|
|
14
|
+
readonly transportProjector?: string;
|
|
9
15
|
readonly verbose: boolean;
|
|
10
16
|
readonly help: boolean;
|
|
11
17
|
}
|
|
12
18
|
declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
13
19
|
declare function parseAuth(spec: string): AuthConfig;
|
|
20
|
+
/** The three optional inputs a CLI run can hand the pure-function catalogs. */
|
|
21
|
+
export type CatalogInputs = Pick<RunConformanceConfig, 'refusalRegistry' | 'refusalProjector' | 'transportRefusalProjector'>;
|
|
22
|
+
/** The flag values that name where each catalog input comes from. */
|
|
23
|
+
export interface CatalogInputPaths {
|
|
24
|
+
readonly registry?: string | undefined;
|
|
25
|
+
readonly projector?: string | undefined;
|
|
26
|
+
readonly transportProjector?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the `--registry` / `--projector` / `--transport-projector`
|
|
30
|
+
* flags into the runner's catalog inputs. A flag that is absent yields
|
|
31
|
+
* no input, and the runner reports that catalog SKIPPED with the flag
|
|
32
|
+
* named. Every failure names the flag and the path.
|
|
33
|
+
*/
|
|
34
|
+
export declare function loadCatalogInputs(paths: CatalogInputPaths): Promise<CatalogInputs>;
|
|
35
|
+
export declare const USAGE: string;
|
|
14
36
|
declare function main(argv: readonly string[]): Promise<number>;
|
|
15
37
|
/**
|
|
16
38
|
* Map a {@link ConformanceResult} to the CLI exit code:
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAsDA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAC1B,MAAM,sBAAsB,CAAC;AAK9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAM7C,UAAU,UAAU;IAClB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,iBAAS,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CA0DtD;AAOD,iBAAS,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAY3C;AAMD,+EAA+E;AAC/E,MAAM,MAAM,aAAa,GAAG,IAAI,CAC9B,oBAAoB,EACpB,iBAAiB,GAAG,kBAAkB,GAAG,2BAA2B,CACrE,CAAC;AAEF,qEAAqE;AACrE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClD;AA4ID;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CA6BxF;AAMD,eAAO,MAAM,KAAK,QAyDL,CAAC;AAEd,iBAAe,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAoE5D;AAED;;;;;;;GAOG;AACH,iBAAS,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAI/D;AA+BD,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -16,25 +16,42 @@
|
|
|
16
16
|
* may legitimately not provide every `ConformanceHost` directive in
|
|
17
17
|
* v1.0 — but a run that is ALL skips carries no signal.
|
|
18
18
|
*
|
|
19
|
+
* The three PURE-FUNCTION catalogs (refusal-envelope,
|
|
20
|
+
* registry-completeness, transport-refusal) grade an in-process
|
|
21
|
+
* function and a data table, not the wire: `--registry <file.json>`,
|
|
22
|
+
* `--projector <module>` and `--transport-projector <module>` hand
|
|
23
|
+
* them their inputs (ggui#803 leg 3). A flag left out leaves its
|
|
24
|
+
* catalog SKIPPED with the flag named, never silently absent. Catalog
|
|
25
|
+
* rows count as executed fixtures, so a run given a catalog flag can
|
|
26
|
+
* exit 0 while every wire fixture is SKIPPED — read the scorecard,
|
|
27
|
+
* not only the exit code, for a wire signal.
|
|
28
|
+
*
|
|
19
29
|
* Honest scope of the bundled CLI: no `host` wiring. The CLI runs
|
|
20
30
|
* the runner with an absent host, meaning every fixture with non-empty
|
|
21
31
|
* `setup` skips cleanly with the "no host provided" reason — and
|
|
22
32
|
* since every fixture in the current catalog declares setup, a
|
|
23
|
-
* hostless CLI run skips them all and exits 2 by
|
|
24
|
-
* pass/fail signal against an implementation,
|
|
25
|
-
* `runConformance({ host: myImpl })` programmatically
|
|
26
|
-
* same guard:
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
33
|
+
* hostless CLI run with no catalog flag skips them all and exits 2 by
|
|
34
|
+
* design. To get real pass/fail signal against an implementation,
|
|
35
|
+
* consumers call `runConformance({ host: myImpl })` programmatically
|
|
36
|
+
* and apply the same guard:
|
|
37
|
+
* `result.failed.length > 0 || result.passed.length === 0` is a red
|
|
38
|
+
* build. The CLI remains the lowest-friction wire-level smoke check
|
|
39
|
+
* for setups where the server under test is provisioned out-of-band
|
|
40
|
+
* rather than through a `ConformanceHost` injection.
|
|
30
41
|
*/
|
|
42
|
+
import { readFile } from 'node:fs/promises';
|
|
43
|
+
import { resolve } from 'node:path';
|
|
44
|
+
import { pathToFileURL } from 'node:url';
|
|
31
45
|
import { createDefaultReporter, formatFailures, formatSkips } from './reporter.js';
|
|
32
|
-
import { runConformance } from './run-conformance.js';
|
|
46
|
+
import { runConformance, } from './run-conformance.js';
|
|
33
47
|
function parseArgs(argv) {
|
|
34
48
|
let url;
|
|
35
49
|
let auth;
|
|
36
50
|
const only = [];
|
|
37
51
|
let timeoutMs;
|
|
52
|
+
let registry;
|
|
53
|
+
let projector;
|
|
54
|
+
let transportProjector;
|
|
38
55
|
let verbose = false;
|
|
39
56
|
let help = false;
|
|
40
57
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -62,6 +79,15 @@ function parseArgs(argv) {
|
|
|
62
79
|
if (Number.isNaN(timeoutMs))
|
|
63
80
|
throw new Error('--timeout-ms must be an integer');
|
|
64
81
|
break;
|
|
82
|
+
case '--registry':
|
|
83
|
+
registry = next();
|
|
84
|
+
break;
|
|
85
|
+
case '--projector':
|
|
86
|
+
projector = next();
|
|
87
|
+
break;
|
|
88
|
+
case '--transport-projector':
|
|
89
|
+
transportProjector = next();
|
|
90
|
+
break;
|
|
65
91
|
case '--verbose':
|
|
66
92
|
case '-v':
|
|
67
93
|
verbose = true;
|
|
@@ -74,7 +100,11 @@ function parseArgs(argv) {
|
|
|
74
100
|
throw new Error(`unknown flag '${flag}' — run with --help for usage`);
|
|
75
101
|
}
|
|
76
102
|
}
|
|
77
|
-
return { url, auth, only, timeoutMs, verbose, help };
|
|
103
|
+
return { url, auth, only, timeoutMs, registry, projector, transportProjector, verbose, help };
|
|
104
|
+
}
|
|
105
|
+
/** The message of a thrown value, without pretending every throw is an Error. */
|
|
106
|
+
function errorMessage(err) {
|
|
107
|
+
return err instanceof Error ? err.message : String(err);
|
|
78
108
|
}
|
|
79
109
|
function parseAuth(spec) {
|
|
80
110
|
const idx = spec.indexOf(':');
|
|
@@ -89,10 +119,150 @@ function parseAuth(spec) {
|
|
|
89
119
|
return { kind: 'session-cookie', cookie: value };
|
|
90
120
|
throw new Error(`--auth kind '${kind}' is not recognized; supported: 'bearer', 'session-cookie'`);
|
|
91
121
|
}
|
|
122
|
+
function isRecord(value) {
|
|
123
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
124
|
+
}
|
|
125
|
+
function isStringArray(value) {
|
|
126
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The runtime twin of {@link RefusalRegistryRow} — a JSON registry is
|
|
130
|
+
* held to exactly the shape a programmatic caller has to satisfy at
|
|
131
|
+
* compile time. Value-level rules (recognized retry class, `fixBy` on
|
|
132
|
+
* `after-fix`, code equals key, non-empty surfaces) are NOT checked
|
|
133
|
+
* here: those are the registry-completeness catalog's pins, and a
|
|
134
|
+
* deliberately broken registry must stay expressible so they can
|
|
135
|
+
* grade it.
|
|
136
|
+
*/
|
|
137
|
+
function isRegistryRow(value) {
|
|
138
|
+
return (isRecord(value) &&
|
|
139
|
+
typeof value.code === 'string' &&
|
|
140
|
+
isStringArray(value.surfaces) &&
|
|
141
|
+
typeof value.retry === 'string' &&
|
|
142
|
+
(value.fixBy === undefined || typeof value.fixBy === 'string') &&
|
|
143
|
+
typeof value.emitter === 'string' &&
|
|
144
|
+
typeof value.description === 'string');
|
|
145
|
+
}
|
|
146
|
+
function parseRegistryView(text, path) {
|
|
147
|
+
let parsed;
|
|
148
|
+
try {
|
|
149
|
+
parsed = JSON.parse(text);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
throw new Error(`--registry ${path} is not JSON: ${errorMessage(err)}`);
|
|
153
|
+
}
|
|
154
|
+
if (!isRecord(parsed)) {
|
|
155
|
+
throw new Error(`--registry ${path} must be a JSON object of rows keyed by code`);
|
|
156
|
+
}
|
|
157
|
+
const rows = [];
|
|
158
|
+
for (const [code, row] of Object.entries(parsed)) {
|
|
159
|
+
if (!isRegistryRow(row)) {
|
|
160
|
+
throw new Error(`--registry ${path}: row '${code}' is not a registry row (needs string code, string[] surfaces, string retry, string emitter, string description, optional string fixBy)`);
|
|
161
|
+
}
|
|
162
|
+
rows.push([code, row]);
|
|
163
|
+
}
|
|
164
|
+
// fromEntries defines OWN properties — a row keyed `__proto__` stays a row.
|
|
165
|
+
return Object.fromEntries(rows);
|
|
166
|
+
}
|
|
167
|
+
function isRefusalInput(value) {
|
|
168
|
+
return (isRecord(value) &&
|
|
169
|
+
typeof value.code === 'string' &&
|
|
170
|
+
typeof value.message === 'string' &&
|
|
171
|
+
typeof value.fix === 'string' &&
|
|
172
|
+
typeof value.retry === 'string' &&
|
|
173
|
+
typeof value.handshake === 'string' &&
|
|
174
|
+
(value.balanceCentsAtCheck === undefined || typeof value.balanceCentsAtCheck === 'number'));
|
|
175
|
+
}
|
|
176
|
+
function isProjectedRefusalResult(value) {
|
|
177
|
+
return (isRecord(value) &&
|
|
178
|
+
typeof value.isError === 'boolean' &&
|
|
179
|
+
typeof value.text === 'string' &&
|
|
180
|
+
isRecord(value.structuredContent) &&
|
|
181
|
+
typeof value.structuredContent.outcome === 'string' &&
|
|
182
|
+
isRefusalInput(value.structuredContent.refusal) &&
|
|
183
|
+
typeof value.hasMeta === 'boolean' &&
|
|
184
|
+
isStringArray(value.identityFields));
|
|
185
|
+
}
|
|
186
|
+
function isTransportRefusalInput(value) {
|
|
187
|
+
return (isRecord(value) &&
|
|
188
|
+
typeof value.code === 'string' &&
|
|
189
|
+
typeof value.message === 'string' &&
|
|
190
|
+
typeof value.fix === 'string' &&
|
|
191
|
+
typeof value.retry === 'string');
|
|
192
|
+
}
|
|
193
|
+
function isProjectedTransportRefusal(value) {
|
|
194
|
+
return (isRecord(value) &&
|
|
195
|
+
typeof value.httpStatus === 'number' &&
|
|
196
|
+
isRecord(value.error) &&
|
|
197
|
+
typeof value.error.code === 'number' &&
|
|
198
|
+
typeof value.error.message === 'string' &&
|
|
199
|
+
isRecord(value.error.data) &&
|
|
200
|
+
isTransportRefusalInput(value.error.data.refusal));
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Import an ES module at `path` (resolved against the working
|
|
204
|
+
* directory) and return the projector it exports —
|
|
205
|
+
* `export function project(...)` or the default export. The module is
|
|
206
|
+
* an external boundary: its export is checked to be a function here,
|
|
207
|
+
* and every value it returns is checked against the projection shape
|
|
208
|
+
* by `guard` before the catalog sees it, so a module that returns the
|
|
209
|
+
* wrong shape fails by flag name instead of typing its way into a
|
|
210
|
+
* grade. The catalogs grade that throw as a FAIL on the case in hand
|
|
211
|
+
* and keep grading the rest — never a crash of the run.
|
|
212
|
+
*/
|
|
213
|
+
async function loadProjector(flag, path, guard) {
|
|
214
|
+
const href = pathToFileURL(resolve(path)).href;
|
|
215
|
+
let mod;
|
|
216
|
+
try {
|
|
217
|
+
mod = await import(/* @vite-ignore */ href);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
throw new Error(`${flag} ${path} could not be imported: ${errorMessage(err)}`);
|
|
221
|
+
}
|
|
222
|
+
const candidate = mod.project ?? mod.default;
|
|
223
|
+
if (typeof candidate !== 'function') {
|
|
224
|
+
throw new Error(`${flag} ${path} must export \`project\` (or a default export) as a function`);
|
|
225
|
+
}
|
|
226
|
+
return (input) => {
|
|
227
|
+
const out = candidate(input);
|
|
228
|
+
if (out === null)
|
|
229
|
+
return null;
|
|
230
|
+
if (!guard(out)) {
|
|
231
|
+
throw new Error(`${flag} ${path} returned a value that is not a projection: ${JSON.stringify(out)}`);
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Resolve the `--registry` / `--projector` / `--transport-projector`
|
|
238
|
+
* flags into the runner's catalog inputs. A flag that is absent yields
|
|
239
|
+
* no input, and the runner reports that catalog SKIPPED with the flag
|
|
240
|
+
* named. Every failure names the flag and the path.
|
|
241
|
+
*/
|
|
242
|
+
export async function loadCatalogInputs(paths) {
|
|
243
|
+
const inputs = {};
|
|
244
|
+
if (paths.registry !== undefined) {
|
|
245
|
+
let text;
|
|
246
|
+
try {
|
|
247
|
+
text = await readFile(resolve(paths.registry), 'utf8');
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
throw new Error(`--registry ${paths.registry} could not be read: ${errorMessage(err)}`);
|
|
251
|
+
}
|
|
252
|
+
inputs.refusalRegistry = parseRegistryView(text, paths.registry);
|
|
253
|
+
}
|
|
254
|
+
if (paths.projector !== undefined) {
|
|
255
|
+
inputs.refusalProjector = await loadProjector('--projector', paths.projector, isProjectedRefusalResult);
|
|
256
|
+
}
|
|
257
|
+
if (paths.transportProjector !== undefined) {
|
|
258
|
+
inputs.transportRefusalProjector = await loadProjector('--transport-projector', paths.transportProjector, isProjectedTransportRefusal);
|
|
259
|
+
}
|
|
260
|
+
return inputs;
|
|
261
|
+
}
|
|
92
262
|
// =============================================================================
|
|
93
263
|
// Entry point
|
|
94
264
|
// =============================================================================
|
|
95
|
-
const USAGE = `
|
|
265
|
+
export const USAGE = `
|
|
96
266
|
ggui-protocol-conformance — run the protocol conformance kit against a live server
|
|
97
267
|
|
|
98
268
|
Usage:
|
|
@@ -113,22 +283,42 @@ Options:
|
|
|
113
283
|
filters the pure-function catalog rows, whose
|
|
114
284
|
names are prefixed <catalog>/<case>.
|
|
115
285
|
--timeout-ms <N> Per-fixture observation window. Default 2000.
|
|
286
|
+
--registry <file.json> The deployment's refusal-code registry, a JSON
|
|
287
|
+
object keyed by code (grades registry-completeness).
|
|
288
|
+
--projector <module> ES module whose \`project(refusal)\` — a named or
|
|
289
|
+
default export — returns the SPEC §7.1 tool result
|
|
290
|
+
for a refusal, or null when the code has no
|
|
291
|
+
envelope on the render-gate surface (grades
|
|
292
|
+
refusal-envelope).
|
|
293
|
+
--transport-projector <module>
|
|
294
|
+
ES module whose \`project(refusal)\` — named or
|
|
295
|
+
default export — returns the endpoint's
|
|
296
|
+
{ httpStatus, error } for a refusal, or null when
|
|
297
|
+
the code has no transport envelope (grades
|
|
298
|
+
transport-refusal).
|
|
116
299
|
--verbose, -v Print failure details + skip reasons at the end.
|
|
117
300
|
--help, -h Show this help.
|
|
118
301
|
|
|
119
|
-
Pure-function catalogs (refusal-envelope, registry-completeness):
|
|
302
|
+
Pure-function catalogs (refusal-envelope, registry-completeness, transport-refusal):
|
|
120
303
|
These grade an in-process function and a data table, not the wire, so
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
304
|
+
a URL alone cannot reach them: the three flags above hand them their
|
|
305
|
+
inputs. A flag left out leaves its catalog on the scorecard as SKIPPED
|
|
306
|
+
with the flag named — an ungraded obligation stays visible rather
|
|
307
|
+
than vanishing. File and module paths resolve against the working
|
|
308
|
+
directory. A projector must be synchronous; one that throws, returns
|
|
309
|
+
undefined, or returns a non-projection is graded as a FAIL on that
|
|
310
|
+
case (exit 1) and the remaining cases are still graded — never a
|
|
311
|
+
crash. Catalog rows count as executed fixtures: a run given a
|
|
312
|
+
catalog flag can exit 0 while every wire fixture is SKIPPED, so read
|
|
313
|
+
the scorecard, not only the exit code, for a wire signal.
|
|
125
314
|
|
|
126
315
|
Exit codes:
|
|
127
316
|
0 — at least one fixture executed and none failed.
|
|
128
317
|
1 — at least one fixture failed.
|
|
129
|
-
2 — invocation error (bad args,
|
|
130
|
-
|
|
131
|
-
proved nothing never reads
|
|
318
|
+
2 — invocation error (bad args, unreadable registry or projector,
|
|
319
|
+
cannot reach server, etc.), or the run executed zero fixtures
|
|
320
|
+
(every fixture skipped) — a run that proved nothing never reads
|
|
321
|
+
as success.
|
|
132
322
|
`.trimStart();
|
|
133
323
|
async function main(argv) {
|
|
134
324
|
let parsed;
|
|
@@ -136,7 +326,7 @@ async function main(argv) {
|
|
|
136
326
|
parsed = parseArgs(argv);
|
|
137
327
|
}
|
|
138
328
|
catch (err) {
|
|
139
|
-
process.stderr.write(`error: ${
|
|
329
|
+
process.stderr.write(`error: ${errorMessage(err)}\n\n`);
|
|
140
330
|
process.stderr.write(USAGE);
|
|
141
331
|
return 2;
|
|
142
332
|
}
|
|
@@ -159,7 +349,16 @@ async function main(argv) {
|
|
|
159
349
|
auth = parseAuth(parsed.auth);
|
|
160
350
|
}
|
|
161
351
|
catch (err) {
|
|
162
|
-
process.stderr.write(`error: ${
|
|
352
|
+
process.stderr.write(`error: ${errorMessage(err)}\n\n`);
|
|
353
|
+
process.stderr.write(USAGE);
|
|
354
|
+
return 2;
|
|
355
|
+
}
|
|
356
|
+
let catalogInputs;
|
|
357
|
+
try {
|
|
358
|
+
catalogInputs = await loadCatalogInputs(parsed);
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
process.stderr.write(`error: ${errorMessage(err)}\n\n`);
|
|
163
362
|
process.stderr.write(USAGE);
|
|
164
363
|
return 2;
|
|
165
364
|
}
|
|
@@ -170,6 +369,7 @@ async function main(argv) {
|
|
|
170
369
|
only: parsed.only !== undefined && parsed.only.length > 0 ? parsed.only : undefined,
|
|
171
370
|
observationTimeoutMs: parsed.timeoutMs,
|
|
172
371
|
reporter,
|
|
372
|
+
...catalogInputs,
|
|
173
373
|
});
|
|
174
374
|
if (parsed.verbose) {
|
|
175
375
|
const failures = formatFailures(result.failed);
|
|
@@ -201,11 +401,11 @@ function exitCodeForResult(result) {
|
|
|
201
401
|
return 0;
|
|
202
402
|
}
|
|
203
403
|
// Immediately-invoked — this module IS the bin entry. Tests import
|
|
204
|
-
// `parseArgs` / `parseAuth` / `
|
|
205
|
-
// exports without triggering the run.
|
|
404
|
+
// `parseArgs` / `parseAuth` / `loadCatalogInputs` / `exitCodeForResult`
|
|
405
|
+
// / `main` via named exports without triggering the run.
|
|
206
406
|
if (isEntryPoint()) {
|
|
207
407
|
main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
|
|
208
|
-
process.stderr.write(`fatal: ${
|
|
408
|
+
process.stderr.write(`fatal: ${errorMessage(err)}\n`);
|
|
209
409
|
process.exit(2);
|
|
210
410
|
});
|
|
211
411
|
}
|
package/dist/fixtures/index.d.ts
CHANGED
|
@@ -38,10 +38,12 @@
|
|
|
38
38
|
*
|
|
39
39
|
* The same is true of SPEC §7.1's PRE-GENERATION refusal obligations
|
|
40
40
|
* (ggui#786): what a server projects for a refusal
|
|
41
|
-
* (`../refusal-envelope-conformance`)
|
|
42
|
-
* refusal registry must satisfy (`../registry-completeness`)
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* (`../refusal-envelope-conformance`), what a deployment's closed
|
|
42
|
+
* refusal registry must satisfy (`../registry-completeness`), and what
|
|
43
|
+
* a per-app endpoint answers for an endpoint-level refusal
|
|
44
|
+
* (`../transport-refusal-conformance`, ggui#825) are two projections
|
|
45
|
+
* and a data check. None is WebSocket-observable, so none is
|
|
46
|
+
* registered below.
|
|
45
47
|
*
|
|
46
48
|
* ## An MCP-binding driver EXISTS for `resources/read` — `../resource-read-conformance`
|
|
47
49
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fixtures/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fixtures/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwGG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,gCAAgC,EAAE,MAAM,uCAAuC,CAAC;AACzF,OAAO,EAAE,8BAA8B,EAAE,MAAM,qCAAqC,CAAC;AACrF,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAExE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAQ5C;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GACpB,oBAAoB,GACpB,gBAAgB,GAChB,cAAc,GACd,4BAA4B,GAC5B,0BAA0B,GAC1B,mBAAmB,GACnB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,SAAS,QAAQ,EAAE,CAAC,CAOlF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,QAAQ,EAGwB,CAAC"}
|
package/dist/fixtures/index.js
CHANGED
|
@@ -38,10 +38,12 @@
|
|
|
38
38
|
*
|
|
39
39
|
* The same is true of SPEC §7.1's PRE-GENERATION refusal obligations
|
|
40
40
|
* (ggui#786): what a server projects for a refusal
|
|
41
|
-
* (`../refusal-envelope-conformance`)
|
|
42
|
-
* refusal registry must satisfy (`../registry-completeness`)
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* (`../refusal-envelope-conformance`), what a deployment's closed
|
|
42
|
+
* refusal registry must satisfy (`../registry-completeness`), and what
|
|
43
|
+
* a per-app endpoint answers for an endpoint-level refusal
|
|
44
|
+
* (`../transport-refusal-conformance`, ggui#825) are two projections
|
|
45
|
+
* and a data check. None is WebSocket-observable, so none is
|
|
46
|
+
* registered below.
|
|
45
47
|
*
|
|
46
48
|
* ## An MCP-binding driver EXISTS for `resources/read` — `../resource-read-conformance`
|
|
47
49
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export { refusalEnvelopeCases, runRefusalEnvelopeConformance, } from './refusal-
|
|
|
28
28
|
export type { PreGenerationRefusalInput, ProjectedRefusalResult, RefusalEnvelopeConformanceCase, RefusalEnvelopeConformanceResult, RefusalEnvelopeMismatch, } from './refusal-envelope-conformance/index.js';
|
|
29
29
|
export { registryCompletenessPins, runRegistryCompletenessConformance, } from './registry-completeness/index.js';
|
|
30
30
|
export type { RefusalRegistryRow, RefusalRegistryView, RegistryCompletenessMismatch, RegistryCompletenessPin, RegistryCompletenessResult, } from './registry-completeness/index.js';
|
|
31
|
+
export { runTransportRefusalConformance, transportRefusalCases, } from './transport-refusal-conformance/index.js';
|
|
32
|
+
export type { ProjectedTransportRefusal, TransportRefusalConformanceCase, TransportRefusalConformanceResult, TransportRefusalInput, TransportRefusalMismatch, } from './transport-refusal-conformance/index.js';
|
|
31
33
|
export { declaresDeliveryChannel, renderLocatorUri, resourceReadCases, runResourceReadConformance, GGUI_RENDER_RESOURCE_URI, } from './resource-read-conformance/index.js';
|
|
32
34
|
export type { DurableSubstrateWiring, JsonRpcErrorFrame, PreparedResourceReadScenario, ResourceReadCaller, ResourceReadConformanceCase, ResourceReadConformanceFailure, ResourceReadConformanceResult, ResourceReadConformanceSkip, ResourceReadErrorCodeDecl, ResourceReadExpectation, ResourceReadKey, ResourceReadLocator, ResourceReadOutcome, ResourceReadProbe, ResourceReadRenderMeta, ResourceReadScenario, ResourceReadScenarioDriver, ResourceReadSeed, ResourceReadServerShape, RunResourceReadConformanceOptions, } from './resource-read-conformance/index.js';
|
|
33
35
|
export { loadFixture, listFixtures, loadAllFixtures } from './loader.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,YAAY,EAEV,eAAe,EACf,wBAAwB,EACxB,sBAAsB,EACtB,UAAU,EAEV,QAAQ,EACR,qBAAqB,EAErB,SAAS,EACT,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,EACd,uBAAuB,EACvB,yBAAyB,EACzB,gCAAgC,EAChC,gBAAgB,EAEhB,YAAY,EAEZ,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,EACpB,wBAAwB,EACxB,wBAAwB,EACxB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,eAAe,EAEf,aAAa,EACb,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAMpB,YAAY,EACV,eAAe,EAEf,SAAS,IAAI,aAAa,EAC1B,sBAAsB,EACtB,wBAAwB,EACxB,0BAA0B,EAC1B,iCAAiC,EACjC,iBAAiB,EAEjB,YAAY,IAAI,gBAAgB,GACjC,MAAM,uBAAuB,CAAC;AAK/B,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,yBAAyB,EACzB,qBAAqB,EACrB,mBAAmB,EACnB,gCAAgC,EAChC,8BAA8B,EAC9B,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AASxD,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,+BAA+B,CAAC;AACvC,YAAY,EACV,qBAAqB,EACrB,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,+BAA+B,CAAC;AAOvC,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,GAC1B,MAAM,qCAAqC,CAAC;AAC7C,YAAY,EACV,0BAA0B,EAC1B,6BAA6B,EAC7B,4BAA4B,EAC5B,4BAA4B,EAC5B,yBAAyB,GAC1B,MAAM,qCAAqC,CAAC;AAK7C,OAAO,EACL,uBAAuB,EACvB,0BAA0B,GAC3B,MAAM,qCAAqC,CAAC;AAC7C,YAAY,EACV,oBAAoB,EACpB,WAAW,EACX,2BAA2B,EAC3B,+BAA+B,EAC/B,6BAA6B,GAC9B,MAAM,qCAAqC,CAAC;AAK7C,OAAO,EACL,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,mCAAmC,CAAC;AAC3C,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,yBAAyB,EACzB,6BAA6B,EAC7B,2BAA2B,GAC5B,MAAM,mCAAmC,CAAC;AAM3C,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,yBAAyB,GAC1B,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,yBAAyB,EACzB,wBAAwB,EACxB,uBAAuB,EACvB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,gCAAgC,CAAC;AAOxC,OAAO,EACL,oBAAoB,EACpB,6BAA6B,GAC9B,MAAM,yCAAyC,CAAC;AACjD,YAAY,EACV,yBAAyB,EACzB,sBAAsB,EACtB,8BAA8B,EAC9B,gCAAgC,EAChC,uBAAuB,GACxB,MAAM,yCAAyC,CAAC;AAMjD,OAAO,EACL,wBAAwB,EACxB,kCAAkC,GACnC,MAAM,kCAAkC,CAAC;AAC1C,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,4BAA4B,EAC5B,uBAAuB,EACvB,0BAA0B,GAC3B,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,12 @@ export { refusalEnvelopeCases, runRefusalEnvelopeConformance, } from './refusal-
|
|
|
52
52
|
// function: `surfaces` non-empty, `code === key`, every `after-fix`
|
|
53
53
|
// entry naming who acts, and `retry` inside the closed four-value set.
|
|
54
54
|
export { registryCompletenessPins, runRegistryCompletenessConformance, } from './registry-completeness/index.js';
|
|
55
|
+
// Transport-refusal — the JSON-RPC error a per-app MCP endpoint answers
|
|
56
|
+
// with when it refuses a request for a typed reason (ggui#825): HTTP 403,
|
|
57
|
+
// `-32003` / `App not found` (ggui#836), `data.refusal` carrying the registry
|
|
58
|
+
// projection without the render-only fields; a code whose surfaces
|
|
59
|
+
// exclude `mcp-endpoint` projects to nothing.
|
|
60
|
+
export { runTransportRefusalConformance, transportRefusalCases, } from './transport-refusal-conformance/index.js';
|
|
55
61
|
// Resource-read conformance — the kit's first MCP-binding driver, and
|
|
56
62
|
// it binds `resources/read` ONLY (`tools/call` has none). A read of a
|
|
57
63
|
// render locator has exactly two exits: a result whose contents
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-equal.d.ts","sourceRoot":"","sources":["../src/json-equal.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAezD"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural equality over plain JSON data — the one comparator every
|
|
3
|
+
* pure-function catalog grades with (key order ignored, arrays ordered).
|
|
4
|
+
*/
|
|
5
|
+
export function jsonEqual(a, b) {
|
|
6
|
+
if (a === b)
|
|
7
|
+
return true;
|
|
8
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
12
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
13
|
+
return false;
|
|
14
|
+
return a.every((item, index) => jsonEqual(item, b[index]));
|
|
15
|
+
}
|
|
16
|
+
const left = a;
|
|
17
|
+
const right = b;
|
|
18
|
+
const leftKeys = Object.keys(left).sort();
|
|
19
|
+
const rightKeys = Object.keys(right).sort();
|
|
20
|
+
if (!jsonEqual(leftKeys, rightKeys))
|
|
21
|
+
return false;
|
|
22
|
+
return leftKeys.every((key) => jsonEqual(left[key], right[key]));
|
|
23
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/refusal-envelope-conformance/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/refusal-envelope-conformance/index.ts"],"names":[],"mappings":"AA4DA;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,2DAA2D;IAC3D,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,QAAQ,CAAC,iBAAiB,EAAE;QAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,OAAO,EAAE,yBAAyB,CAAC;KAC7C,CAAC;IACF,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,8BAA8B;IAC7C,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,wCAAwC;IACxC,QAAQ,CAAC,OAAO,EAAE,yBAAyB,CAAC;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAAC;CAChD;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,EAAE,SAAS,8BAA8B,EAOzE,CAAC;AAEF,sDAAsD;AACtD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACjD;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,GAAG,MAAM,CAAC;CACzD;AAED,0DAA0D;AAC1D,MAAM,WAAW,gCAAgC;IAC/C,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,SAAS,uBAAuB,EAAE,CAAC;CACrD;AAgBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,CACP,OAAO,EAAE,yBAAyB,KAC/B,sBAAsB,GAAG,IAAI,GACjC,gCAAgC,CAwBlC"}
|
|
@@ -55,6 +55,7 @@ import refuseLater from './cases/refuse-later.json' with { type: 'json' };
|
|
|
55
55
|
import refuseNever from './cases/refuse-never.json' with { type: 'json' };
|
|
56
56
|
import refuseNextPeriod from './cases/refuse-next-period.json' with { type: 'json' };
|
|
57
57
|
import refuseNonRenderSurface from './cases/refuse-non-render-surface.json' with { type: 'json' };
|
|
58
|
+
import { jsonEqual } from '../json-equal.js';
|
|
58
59
|
/**
|
|
59
60
|
* Every refusal-envelope case the kit ships, in deterministic order:
|
|
60
61
|
* the projectable cases by retry class (after-fix, next-period, later,
|
|
@@ -68,26 +69,6 @@ export const refusalEnvelopeCases = [
|
|
|
68
69
|
refuseNever,
|
|
69
70
|
refuseNonRenderSurface,
|
|
70
71
|
];
|
|
71
|
-
/** Structural equality over plain JSON data. */
|
|
72
|
-
function jsonEqual(a, b) {
|
|
73
|
-
if (a === b)
|
|
74
|
-
return true;
|
|
75
|
-
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
79
|
-
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
80
|
-
return false;
|
|
81
|
-
return a.every((item, index) => jsonEqual(item, b[index]));
|
|
82
|
-
}
|
|
83
|
-
const left = a;
|
|
84
|
-
const right = b;
|
|
85
|
-
const leftKeys = Object.keys(left).sort();
|
|
86
|
-
const rightKeys = Object.keys(right).sort();
|
|
87
|
-
if (!jsonEqual(leftKeys, rightKeys))
|
|
88
|
-
return false;
|
|
89
|
-
return leftKeys.every((key) => jsonEqual(left[key], right[key]));
|
|
90
|
-
}
|
|
91
72
|
function projectionEqual(a, b) {
|
|
92
73
|
if (a === null || b === null)
|
|
93
74
|
return a === b;
|
|
@@ -4,6 +4,7 @@ import { type MatchResult } from './match-behavior.js';
|
|
|
4
4
|
import type { AuthConfig, SessionStateBehavior, StreamUpdateBehavior, VersionMismatchBehavior } from './types.js';
|
|
5
5
|
import { type PreGenerationRefusalInput, type ProjectedRefusalResult } from './refusal-envelope-conformance/index.js';
|
|
6
6
|
import { type RefusalRegistryView } from './registry-completeness/index.js';
|
|
7
|
+
import { type ProjectedTransportRefusal, type TransportRefusalInput } from './transport-refusal-conformance/index.js';
|
|
7
8
|
export interface RunConformanceConfig {
|
|
8
9
|
/**
|
|
9
10
|
* URL of the implementation under test. `http://` / `https://`
|
|
@@ -47,6 +48,14 @@ export interface RunConformanceConfig {
|
|
|
47
48
|
* this run; omitted ⇒ reported SKIPPED, same reasoning as above.
|
|
48
49
|
*/
|
|
49
50
|
readonly refusalRegistry?: RefusalRegistryView;
|
|
51
|
+
/**
|
|
52
|
+
* The deployment's PER-APP ENDPOINT refusal projector (SPEC §7.1's
|
|
53
|
+
* endpoint-level refusal, ggui#825): given a registry refusal, return
|
|
54
|
+
* the HTTP status + JSON-RPC error object the endpoint answers with, or
|
|
55
|
+
* `null` when the code has no transport envelope. Supplied ⇒ the
|
|
56
|
+
* `transport-refusal` catalog is graded; omitted ⇒ SKIPPED, named.
|
|
57
|
+
*/
|
|
58
|
+
readonly transportRefusalProjector?: (refusal: TransportRefusalInput) => ProjectedTransportRefusal | null;
|
|
50
59
|
}
|
|
51
60
|
/**
|
|
52
61
|
* Row-name prefixes for the PURE-FUNCTION catalogs the runner folds
|
|
@@ -54,7 +63,7 @@ export interface RunConformanceConfig {
|
|
|
54
63
|
* WebSocket fixture name, and so the reporter can group them without a
|
|
55
64
|
* second registry of names.
|
|
56
65
|
*/
|
|
57
|
-
export declare const PURE_FUNCTION_CATALOG_SLUGS: readonly ["refusal-envelope", "registry-completeness"];
|
|
66
|
+
export declare const PURE_FUNCTION_CATALOG_SLUGS: readonly ["refusal-envelope", "registry-completeness", "transport-refusal"];
|
|
58
67
|
/** One member of {@link PURE_FUNCTION_CATALOG_SLUGS}. */
|
|
59
68
|
export type PureFunctionCatalogSlug = (typeof PURE_FUNCTION_CATALOG_SLUGS)[number];
|
|
60
69
|
export interface ConformanceResult {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-conformance.d.ts","sourceRoot":"","sources":["../src/run-conformance.ts"],"names":[],"mappings":"AA6DA,OAAO,KAAK,EACV,0BAA0B,EAG3B,MAAM,mBAAmB,CAAC;AAG3B,OAAO,KAAK,EACV,eAAe,EACf,SAAS,IAAI,aAAa,EAE3B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,KAAK,EAEV,UAAU,EAEV,oBAAoB,EACpB,oBAAoB,EAEpB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAGL,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC5B,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"run-conformance.d.ts","sourceRoot":"","sources":["../src/run-conformance.ts"],"names":[],"mappings":"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"}
|
package/dist/run-conformance.js
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* loop is empty today.)
|
|
48
48
|
* l. Record the outcome via reporter + accumulate in result.
|
|
49
49
|
* 3. Fold in the PURE-FUNCTION catalogs (ggui#786) —
|
|
50
|
-
* `refusal-envelope` and `
|
|
50
|
+
* `refusal-envelope`, `registry-completeness` and `transport-refusal`. They grade a
|
|
51
51
|
* caller-supplied projector / registry rather than the wire, so
|
|
52
52
|
* they need no transport; when the caller supplies neither, their
|
|
53
53
|
* rows are reported SKIPPED with that reason so an ungraded
|
|
@@ -65,6 +65,7 @@ import { behaviorIs, deepEqual, matchBehavior } from './match-behavior.js';
|
|
|
65
65
|
import { openWsTransport } from './ws-transport.js';
|
|
66
66
|
import { refusalEnvelopeCases, runRefusalEnvelopeConformance, } from './refusal-envelope-conformance/index.js';
|
|
67
67
|
import { registryCompletenessPins, runRegistryCompletenessConformance, } from './registry-completeness/index.js';
|
|
68
|
+
import { runTransportRefusalConformance, transportRefusalCases, } from './transport-refusal-conformance/index.js';
|
|
68
69
|
/**
|
|
69
70
|
* Row-name prefixes for the PURE-FUNCTION catalogs the runner folds
|
|
70
71
|
* into a run. Prefixed so a catalog row can never collide with a
|
|
@@ -74,6 +75,7 @@ import { registryCompletenessPins, runRegistryCompletenessConformance, } from '.
|
|
|
74
75
|
export const PURE_FUNCTION_CATALOG_SLUGS = [
|
|
75
76
|
'refusal-envelope',
|
|
76
77
|
'registry-completeness',
|
|
78
|
+
'transport-refusal',
|
|
77
79
|
];
|
|
78
80
|
// =============================================================================
|
|
79
81
|
// Runner
|
|
@@ -161,7 +163,7 @@ function runPureFunctionCatalogs(config, reporter, buckets) {
|
|
|
161
163
|
const projector = config.refusalProjector;
|
|
162
164
|
if (projector === undefined) {
|
|
163
165
|
for (const testCase of refusalEnvelopeCases) {
|
|
164
|
-
skip(`refusal-envelope/${testCase.name}`, 'no `refusalProjector` supplied — pass one to runConformance() to grade SPEC §7.1\u2019s refused arm');
|
|
166
|
+
skip(`refusal-envelope/${testCase.name}`, 'no `refusalProjector` supplied — pass one to runConformance(), or `--projector <module>` on the CLI, to grade SPEC §7.1\u2019s refused arm');
|
|
165
167
|
}
|
|
166
168
|
}
|
|
167
169
|
else {
|
|
@@ -178,11 +180,32 @@ function runPureFunctionCatalogs(config, reporter, buckets) {
|
|
|
178
180
|
});
|
|
179
181
|
}
|
|
180
182
|
}
|
|
183
|
+
// ── transport-refusal (ggui#825) ──
|
|
184
|
+
const endpointProjector = config.transportRefusalProjector;
|
|
185
|
+
if (endpointProjector === undefined) {
|
|
186
|
+
for (const testCase of transportRefusalCases) {
|
|
187
|
+
skip(`transport-refusal/${testCase.name}`, 'no `transportRefusalProjector` supplied — pass one to runConformance(), or `--transport-projector <module>` on the CLI, to grade the endpoint-level refusal (SPEC §7.1, mcp-endpoint surface)');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const endpoint = runTransportRefusalConformance(endpointProjector);
|
|
192
|
+
for (const name of endpoint.passed)
|
|
193
|
+
pass(`transport-refusal/${name}`);
|
|
194
|
+
for (const mismatch of endpoint.failed) {
|
|
195
|
+
fail({
|
|
196
|
+
name: `transport-refusal/${mismatch.name}`,
|
|
197
|
+
criterion: 'endpoint-level refusal envelope (SPEC §7.1, mcp-endpoint surface)',
|
|
198
|
+
expected: mismatch.expected,
|
|
199
|
+
received: mismatch.actual,
|
|
200
|
+
message: 'the projected endpoint refusal does not match the catalog case',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
181
204
|
// ── registry-completeness ──
|
|
182
205
|
const registry = config.refusalRegistry;
|
|
183
206
|
if (registry === undefined) {
|
|
184
207
|
for (const pin of registryCompletenessPins) {
|
|
185
|
-
skip(`registry-completeness/${pin.name}`, 'no `refusalRegistry` supplied — pass the deployment\u2019s refusal-code registry to runConformance() to grade it');
|
|
208
|
+
skip(`registry-completeness/${pin.name}`, 'no `refusalRegistry` supplied — pass the deployment\u2019s refusal-code registry to runConformance(), or `--registry <file.json>` on the CLI, to grade it');
|
|
186
209
|
}
|
|
187
210
|
}
|
|
188
211
|
else {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "refuse-deprovisioned-endpoint",
|
|
3
|
+
"description": "A deprovisioned app answers EVERY JSON-RPC request on its per-app MCP endpoint (initialize included) with HTTP 403 and a JSON-RPC error whose data.refusal carries the registry projection \u2014 code, message, fix, retry \u2014 so the client reads app_deprovisioned instead of guessing between a deleted app and a bad credential. error.code is -32003 (APP_NOT_FOUND \u2014 the endpoint no longer serves this app) and error.message is App not found; an untyped authorization failure answers -32001 (UNAUTHORIZED) with no data, so a client can tell the two apart by code and can tell neither from a dropped socket by mistake \u2014 -32000 is the SDK client's ConnectionClosed and a first-party server never chooses it (ggui#836).",
|
|
4
|
+
"refusal": {
|
|
5
|
+
"code": "app_deprovisioned",
|
|
6
|
+
"message": "the app record has no owner claim any more \u2014 the app was deprovisioned",
|
|
7
|
+
"fix": "provision the app again; this app id never renders again",
|
|
8
|
+
"retry": "never"
|
|
9
|
+
},
|
|
10
|
+
"expect": {
|
|
11
|
+
"httpStatus": 403,
|
|
12
|
+
"error": {
|
|
13
|
+
"code": -32003,
|
|
14
|
+
"message": "App not found",
|
|
15
|
+
"data": {
|
|
16
|
+
"refusal": {
|
|
17
|
+
"code": "app_deprovisioned",
|
|
18
|
+
"message": "the app record has no owner claim any more \u2014 the app was deprovisioned",
|
|
19
|
+
"fix": "provision the app again; this app id never renders again",
|
|
20
|
+
"retry": "never"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "refuse-render-only-code",
|
|
3
|
+
"description": "A REGISTERED code whose surfaces exclude mcp-endpoint has no transport envelope: the per-app endpoint's authorization never speaks it, and a projector that types it anyway would put a render-gate state on a surface the registry does not list. The three auth arms (no issuer, foreign issuer, denied subject) stay the bare 403 by the same rule \u2014 naming any of them would say which is true.",
|
|
4
|
+
"refusal": {
|
|
5
|
+
"code": "insufficient_credit",
|
|
6
|
+
"message": "the configured allowance for this app is exhausted",
|
|
7
|
+
"fix": "top up the allowance for this app, then retry",
|
|
8
|
+
"retry": "after-fix"
|
|
9
|
+
},
|
|
10
|
+
"expect": null
|
|
11
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The refusal handed to the projector — the registry projection on the
|
|
3
|
+
* transport surface. Authored here rather than imported from
|
|
4
|
+
* `@ggui-ai/protocol` for the same reason as the render catalog: the kit
|
|
5
|
+
* grades the WIRE, and a stringly input keeps a stale kit from typing
|
|
6
|
+
* its way past a registry change.
|
|
7
|
+
*/
|
|
8
|
+
export interface TransportRefusalInput {
|
|
9
|
+
/** Registered refusal code. */
|
|
10
|
+
readonly code: string;
|
|
11
|
+
/** Precise diagnostic. */
|
|
12
|
+
readonly message: string;
|
|
13
|
+
/** The one recovery step. */
|
|
14
|
+
readonly fix: string;
|
|
15
|
+
/** Retry class (`after-fix` | `next-period` | `later` | `never`). */
|
|
16
|
+
readonly retry: string;
|
|
17
|
+
}
|
|
18
|
+
/** What the endpoint answers: the HTTP status and the JSON-RPC error object. */
|
|
19
|
+
export interface ProjectedTransportRefusal {
|
|
20
|
+
readonly httpStatus: number;
|
|
21
|
+
readonly error: {
|
|
22
|
+
readonly code: number;
|
|
23
|
+
readonly message: string;
|
|
24
|
+
readonly data: {
|
|
25
|
+
readonly refusal: TransportRefusalInput;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** One transport-refusal case, authored as JSON under `./cases/`. */
|
|
30
|
+
export interface TransportRefusalConformanceCase {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly description: string;
|
|
33
|
+
readonly refusal: TransportRefusalInput;
|
|
34
|
+
/** `null` when the code has no transport envelope (surfaces exclude `mcp-endpoint`). */
|
|
35
|
+
readonly expect: ProjectedTransportRefusal | null;
|
|
36
|
+
}
|
|
37
|
+
export declare const transportRefusalCases: readonly TransportRefusalConformanceCase[];
|
|
38
|
+
export interface TransportRefusalMismatch {
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly expected: ProjectedTransportRefusal | null;
|
|
41
|
+
/** What the projector produced — or the message of the error it threw. */
|
|
42
|
+
readonly actual: ProjectedTransportRefusal | null | string;
|
|
43
|
+
}
|
|
44
|
+
export interface TransportRefusalConformanceResult {
|
|
45
|
+
readonly passed: readonly string[];
|
|
46
|
+
readonly failed: readonly TransportRefusalMismatch[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Grade a per-app endpoint refusal projector against the catalog. Pure
|
|
50
|
+
* and synchronous: no server, no transport — the deterministic
|
|
51
|
+
* projection is the whole obligation.
|
|
52
|
+
*/
|
|
53
|
+
export declare function runTransportRefusalConformance(project: (refusal: TransportRefusalInput) => ProjectedTransportRefusal | null): TransportRefusalConformanceResult;
|
|
54
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transport-refusal-conformance/index.ts"],"names":[],"mappings":"AAyBA;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0BAA0B;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6BAA6B;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,gFAAgF;AAChF,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,IAAI,EAAE;YAAE,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAA;SAAE,CAAC;KAC5D,CAAC;CACH;AAED,qEAAqE;AACrE,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,wFAAwF;IACxF,QAAQ,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,CAAC;CACnD;AAED,eAAO,MAAM,qBAAqB,EAAE,SAAS,+BAA+B,EAG3E,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,yBAAyB,GAAG,IAAI,CAAC;IACpD,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,GAAG,MAAM,CAAC;CAC5D;AAED,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,wBAAwB,EAAE,CAAC;CACtD;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,yBAAyB,GAAG,IAAI,GAC5E,iCAAiC,CAiBnC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-refusal conformance catalog (ggui#825) — grades a deployment's
|
|
3
|
+
* PER-APP ENDPOINT refusal projector: the function that turns a registry
|
|
4
|
+
* refusal into the JSON-RPC error a per-app MCP endpoint answers with when
|
|
5
|
+
* it refuses a request for a typed reason, on ANY method (`initialize`
|
|
6
|
+
* included).
|
|
7
|
+
*
|
|
8
|
+
* The contract it makes observable: a deprovisioned app answers HTTP 403
|
|
9
|
+
* with `{ code: -32003, message: "App not found", data: { refusal } }` where
|
|
10
|
+
* `refusal` is the registry projection without the render-only fields
|
|
11
|
+
* (`code`, `message`, `fix`, `retry`); an authorization failure that is
|
|
12
|
+
* NOT a registry state answers 403 with `-32001` and no `data` — the untyped
|
|
13
|
+
* arms deliberately indistinguishable among themselves, so a client never
|
|
14
|
+
* learns which one it hit (codes ruled in ggui#836).
|
|
15
|
+
*
|
|
16
|
+
* Each case ships as raw JSON under `./cases/`. The kit never imports a
|
|
17
|
+
* concrete server: the adopter supplies the projector, the catalog grades
|
|
18
|
+
* it, and a code whose `surfaces` exclude `mcp-endpoint` must project to
|
|
19
|
+
* `null` (no transport envelope exists for it).
|
|
20
|
+
*/
|
|
21
|
+
import refuseDeprovisionedEndpoint from './cases/refuse-deprovisioned-endpoint.json' with { type: 'json' };
|
|
22
|
+
import refuseRenderOnlyCode from './cases/refuse-render-only-code.json' with { type: 'json' };
|
|
23
|
+
import { jsonEqual } from '../json-equal.js';
|
|
24
|
+
export const transportRefusalCases = [
|
|
25
|
+
refuseDeprovisionedEndpoint,
|
|
26
|
+
refuseRenderOnlyCode,
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* Grade a per-app endpoint refusal projector against the catalog. Pure
|
|
30
|
+
* and synchronous: no server, no transport — the deterministic
|
|
31
|
+
* projection is the whole obligation.
|
|
32
|
+
*/
|
|
33
|
+
export function runTransportRefusalConformance(project) {
|
|
34
|
+
const passed = [];
|
|
35
|
+
const failed = [];
|
|
36
|
+
for (const testCase of transportRefusalCases) {
|
|
37
|
+
let actual;
|
|
38
|
+
try {
|
|
39
|
+
actual = project(testCase.refusal);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
actual = error instanceof Error ? error.message : String(error);
|
|
43
|
+
}
|
|
44
|
+
if (typeof actual !== 'string' && jsonEqual(actual, testCase.expect)) {
|
|
45
|
+
passed.push(testCase.name);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
failed.push({ name: testCase.name, expected: testCase.expect, actual });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { passed, failed };
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/protocol-conformance",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Conformance test kit for the ggui protocol. Drives a behavioral fixture catalog against a live implementation over WebSocket, a resources/read catalog against a live MCP server, and
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"description": "Conformance test kit for the ggui protocol. Drives a behavioral fixture catalog against a live implementation over WebSocket, a resources/read catalog against a live MCP server, and nine pure-function catalogs (schema, registration, resolution, binding, props-schema, host-helper, refusal-envelope, registry-completeness, transport-refusal) for the gadget, tool-binding, schema-precise-render, host-helper and pre-generation-refusal obligations, reporting a scorecard-style pass / fail / skip. MCP builders adopt this kit to prove their implementation is protocol-conformant.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ggui",
|
|
@@ -75,11 +75,16 @@
|
|
|
75
75
|
"types": "./dist/registry-completeness/index.d.ts",
|
|
76
76
|
"import": "./dist/registry-completeness/index.js",
|
|
77
77
|
"default": "./dist/registry-completeness/index.js"
|
|
78
|
+
},
|
|
79
|
+
"./transport-refusal-conformance": {
|
|
80
|
+
"types": "./dist/transport-refusal-conformance/index.d.ts",
|
|
81
|
+
"import": "./dist/transport-refusal-conformance/index.js",
|
|
82
|
+
"default": "./dist/transport-refusal-conformance/index.js"
|
|
78
83
|
}
|
|
79
84
|
},
|
|
80
85
|
"dependencies": {
|
|
81
86
|
"ws": "^8.20.1",
|
|
82
|
-
"@ggui-ai/protocol": "0.
|
|
87
|
+
"@ggui-ai/protocol": "0.15.0"
|
|
83
88
|
},
|
|
84
89
|
"devDependencies": {
|
|
85
90
|
"@types/node": "^22.0.0",
|