@proof-holdings/delegation-self-refusal 0.1.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/LICENSE +21 -0
- package/README.md +264 -0
- package/dist/cache.d.ts +10 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +58 -0
- package/dist/cache.js.map +1 -0
- package/dist/guard.d.ts +19 -0
- package/dist/guard.d.ts.map +1 -0
- package/dist/guard.js +57 -0
- package/dist/guard.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +8 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +123 -0
- package/dist/install.js.map +1 -0
- package/dist/poll.d.ts +10 -0
- package/dist/poll.d.ts.map +1 -0
- package/dist/poll.js +60 -0
- package/dist/poll.js.map +1 -0
- package/dist/refusal.d.ts +92 -0
- package/dist/refusal.d.ts.map +1 -0
- package/dist/refusal.js +188 -0
- package/dist/refusal.js.map +1 -0
- package/dist/registrar.d.ts +77 -0
- package/dist/registrar.d.ts.map +1 -0
- package/dist/registrar.js +106 -0
- package/dist/registrar.js.map +1 -0
- package/dist/result.d.ts +16 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +12 -0
- package/dist/result.js.map +1 -0
- package/dist/schedule.d.ts +14 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +20 -0
- package/dist/schedule.js.map +1 -0
- package/dist/showcase/breaker.d.ts +91 -0
- package/dist/showcase/breaker.d.ts.map +1 -0
- package/dist/showcase/breaker.js +132 -0
- package/dist/showcase/breaker.js.map +1 -0
- package/dist/showcase/connect.d.ts +39 -0
- package/dist/showcase/connect.d.ts.map +1 -0
- package/dist/showcase/connect.js +98 -0
- package/dist/showcase/connect.js.map +1 -0
- package/dist/showcase/descriptions.d.ts +46 -0
- package/dist/showcase/descriptions.d.ts.map +1 -0
- package/dist/showcase/descriptions.js +86 -0
- package/dist/showcase/descriptions.js.map +1 -0
- package/dist/showcase/instructions.d.ts +13 -0
- package/dist/showcase/instructions.d.ts.map +1 -0
- package/dist/showcase/instructions.js +20 -0
- package/dist/showcase/instructions.js.map +1 -0
- package/dist/showcase/marked-fetch.d.ts +37 -0
- package/dist/showcase/marked-fetch.d.ts.map +1 -0
- package/dist/showcase/marked-fetch.js +48 -0
- package/dist/showcase/marked-fetch.js.map +1 -0
- package/dist/showcase/tools.d.ts +49 -0
- package/dist/showcase/tools.d.ts.map +1 -0
- package/dist/showcase/tools.js +198 -0
- package/dist/showcase/tools.js.map +1 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/verdict.d.ts +47 -0
- package/dist/verdict.d.ts.map +1 -0
- package/dist/verdict.js +89 -0
- package/dist/verdict.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { refusalMessage } from './refusal.js';
|
|
2
|
+
import { errorResult } from './result.js';
|
|
3
|
+
import { currentVerdict } from './verdict.js';
|
|
4
|
+
export const INSTALLED_MARKER = Symbol.for('proof-holdings.delegation-self-refusal.installed');
|
|
5
|
+
export function readInstallKind(server) {
|
|
6
|
+
const value = server[INSTALLED_MARKER];
|
|
7
|
+
if (value === 'optout')
|
|
8
|
+
return 'optout';
|
|
9
|
+
// ANY other truthy value reads as `gated`, not as absent. The marker key is a `Symbol.for`, so it
|
|
10
|
+
// is shared with every OTHER copy of this package in the process — and the direction that matters
|
|
11
|
+
// is FORWARD: a newer copy may write a kind this version has never heard of, and a strict
|
|
12
|
+
// two-string check would read such a server as untouched and install a second layer over an
|
|
13
|
+
// existing gate. (An earlier version of this comment justified the widening by a copy written
|
|
14
|
+
// BEFORE kinds existed. That was false and a reviewer proved it: the marker string appears in no
|
|
15
|
+
// commit — `git log --all -S` finds nothing — and no version of this package had been published
|
|
16
|
+
// at the time, so the FIRST installable version already carries kinds. The rule is right; the
|
|
17
|
+
// story was not.)
|
|
18
|
+
// Erring toward `gated` errs toward refusing, which is the direction that cannot silently ungate
|
|
19
|
+
// anything; the cost is a clear error where an exotic combination might have been survivable.
|
|
20
|
+
if (value)
|
|
21
|
+
return 'gated';
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
export function markInstalled(server, kind) {
|
|
25
|
+
server[INSTALLED_MARKER] = kind;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Refuses a second call that cannot cover the registrar a first call already handed out.
|
|
29
|
+
*
|
|
30
|
+
* The two states fail for OPPOSITE reasons, which is why one summary cannot serve both. After
|
|
31
|
+
* `gated`, a second call would install over a working gate. After `optout`, the raw registrar stays
|
|
32
|
+
* perfectly VALID and simply bypasses whatever is installed next — nothing invalidated it. The
|
|
33
|
+
* message below is picked from the kind for that reason: claiming an installed layer where only a
|
|
34
|
+
* raw registrar was handed out sends the reader looking for something that does not exist.
|
|
35
|
+
*/
|
|
36
|
+
export function assertNotAlreadyInstalled(server, entryPoint) {
|
|
37
|
+
const kind = readInstallKind(server);
|
|
38
|
+
if (kind === undefined)
|
|
39
|
+
return;
|
|
40
|
+
if (kind === 'optout') {
|
|
41
|
+
throw new Error(`${entryPoint}: a previous call with no token already handed out this server's RAW ` +
|
|
42
|
+
'registrar. Whatever is installed now cannot cover it — tools registered through the ' +
|
|
43
|
+
'registrar that is already out would bypass it. Configure the token before the first ' +
|
|
44
|
+
'call, or keep using the un-gated one.');
|
|
45
|
+
}
|
|
46
|
+
// No per-case CONSEQUENCE in the text. The previous tail ("running both leaves the showcase
|
|
47
|
+
// gated by it") held only where a showcase is registered after a gate exists, and was false in
|
|
48
|
+
// guard→guard (no showcase anywhere) and install→guard (the showcase is already registered,
|
|
49
|
+
// un-gated, and a later gate cannot retroactively cover it). What IS true in all four gated
|
|
50
|
+
// cells is only the state and the instruction, so that is all this says.
|
|
51
|
+
throw new Error(`${entryPoint}: this server already carries the Proof self-refusal layer. Install it exactly ` +
|
|
52
|
+
'ONCE — call either guardDelegation or installProofLayer, never both (installProofLayer ' +
|
|
53
|
+
'includes the gate).');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The shared precondition both entry points check: the SDK must expose `_registeredTools`, and
|
|
57
|
+
* nothing may be registered yet. Placed late, this package does nothing — `.bind()` snapshots the
|
|
58
|
+
* function value — so refusing to run is the only honest answer to "installed too late".
|
|
59
|
+
*/
|
|
60
|
+
export function assertInstallableBeforeRegistration(server, entryPoint) {
|
|
61
|
+
// The entry point is a PARAMETER because this message is read by a publisher staring at a
|
|
62
|
+
// startup crash: naming `guardDelegation` when they called `installProofLayer` sends them to fix
|
|
63
|
+
// a function they never invoked — the same ambiguity SC-13 exists to remove at the other
|
|
64
|
+
// precondition.
|
|
65
|
+
if (server._registeredTools === undefined) {
|
|
66
|
+
throw new Error(`${entryPoint}: the installed @modelcontextprotocol/sdk McpServer does not expose ` +
|
|
67
|
+
'_registeredTools — this package cannot verify no tools were registered before the gate ' +
|
|
68
|
+
'and refuses to run silently unguarded. Pin a known-compatible SDK version.');
|
|
69
|
+
}
|
|
70
|
+
const alreadyRegistered = Object.keys(server._registeredTools);
|
|
71
|
+
if (alreadyRegistered.length > 0) {
|
|
72
|
+
throw new Error(`${entryPoint} must run before any server.tool()/registerTool() call — ` +
|
|
73
|
+
`${alreadyRegistered.length} tool(s) already registered: ${alreadyRegistered.join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Wraps a registrar (`server.tool` or `server.registerTool` — both take the handler as their
|
|
78
|
+
* final positional argument) so every call through it is gated on `currentVerdict`.
|
|
79
|
+
*
|
|
80
|
+
* The dispatch check is a deliberate ALLOW-LIST (`kind === 'valid'` runs the handler; anything
|
|
81
|
+
* else, including a shape `currentVerdict`'s return type does not admit but that could reach here
|
|
82
|
+
* through some future bug, refuses) rather than a deny-list keyed on `'refused'` — a security gate
|
|
83
|
+
* must default to denying what it does not positively recognize as authorized.
|
|
84
|
+
*
|
|
85
|
+
* The handler's own arguments are passed through OPAQUELY: never read, indexed, logged or
|
|
86
|
+
* serialized. This package runs inside someone else's production process, where those arguments
|
|
87
|
+
* are that publisher's user data — `src/__tests__/drift/mcp-showcase.test.ts` pins the property
|
|
88
|
+
* behaviourally rather than trusting this comment.
|
|
89
|
+
*/
|
|
90
|
+
export function wrapRegistrar(original, opts) {
|
|
91
|
+
return (...args) => {
|
|
92
|
+
const handler = args[args.length - 1];
|
|
93
|
+
if (typeof handler !== 'function') {
|
|
94
|
+
return original(...args);
|
|
95
|
+
}
|
|
96
|
+
const gatedHandler = async (...handlerArgs) => {
|
|
97
|
+
const verdict = await currentVerdict(opts);
|
|
98
|
+
if (verdict.kind === 'valid') {
|
|
99
|
+
return handler(...handlerArgs);
|
|
100
|
+
}
|
|
101
|
+
return errorResult(refusalMessage(opts.principal, verdict.reason));
|
|
102
|
+
};
|
|
103
|
+
return original(...args.slice(0, -1), gatedHandler);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=registrar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registrar.js","sourceRoot":"","sources":["../src/registrar.ts"],"names":[],"mappings":"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"}
|
package/dist/result.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Matches the MCP `CallToolResult` shape (content blocks + isError) without a runtime SDK import. */
|
|
2
|
+
export interface ToolResult {
|
|
3
|
+
content: Array<{
|
|
4
|
+
type: 'text';
|
|
5
|
+
text: string;
|
|
6
|
+
}>;
|
|
7
|
+
isError: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function errorResult(message: string): ToolResult;
|
|
10
|
+
/**
|
|
11
|
+
* A successful result carrying structured data. The MCP content model has no JSON block, so the
|
|
12
|
+
* repo-wide convention (mcp/src/types.ts `jsonResult`) is a single pretty-printed text block —
|
|
13
|
+
* mirrored here rather than imported, since this package cannot depend on the MCP server.
|
|
14
|
+
*/
|
|
15
|
+
export declare function jsonResult(data: unknown): ToolResult;
|
|
16
|
+
//# sourceMappingURL=result.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../src/result.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAEvD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU,CAEpD"}
|
package/dist/result.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function errorResult(message) {
|
|
2
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* A successful result carrying structured data. The MCP content model has no JSON block, so the
|
|
6
|
+
* repo-wide convention (mcp/src/types.ts `jsonResult`) is a single pretty-printed text block —
|
|
7
|
+
* mirrored here rather than imported, since this package cannot depend on the MCP server.
|
|
8
|
+
*/
|
|
9
|
+
export function jsonResult(data) {
|
|
10
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], isError: false };
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=result.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result.js","sourceRoot":"","sources":["../src/result.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAAa;IACtC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAC9F,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hard ceiling on how many consecutive unresolved polls may be bridged by serving the last good
|
|
3
|
+
* verdict before the gate refuses outright. This is a library constant, not a GuardOptions field
|
|
4
|
+
* — SEC-DLG-02 (b-brainstorm-delegation-revocation) found an unbounded grace window CRITICAL, and
|
|
5
|
+
* the recorded fix was a bound the caller cannot raise, not a documented recommendation.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MAX_GRACE_FAILURES = 3;
|
|
8
|
+
/**
|
|
9
|
+
* Returns baseMs plus a jitter in [1, baseMs * JITTER_FRACTION), guaranteeing the result is never
|
|
10
|
+
* an exact multiple of 60_000 when baseMs is (the default case) — a fleet of installs polling on
|
|
11
|
+
* the wall-clock minute is a self-inflicted thundering herd against /proofs/validate.
|
|
12
|
+
*/
|
|
13
|
+
export declare function nextIntervalMs(baseMs?: number): number;
|
|
14
|
+
//# sourceMappingURL=schedule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAKpC;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,GAAE,MAAiC,GAAG,MAAM,CAIhF"}
|
package/dist/schedule.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hard ceiling on how many consecutive unresolved polls may be bridged by serving the last good
|
|
3
|
+
* verdict before the gate refuses outright. This is a library constant, not a GuardOptions field
|
|
4
|
+
* — SEC-DLG-02 (b-brainstorm-delegation-revocation) found an unbounded grace window CRITICAL, and
|
|
5
|
+
* the recorded fix was a bound the caller cannot raise, not a documented recommendation.
|
|
6
|
+
*/
|
|
7
|
+
export const MAX_GRACE_FAILURES = 3;
|
|
8
|
+
const DEFAULT_POLL_INTERVAL_MS = 60_000;
|
|
9
|
+
const JITTER_FRACTION = 0.2;
|
|
10
|
+
/**
|
|
11
|
+
* Returns baseMs plus a jitter in [1, baseMs * JITTER_FRACTION), guaranteeing the result is never
|
|
12
|
+
* an exact multiple of 60_000 when baseMs is (the default case) — a fleet of installs polling on
|
|
13
|
+
* the wall-clock minute is a self-inflicted thundering herd against /proofs/validate.
|
|
14
|
+
*/
|
|
15
|
+
export function nextIntervalMs(baseMs = DEFAULT_POLL_INTERVAL_MS) {
|
|
16
|
+
const jitterCeiling = Math.max(1, Math.floor(baseMs * JITTER_FRACTION));
|
|
17
|
+
const jitter = 1 + Math.floor(Math.random() * jitterCeiling);
|
|
18
|
+
return baseMs + jitter;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=schedule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule.js","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAEpC,MAAM,wBAAwB,GAAG,MAAM,CAAC;AACxC,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB,wBAAwB;IACtE,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,eAAe,CAAC,CAAC,CAAC;IACxE,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,aAAa,CAAC,CAAC;IAC7D,OAAO,MAAM,GAAG,MAAM,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hard timeout for a whole showcase call — not for each HTTP request inside it.
|
|
3
|
+
*
|
|
4
|
+
* Scope, stated because the name does not carry it: this bounds the two showcase tools that call
|
|
5
|
+
* out on their OWN behalf (`proof_verify_delegation`, `proof_connect`).
|
|
6
|
+
* `proof_check_this_server` is deliberately outside it — it reads the gate's shared verdict
|
|
7
|
+
* (SC-3), so its network cost is `poll.ts`'s 10s budget, the same one every gated publisher tool
|
|
8
|
+
* already pays, and wrapping it would fork the grace state machine `schedule.ts` owns.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately BELOW both library defaults it sits in front of — `@proof-holdings/delegation-verifier`
|
|
11
|
+
* defaults to 5000ms and `poll.ts` to 10000ms — and applied to the ENTIRE operation, because the
|
|
12
|
+
* verifier's own budget is PER FETCH and one verification walks up to `SHOWCASE_MAX_LEGS` requests
|
|
13
|
+
* sequentially. Giving both bounds the same number would permit a ~9s user-facing stall, which is
|
|
14
|
+
* exactly the "our slowness is indistinguishable from the publisher's code" failure this constant
|
|
15
|
+
* exists to prevent. Pinned by `breaker.test.ts`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const SHOWCASE_TIMEOUT_MS = 4500;
|
|
18
|
+
/**
|
|
19
|
+
* How many sequential HTTP legs one verification can take: JWKS → status list → status endpoint.
|
|
20
|
+
*
|
|
21
|
+
* THREE, not two. `checkDelegationStatus` (packages/delegation-verifier/src/status.ts) falls
|
|
22
|
+
* through to the status ENDPOINT whenever the status list cannot answer, so the third leg is
|
|
23
|
+
* reachable on a live issuer. An earlier version of this file derived the per-request budget from
|
|
24
|
+
* a two-leg walk while the comment above it said three — and at two legs the arithmetic was a
|
|
25
|
+
* photo finish (2 × 1500 = 3000, exactly the whole-call bound), so any parse or verify time
|
|
26
|
+
* between them pushed a healthy-but-slow issuer over the deadline and charged it as a failure.
|
|
27
|
+
*/
|
|
28
|
+
export declare const SHOWCASE_MAX_LEGS = 3;
|
|
29
|
+
/**
|
|
30
|
+
* The budget handed to a LIBRARY that applies it per request, derived from the leg count PLUS ONE
|
|
31
|
+
* so the whole-call bound is a real ceiling with headroom rather than a photo finish.
|
|
32
|
+
*
|
|
33
|
+
* Dividing by the leg count exactly (the first attempt at this fix) reproduced the very defect the
|
|
34
|
+
* comment above rejects: `3 × 1000 = 3000` IS the whole-call bound, leaving zero milliseconds for
|
|
35
|
+
* the work BETWEEN the legs — JWKS parsing, JWT signature verification, JSON decoding — all of
|
|
36
|
+
* which sit inside the same deadline, because the timer starts before `fn` is invoked. The `+ 1`
|
|
37
|
+
* is that work's share.
|
|
38
|
+
*
|
|
39
|
+
* It also matters which way the arithmetic errs. A per-fetch budget squeezed too tight aborts a leg
|
|
40
|
+
* against a merely-average issuer, the verifier reports `jwks_unavailable`, `isIssuerUnreachable`
|
|
41
|
+
* charges it to the breaker, and three of those deny verification of EVERY artifact for a cooldown
|
|
42
|
+
* — a false `issuer_unreachable_cooldown` against a live service. So the total was raised (still
|
|
43
|
+
* strictly below the verifier's 5000ms and `poll.ts`'s 10000ms, which SC-7 requires) rather than
|
|
44
|
+
* the per-request budget being cut further.
|
|
45
|
+
*/
|
|
46
|
+
export declare const SHOWCASE_PER_REQUEST_TIMEOUT_MS: number;
|
|
47
|
+
export type BreakerResult<T> = {
|
|
48
|
+
ok: true;
|
|
49
|
+
value: T;
|
|
50
|
+
} | {
|
|
51
|
+
ok: false;
|
|
52
|
+
tripped: true;
|
|
53
|
+
} | {
|
|
54
|
+
ok: false;
|
|
55
|
+
tripped: false;
|
|
56
|
+
error: unknown;
|
|
57
|
+
};
|
|
58
|
+
export interface RunOptions<T> {
|
|
59
|
+
/**
|
|
60
|
+
* Decides whether a RETURNED value counts as a failure.
|
|
61
|
+
*
|
|
62
|
+
* Required because "did it throw" is the wrong question for this package's own dependencies:
|
|
63
|
+
* `@proof-holdings/delegation-verifier` reports an unreachable issuer by RETURNING
|
|
64
|
+
* `{valid: false, outcome: 'unconfirmed'}` rather than throwing, so a throw-only breaker read a
|
|
65
|
+
* dead issuer as a success, reset its counter on every call, and never opened — measured in code
|
|
66
|
+
* review as 10 outbound attempts with 0 trips. Without this hook SC-7 is unmet for the very tool
|
|
67
|
+
* that makes the most outbound calls.
|
|
68
|
+
*/
|
|
69
|
+
isFailure?: (value: T) => boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface Breaker {
|
|
72
|
+
run<T>(fn: (signal: AbortSignal) => Promise<T>, options?: RunOptions<T>): Promise<BreakerResult<T>>;
|
|
73
|
+
}
|
|
74
|
+
export interface BreakerOptions {
|
|
75
|
+
failureThreshold?: number;
|
|
76
|
+
cooldownMs?: number;
|
|
77
|
+
timeoutMs?: number;
|
|
78
|
+
/** Injected clock. Tests advance it directly rather than reaching for fake global timers. */
|
|
79
|
+
now?: () => number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A per-installation circuit breaker for the showcase's outbound calls.
|
|
83
|
+
*
|
|
84
|
+
* State lives in this closure, NOT on disk: the cache directory belongs to the self-refusal grace
|
|
85
|
+
* state machine, and writing showcase state into it would move `lastCheckedAtMs` outside the
|
|
86
|
+
* jittered schedule `schedule.ts` exists to keep (the SEC-DLG-02 fix). The showcase therefore does
|
|
87
|
+
* not inherit the grace window and needs its own failure accounting — which is exactly what SC-7
|
|
88
|
+
* asks for.
|
|
89
|
+
*/
|
|
90
|
+
export declare function createBreaker(options?: BreakerOptions): Breaker;
|
|
91
|
+
//# sourceMappingURL=breaker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"breaker.d.ts","sourceRoot":"","sources":["../../src/showcase/breaker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAEzC;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,+BAA+B,QAA4D,CAAC;AAKzG,MAAM,MAAM,aAAa,CAAC,CAAC,IACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACtB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAElD,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;CACnC;AAED,MAAM,WAAW,OAAO;IACtB,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;CACrG;AAED,MAAM,WAAW,cAAc;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6FAA6F;IAC7F,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AASD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CA+EnE"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hard timeout for a whole showcase call — not for each HTTP request inside it.
|
|
3
|
+
*
|
|
4
|
+
* Scope, stated because the name does not carry it: this bounds the two showcase tools that call
|
|
5
|
+
* out on their OWN behalf (`proof_verify_delegation`, `proof_connect`).
|
|
6
|
+
* `proof_check_this_server` is deliberately outside it — it reads the gate's shared verdict
|
|
7
|
+
* (SC-3), so its network cost is `poll.ts`'s 10s budget, the same one every gated publisher tool
|
|
8
|
+
* already pays, and wrapping it would fork the grace state machine `schedule.ts` owns.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately BELOW both library defaults it sits in front of — `@proof-holdings/delegation-verifier`
|
|
11
|
+
* defaults to 5000ms and `poll.ts` to 10000ms — and applied to the ENTIRE operation, because the
|
|
12
|
+
* verifier's own budget is PER FETCH and one verification walks up to `SHOWCASE_MAX_LEGS` requests
|
|
13
|
+
* sequentially. Giving both bounds the same number would permit a ~9s user-facing stall, which is
|
|
14
|
+
* exactly the "our slowness is indistinguishable from the publisher's code" failure this constant
|
|
15
|
+
* exists to prevent. Pinned by `breaker.test.ts`.
|
|
16
|
+
*/
|
|
17
|
+
export const SHOWCASE_TIMEOUT_MS = 4_500;
|
|
18
|
+
/**
|
|
19
|
+
* How many sequential HTTP legs one verification can take: JWKS → status list → status endpoint.
|
|
20
|
+
*
|
|
21
|
+
* THREE, not two. `checkDelegationStatus` (packages/delegation-verifier/src/status.ts) falls
|
|
22
|
+
* through to the status ENDPOINT whenever the status list cannot answer, so the third leg is
|
|
23
|
+
* reachable on a live issuer. An earlier version of this file derived the per-request budget from
|
|
24
|
+
* a two-leg walk while the comment above it said three — and at two legs the arithmetic was a
|
|
25
|
+
* photo finish (2 × 1500 = 3000, exactly the whole-call bound), so any parse or verify time
|
|
26
|
+
* between them pushed a healthy-but-slow issuer over the deadline and charged it as a failure.
|
|
27
|
+
*/
|
|
28
|
+
export const SHOWCASE_MAX_LEGS = 3;
|
|
29
|
+
/**
|
|
30
|
+
* The budget handed to a LIBRARY that applies it per request, derived from the leg count PLUS ONE
|
|
31
|
+
* so the whole-call bound is a real ceiling with headroom rather than a photo finish.
|
|
32
|
+
*
|
|
33
|
+
* Dividing by the leg count exactly (the first attempt at this fix) reproduced the very defect the
|
|
34
|
+
* comment above rejects: `3 × 1000 = 3000` IS the whole-call bound, leaving zero milliseconds for
|
|
35
|
+
* the work BETWEEN the legs — JWKS parsing, JWT signature verification, JSON decoding — all of
|
|
36
|
+
* which sit inside the same deadline, because the timer starts before `fn` is invoked. The `+ 1`
|
|
37
|
+
* is that work's share.
|
|
38
|
+
*
|
|
39
|
+
* It also matters which way the arithmetic errs. A per-fetch budget squeezed too tight aborts a leg
|
|
40
|
+
* against a merely-average issuer, the verifier reports `jwks_unavailable`, `isIssuerUnreachable`
|
|
41
|
+
* charges it to the breaker, and three of those deny verification of EVERY artifact for a cooldown
|
|
42
|
+
* — a false `issuer_unreachable_cooldown` against a live service. So the total was raised (still
|
|
43
|
+
* strictly below the verifier's 5000ms and `poll.ts`'s 10000ms, which SC-7 requires) rather than
|
|
44
|
+
* the per-request budget being cut further.
|
|
45
|
+
*/
|
|
46
|
+
export const SHOWCASE_PER_REQUEST_TIMEOUT_MS = Math.floor(SHOWCASE_TIMEOUT_MS / (SHOWCASE_MAX_LEGS + 1));
|
|
47
|
+
const DEFAULT_FAILURE_THRESHOLD = 3;
|
|
48
|
+
const DEFAULT_COOLDOWN_MS = 60_000;
|
|
49
|
+
class ShowcaseTimeoutError extends Error {
|
|
50
|
+
constructor(timeoutMs) {
|
|
51
|
+
super(`showcase call exceeded its ${timeoutMs}ms budget`);
|
|
52
|
+
this.name = 'ShowcaseTimeoutError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A per-installation circuit breaker for the showcase's outbound calls.
|
|
57
|
+
*
|
|
58
|
+
* State lives in this closure, NOT on disk: the cache directory belongs to the self-refusal grace
|
|
59
|
+
* state machine, and writing showcase state into it would move `lastCheckedAtMs` outside the
|
|
60
|
+
* jittered schedule `schedule.ts` exists to keep (the SEC-DLG-02 fix). The showcase therefore does
|
|
61
|
+
* not inherit the grace window and needs its own failure accounting — which is exactly what SC-7
|
|
62
|
+
* asks for.
|
|
63
|
+
*/
|
|
64
|
+
export function createBreaker(options = {}) {
|
|
65
|
+
const failureThreshold = options.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
|
|
66
|
+
const cooldownMs = options.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
67
|
+
const timeoutMs = options.timeoutMs ?? SHOWCASE_TIMEOUT_MS;
|
|
68
|
+
const now = options.now ?? Date.now;
|
|
69
|
+
let consecutiveFailures = 0;
|
|
70
|
+
let openUntilMs = 0;
|
|
71
|
+
function recordFailure() {
|
|
72
|
+
consecutiveFailures++;
|
|
73
|
+
if (consecutiveFailures >= failureThreshold) {
|
|
74
|
+
openUntilMs = now() + cooldownMs;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
async run(fn, runOptions = {}) {
|
|
79
|
+
if (openUntilMs > now()) {
|
|
80
|
+
return { ok: false, tripped: true };
|
|
81
|
+
}
|
|
82
|
+
// The window elapsed: forget the streak that opened it, so one more failure does not
|
|
83
|
+
// instantly re-open on a stale count.
|
|
84
|
+
if (openUntilMs !== 0) {
|
|
85
|
+
openUntilMs = 0;
|
|
86
|
+
consecutiveFailures = 0;
|
|
87
|
+
}
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
90
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
91
|
+
controller.signal.addEventListener('abort', () => reject(new ShowcaseTimeoutError(timeoutMs)), { once: true });
|
|
92
|
+
});
|
|
93
|
+
try {
|
|
94
|
+
// Bounds the WHOLE call, not each request inside it — a callee that ignores the signal
|
|
95
|
+
// (the verifier applies its own per-fetch budget instead) is still cut off here.
|
|
96
|
+
const value = await Promise.race([fn(controller.signal), deadline]);
|
|
97
|
+
// A throwing `isFailure` is a bug in THIS package, so it is neither charged to the issuer
|
|
98
|
+
// (which would open the breaker against a service that answered fine) nor allowed to CLEAR
|
|
99
|
+
// the streak — a predicate that threw on exactly the dead-issuer shape would otherwise
|
|
100
|
+
// erase real failures and hold the breaker closed forever, which is the original defect
|
|
101
|
+
// wearing a new disguise. It is also logged: `verdict.ts` uses the same channel, and a
|
|
102
|
+
// silent swallow here would hide the one condition that makes the breaker inert.
|
|
103
|
+
let predicateThrew = false;
|
|
104
|
+
let predicateSaysFailure = false;
|
|
105
|
+
try {
|
|
106
|
+
predicateSaysFailure = runOptions.isFailure?.(value) === true;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
predicateThrew = true;
|
|
110
|
+
console.error(`delegation-self-refusal: showcase isFailure predicate threw (failure count left ` +
|
|
111
|
+
`untouched): ${error instanceof Error ? error.message : String(error)}`);
|
|
112
|
+
}
|
|
113
|
+
if (predicateSaysFailure) {
|
|
114
|
+
recordFailure();
|
|
115
|
+
return { ok: false, tripped: false, error: value };
|
|
116
|
+
}
|
|
117
|
+
if (!predicateThrew) {
|
|
118
|
+
consecutiveFailures = 0;
|
|
119
|
+
}
|
|
120
|
+
return { ok: true, value };
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
recordFailure();
|
|
124
|
+
return { ok: false, tripped: false, error };
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=breaker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"breaker.js","sourceRoot":"","sources":["../../src/showcase/breaker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Breaker } from './breaker.js';
|
|
2
|
+
import type { FetchLike } from './marked-fetch.js';
|
|
3
|
+
export interface ConnectInfo {
|
|
4
|
+
message: string;
|
|
5
|
+
/**
|
|
6
|
+
* The hosted server's own URL — the route that needs no install and no API key to start. Optional
|
|
7
|
+
* because an older issuer deployment answers without it; absent must stay absent rather than be
|
|
8
|
+
* filled in with a guess, since a fabricated URL would point the agent at a host that may serve
|
|
9
|
+
* nothing.
|
|
10
|
+
*/
|
|
11
|
+
remote_url?: string;
|
|
12
|
+
remote_config?: unknown;
|
|
13
|
+
install_command: string;
|
|
14
|
+
/** Why the npm route is the second choice while the published build lags the current surface. */
|
|
15
|
+
install_caveat?: string;
|
|
16
|
+
docs_url: string;
|
|
17
|
+
client_config?: unknown;
|
|
18
|
+
updated_at?: string;
|
|
19
|
+
source: 'live' | 'offline_fallback';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The text `proof_connect` falls back to when the issuer cannot be reached.
|
|
23
|
+
*
|
|
24
|
+
* It is deliberately marked `offline_fallback` in the payload rather than served silently: this
|
|
25
|
+
* copy is FROZEN into the publisher's `node_modules` at install time, and the live answer changes
|
|
26
|
+
* between the parts of the single-MCP plan (a remote server arrives in part 3). An agent reading
|
|
27
|
+
* a stale instruction with no signal that it is stale would confidently tell a user to do the
|
|
28
|
+
* wrong thing.
|
|
29
|
+
*/
|
|
30
|
+
export declare const OFFLINE_FALLBACK: ConnectInfo;
|
|
31
|
+
/**
|
|
32
|
+
* Fetches the current connection instructions at CALL time (SC-6), through the showcase breaker
|
|
33
|
+
* so a slow or dead issuer costs the publisher's user one bounded wait rather than one per call.
|
|
34
|
+
*
|
|
35
|
+
* Never throws and never caches: two calls in a row do exactly the same thing, which is what
|
|
36
|
+
* "harmless on repeat" means for a tool an agent may call speculatively.
|
|
37
|
+
*/
|
|
38
|
+
export declare function fetchConnectInfo(baseUrl: string, breaker: Breaker, fetchImpl: FetchLike): Promise<ConnectInfo>;
|
|
39
|
+
//# sourceMappingURL=connect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/showcase/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,iGAAiG;IACjG,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,kBAAkB,CAAC;CACrC;AAoBD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,WAc9B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,WAAW,CAAC,CAwDtB"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cap on the connect payload this will accept.
|
|
3
|
+
*
|
|
4
|
+
* The live payload measures ~800 bytes, so 64 KiB is ~80× headroom. The verifier's own
|
|
5
|
+
* `MAX_REMOTE_BODY_BYTES` is 512 KiB because it reads artifacts that can legitimately be large (a
|
|
6
|
+
* JWKS, a status list); nothing here can be, so the cap is tighter. A local constant rather than an
|
|
7
|
+
* import: it is not exported from the verifier's `index.ts`, and this package must not reach into
|
|
8
|
+
* another one's internals.
|
|
9
|
+
*
|
|
10
|
+
* TWO BOUNDS the name does not carry, both shared verbatim with the verifier's version of this
|
|
11
|
+
* check and recorded so `_BYTES` is not later read as a guarantee: `body.length` counts UTF-16 code
|
|
12
|
+
* units, so a 64 Ki-CHARACTER body can be ~192 KiB of UTF-8; and the second check runs after
|
|
13
|
+
* `response.text()` has already buffered, so it refuses the payload rather than preventing it being
|
|
14
|
+
* held. The declared-length check above it is the only pre-transfer refusal, and the real bound on
|
|
15
|
+
* what can be buffered is the breaker's 4.5s deadline.
|
|
16
|
+
*/
|
|
17
|
+
const MAX_CONNECT_BODY_BYTES = 64 * 1024;
|
|
18
|
+
/**
|
|
19
|
+
* The text `proof_connect` falls back to when the issuer cannot be reached.
|
|
20
|
+
*
|
|
21
|
+
* It is deliberately marked `offline_fallback` in the payload rather than served silently: this
|
|
22
|
+
* copy is FROZEN into the publisher's `node_modules` at install time, and the live answer changes
|
|
23
|
+
* between the parts of the single-MCP plan (a remote server arrives in part 3). An agent reading
|
|
24
|
+
* a stale instruction with no signal that it is stale would confidently tell a user to do the
|
|
25
|
+
* wrong thing.
|
|
26
|
+
*/
|
|
27
|
+
export const OFFLINE_FALLBACK = {
|
|
28
|
+
message: 'Proof (proof.holdings) verifies real-world control — domains, phone numbers, and human approval — and issues signed proof tokens an agent can check. The full Proof MCP server adds the rest of the platform to this client. The quickest way in is the hosted server at https://api.proof.holdings/mcp — nothing to install and no API key needed to start. This text was packaged with the Proof layer and may be out of date; proof.holdings/docs/mcp always has the current instructions.',
|
|
29
|
+
// The production address, deliberately: a copy frozen into node_modules cannot know which
|
|
30
|
+
// deployment it will be read beside, and this is the same posture `docs_url` already takes.
|
|
31
|
+
remote_url: 'https://api.proof.holdings/mcp',
|
|
32
|
+
remote_config: {
|
|
33
|
+
mcpServers: { proof: { type: 'http', url: 'https://api.proof.holdings/mcp' } },
|
|
34
|
+
},
|
|
35
|
+
install_command: 'npx -y @proof-holdings/mcp-server',
|
|
36
|
+
install_caveat: 'The @proof-holdings/mcp-server build currently on npm predates the delegation tools and the keyless public mode, so it exposes an older and smaller surface. Until it is republished, remote_url is the way to reach the current server.',
|
|
37
|
+
docs_url: 'https://proof.holdings/docs/mcp',
|
|
38
|
+
source: 'offline_fallback',
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Fetches the current connection instructions at CALL time (SC-6), through the showcase breaker
|
|
42
|
+
* so a slow or dead issuer costs the publisher's user one bounded wait rather than one per call.
|
|
43
|
+
*
|
|
44
|
+
* Never throws and never caches: two calls in a row do exactly the same thing, which is what
|
|
45
|
+
* "harmless on repeat" means for a tool an agent may call speculatively.
|
|
46
|
+
*/
|
|
47
|
+
export async function fetchConnectInfo(baseUrl, breaker, fetchImpl) {
|
|
48
|
+
const origin = baseUrl.replace(/\/+$/, '');
|
|
49
|
+
const outcome = await breaker.run(async (signal) => {
|
|
50
|
+
const response = await fetchImpl(`${origin}/api/v1/mcp/connect`, {
|
|
51
|
+
method: 'GET',
|
|
52
|
+
signal,
|
|
53
|
+
// The same posture `packages/delegation-verifier/src/status.ts` takes on its remote reads,
|
|
54
|
+
// adopted here rather than left as an unexplained difference — and the reason is sharper on
|
|
55
|
+
// this path: what comes back becomes INSTRUCTIONS in the agent's context. `baseUrl` is the
|
|
56
|
+
// publisher's to configure, so an open redirect on it would carry this read off the origin
|
|
57
|
+
// they chose, with the answer still presented as ours.
|
|
58
|
+
redirect: 'error',
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(`connect info responded ${response.status}`);
|
|
62
|
+
}
|
|
63
|
+
// Declared length first (cheap, refuses before the transfer), then the real one, because
|
|
64
|
+
// `content-length` is absent under chunked encoding and is a claim either way.
|
|
65
|
+
const declaredLength = Number(response.headers.get('content-length'));
|
|
66
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_CONNECT_BODY_BYTES) {
|
|
67
|
+
throw new Error('connect info response is too large');
|
|
68
|
+
}
|
|
69
|
+
const body = await response.text();
|
|
70
|
+
if (body.length > MAX_CONNECT_BODY_BYTES) {
|
|
71
|
+
throw new Error('connect info response is too large');
|
|
72
|
+
}
|
|
73
|
+
return JSON.parse(body);
|
|
74
|
+
});
|
|
75
|
+
if (!outcome.ok) {
|
|
76
|
+
return OFFLINE_FALLBACK;
|
|
77
|
+
}
|
|
78
|
+
const body = outcome.value;
|
|
79
|
+
if (typeof body?.message !== 'string' || typeof body?.install_command !== 'string') {
|
|
80
|
+
return OFFLINE_FALLBACK;
|
|
81
|
+
}
|
|
82
|
+
// Field-by-field rather than a spread: what comes back becomes instructions in the agent's
|
|
83
|
+
// context, so the shape an issuer can put there is the one this package chose to carry. The cost
|
|
84
|
+
// is that a new issuer-side field is inert until it is added here — which is why the remote
|
|
85
|
+
// route below had to be added explicitly rather than arriving on its own.
|
|
86
|
+
return {
|
|
87
|
+
message: body.message,
|
|
88
|
+
...(typeof body.remote_url === 'string' ? { remote_url: body.remote_url } : {}),
|
|
89
|
+
...(body.remote_config !== undefined ? { remote_config: body.remote_config } : {}),
|
|
90
|
+
install_command: body.install_command,
|
|
91
|
+
...(typeof body.install_caveat === 'string' ? { install_caveat: body.install_caveat } : {}),
|
|
92
|
+
docs_url: typeof body.docs_url === 'string' ? body.docs_url : OFFLINE_FALLBACK.docs_url,
|
|
93
|
+
...(body.client_config !== undefined ? { client_config: body.client_config } : {}),
|
|
94
|
+
...(typeof body.updated_at === 'string' ? { updated_at: body.updated_at } : {}),
|
|
95
|
+
source: 'live',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=connect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.js","sourceRoot":"","sources":["../../src/showcase/connect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The showcase tool descriptions, in their own module with ZERO external imports.
|
|
3
|
+
*
|
|
4
|
+
* Separated from `tools.ts` (which pulls in `zod` and the verifier) so the drift suite in the
|
|
5
|
+
* issuer's repository can import and assert the REAL strings rather than text-scanning for them.
|
|
6
|
+
* A description is what an agent reads instead of the schema, so it is the part most worth a guard
|
|
7
|
+
* and the part a text scan checks least honestly.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The cross-reference every showcase description carries (SC-5).
|
|
11
|
+
*
|
|
12
|
+
* Three tools with different names do not, on their own, tell anyone that a fuller server exists —
|
|
13
|
+
* they just look like three unrelated tools, which HIDES the duplication rather than resolving it.
|
|
14
|
+
* Naming the delta is what turns the showcase from a second surface into a doorway.
|
|
15
|
+
*/
|
|
16
|
+
export declare function showcaseFooter(principal: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* The short form of the consequence measured in `docs/agent-instruction-compliance.md` (Finding
|
|
19
|
+
* 2): naming what goes wrong for the reader, not explaining the mechanism, is what moved
|
|
20
|
+
* compliance (`t3` at 9/10 against 3/10 for each softer wording that only explained or instructed).
|
|
21
|
+
*
|
|
22
|
+
* That result was measured on the description of `send_email` — a tool the agent was ALREADY
|
|
23
|
+
* calling for its own task. These three tools are not: an agent busy with an unrelated task has no
|
|
24
|
+
* reason to call any of them, so this exact configuration — the phrase on a tool the agent's task
|
|
25
|
+
* never touches — has never been measured. The cost of carrying it anyway is low (our own words in
|
|
26
|
+
* our own tools, no publisher consent needed, no third party harmed), which is why it ships. But
|
|
27
|
+
* that cost argument is not a measurement, and this is a hypothesis, not a confirmed result.
|
|
28
|
+
*/
|
|
29
|
+
export declare function showcaseConsequence(principal: string): string;
|
|
30
|
+
export declare function describeCheckThisServer(principal: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* The identity paragraph states the same three rules as the full server's `verify_delegation`
|
|
33
|
+
* description (`mcp/src/tools/delegation-verify.ts`), and the issuer's
|
|
34
|
+
* `src/__tests__/drift/mcp-showcase.test.ts` holds THOSE THREE STATEMENTS across both — not the
|
|
35
|
+
* paragraph. The wordings are deliberately not identical (this one keeps the positive example
|
|
36
|
+
* inline), so do not read the guard as pinning equality: what it forbids is either side losing a
|
|
37
|
+
* rule, not either side rephrasing.
|
|
38
|
+
*
|
|
39
|
+
* It is here in full rather than summarised because this is the description an agent reads when it
|
|
40
|
+
* meets Proof inside a publisher's server, and the shorter version it used to carry is what let a
|
|
41
|
+
* live agent take the artifact name out of the checked artifact's own `package.json` and accuse a
|
|
42
|
+
* real publisher of acting without authority.
|
|
43
|
+
*/
|
|
44
|
+
export declare function describeVerifyDelegation(principal: string): string;
|
|
45
|
+
export declare function describeConnect(principal: string): string;
|
|
46
|
+
//# sourceMappingURL=descriptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"descriptions.d.ts","sourceRoot":"","sources":["../../src/showcase/descriptions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAOxD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAK7D;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CASjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAqBlE;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQzD"}
|