@ggui-ai/mcp-server 0.1.0-rc.1
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 +201 -0
- package/README.md +48 -0
- package/dist/admin-blueprints-transport.d.ts +114 -0
- package/dist/admin-blueprints-transport.d.ts.map +1 -0
- package/dist/admin-blueprints-transport.js +118 -0
- package/dist/admin-oauth-providers-transport.d.ts +40 -0
- package/dist/admin-oauth-providers-transport.d.ts.map +1 -0
- package/dist/admin-oauth-providers-transport.js +263 -0
- package/dist/auth.d.ts +39 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +75 -0
- package/dist/build-mcp.d.ts +128 -0
- package/dist/build-mcp.d.ts.map +1 -0
- package/dist/build-mcp.js +113 -0
- package/dist/code-store-fs.d.ts +19 -0
- package/dist/code-store-fs.d.ts.map +1 -0
- package/dist/code-store-fs.js +98 -0
- package/dist/console-auth.d.ts +139 -0
- package/dist/console-auth.d.ts.map +1 -0
- package/dist/console-auth.js +102 -0
- package/dist/console-cache.d.ts +78 -0
- package/dist/console-cache.d.ts.map +1 -0
- package/dist/console-cache.js +105 -0
- package/dist/console-headers.d.ts +124 -0
- package/dist/console-headers.d.ts.map +1 -0
- package/dist/console-headers.js +49 -0
- package/dist/console-llm-trace.d.ts +66 -0
- package/dist/console-llm-trace.d.ts.map +1 -0
- package/dist/console-llm-trace.js +105 -0
- package/dist/console-payloads.d.ts +67 -0
- package/dist/console-payloads.d.ts.map +1 -0
- package/dist/console-payloads.js +105 -0
- package/dist/console-theme-routes.d.ts +111 -0
- package/dist/console-theme-routes.d.ts.map +1 -0
- package/dist/console-theme-routes.js +202 -0
- package/dist/console-timeline.d.ts +45 -0
- package/dist/console-timeline.d.ts.map +1 -0
- package/dist/console-timeline.js +169 -0
- package/dist/console-validator.d.ts +67 -0
- package/dist/console-validator.d.ts.map +1 -0
- package/dist/console-validator.js +105 -0
- package/dist/console-welcome.d.ts +7 -0
- package/dist/console-welcome.d.ts.map +1 -0
- package/dist/console-welcome.js +221 -0
- package/dist/csrf-middleware.d.ts +55 -0
- package/dist/csrf-middleware.d.ts.map +1 -0
- package/dist/csrf-middleware.js +138 -0
- package/dist/email-login.d.ts +174 -0
- package/dist/email-login.d.ts.map +1 -0
- package/dist/email-login.js +254 -0
- package/dist/email-resend.d.ts +29 -0
- package/dist/email-resend.d.ts.map +1 -0
- package/dist/email-resend.js +71 -0
- package/dist/email-sender-from-env.d.ts +34 -0
- package/dist/email-sender-from-env.d.ts.map +1 -0
- package/dist/email-sender-from-env.js +112 -0
- package/dist/email-smtp.d.ts +42 -0
- package/dist/email-smtp.d.ts.map +1 -0
- package/dist/email-smtp.js +81 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +122 -0
- package/dist/instructions-presets.d.ts +112 -0
- package/dist/instructions-presets.d.ts.map +1 -0
- package/dist/instructions-presets.js +195 -0
- package/dist/llm-backed-negotiator.d.ts +178 -0
- package/dist/llm-backed-negotiator.d.ts.map +1 -0
- package/dist/llm-backed-negotiator.js +579 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +41 -0
- package/dist/mcp-apps-inbound.d.ts +86 -0
- package/dist/mcp-apps-inbound.d.ts.map +1 -0
- package/dist/mcp-apps-inbound.js +278 -0
- package/dist/mcp-apps-outbound.d.ts +448 -0
- package/dist/mcp-apps-outbound.d.ts.map +1 -0
- package/dist/mcp-apps-outbound.js +1163 -0
- package/dist/mcp-mounts.d.ts +239 -0
- package/dist/mcp-mounts.d.ts.map +1 -0
- package/dist/mcp-mounts.js +222 -0
- package/dist/oauth-login-types.d.ts +160 -0
- package/dist/oauth-login-types.d.ts.map +1 -0
- package/dist/oauth-login-types.js +9 -0
- package/dist/oauth-login.d.ts +77 -0
- package/dist/oauth-login.d.ts.map +1 -0
- package/dist/oauth-login.js +455 -0
- package/dist/oauth-providers/github.d.ts +17 -0
- package/dist/oauth-providers/github.d.ts.map +1 -0
- package/dist/oauth-providers/github.js +89 -0
- package/dist/oauth-providers/google.d.ts +18 -0
- package/dist/oauth-providers/google.d.ts.map +1 -0
- package/dist/oauth-providers/google.js +59 -0
- package/dist/oauth-providers-store.d.ts +32 -0
- package/dist/oauth-providers-store.d.ts.map +1 -0
- package/dist/oauth-providers-store.js +291 -0
- package/dist/oauth.d.ts +347 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +686 -0
- package/dist/pairing-transport.d.ts +99 -0
- package/dist/pairing-transport.d.ts.map +1 -0
- package/dist/pairing-transport.js +223 -0
- package/dist/rate-limit-middleware.d.ts +36 -0
- package/dist/rate-limit-middleware.d.ts.map +1 -0
- package/dist/rate-limit-middleware.js +57 -0
- package/dist/render-gate.d.ts +87 -0
- package/dist/render-gate.d.ts.map +1 -0
- package/dist/render-gate.js +77 -0
- package/dist/render-rate-limit.d.ts +59 -0
- package/dist/render-rate-limit.d.ts.map +1 -0
- package/dist/render-rate-limit.js +73 -0
- package/dist/render-signing.d.ts +98 -0
- package/dist/render-signing.d.ts.map +1 -0
- package/dist/render-signing.js +113 -0
- package/dist/request-context.d.ts +113 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +154 -0
- package/dist/reserved-validators.d.ts +22 -0
- package/dist/reserved-validators.d.ts.map +1 -0
- package/dist/reserved-validators.js +101 -0
- package/dist/schema-compat.d.ts +167 -0
- package/dist/schema-compat.d.ts.map +1 -0
- package/dist/schema-compat.js +187 -0
- package/dist/security-headers-middleware.d.ts +38 -0
- package/dist/security-headers-middleware.d.ts.map +1 -0
- package/dist/security-headers-middleware.js +30 -0
- package/dist/server.d.ts +2060 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +6338 -0
- package/dist/session-channel.d.ts +651 -0
- package/dist/session-channel.d.ts.map +1 -0
- package/dist/session-channel.js +1756 -0
- package/dist/storage.d.ts +89 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +171 -0
- package/dist/thread-transport.d.ts +118 -0
- package/dist/thread-transport.d.ts.map +1 -0
- package/dist/thread-transport.js +478 -0
- package/dist/user-session-auth.d.ts +167 -0
- package/dist/user-session-auth.d.ts.map +1 -0
- package/dist/user-session-auth.js +148 -0
- package/package.json +76 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reserved-channel payload validator composition for `@ggui-ai/mcp-server`.
|
|
3
|
+
*
|
|
4
|
+
* `@ggui-ai/protocol` ships zero knowledge of A2UI (see Protocol #6,
|
|
5
|
+
* vendor-neutral separation). The preview channel's payload shape is
|
|
6
|
+
* authored in `@ggui-ai/preview-a2ui`; this module adapts that package's
|
|
7
|
+
* `parseServerMessage` into the
|
|
8
|
+
* {@link ReservedChannelValidator} shape `validateStreamData` expects
|
|
9
|
+
* via its `extraReservedValidators` injection point.
|
|
10
|
+
*
|
|
11
|
+
* Composition surface:
|
|
12
|
+
*
|
|
13
|
+
* - {@link composePreviewReservedValidator} — returns a single-entry
|
|
14
|
+
* map binding `_ggui:preview` to the A2UI adapter. Call at server
|
|
15
|
+
* construction and feed into `extraReservedValidators` on
|
|
16
|
+
* `SessionChannelOptions` / `CreateGguiServerOptions`.
|
|
17
|
+
* - {@link mergeReservedValidators} — combine multiple validator maps
|
|
18
|
+
* when a caller provides their own extras AND the server wants to
|
|
19
|
+
* layer A2UI on top. Caller-provided entries win on key conflict.
|
|
20
|
+
*
|
|
21
|
+
* Design note: kept deliberately narrow. No server-opinionated default
|
|
22
|
+
* catalog filtering, no surface-id checking — those live in the A2UI
|
|
23
|
+
* runtime consumer. The adapter enforces only what `parseServerMessage`
|
|
24
|
+
* enforces: message-shape conformance to the V1 write-path union.
|
|
25
|
+
*/
|
|
26
|
+
import { parseServerMessage, } from '@ggui-ai/preview-a2ui';
|
|
27
|
+
import { PREVIEW_CHANNEL, } from '@ggui-ai/protocol';
|
|
28
|
+
/**
|
|
29
|
+
* Adapter mapping `parseServerMessage` output to the
|
|
30
|
+
* `ValidationResult` shape `validateStreamData` returns. Accepts any
|
|
31
|
+
* `_ggui:preview` payload and surfaces the A2UI parse issues under the
|
|
32
|
+
* `payload` field path when rejection fires.
|
|
33
|
+
*
|
|
34
|
+
* Channel-close sentinel: `null` / `undefined` payloads on
|
|
35
|
+
* `_ggui:preview` are the live-channel terminal envelope emitted by the
|
|
36
|
+
* preview runner's `finalizePreviewChannel` (alongside
|
|
37
|
+
* `complete: true`). This is a transport-level teardown marker, NOT an
|
|
38
|
+
* A2UI message, so the A2UI adapter accepts it verbatim. Any
|
|
39
|
+
* non-null, non-undefined payload runs through the full A2UI validator.
|
|
40
|
+
*/
|
|
41
|
+
function a2uiPreviewValidator(payload) {
|
|
42
|
+
// Live-channel teardown sentinel — see JSDoc above.
|
|
43
|
+
if (payload === null || payload === undefined) {
|
|
44
|
+
return { valid: true, violations: [] };
|
|
45
|
+
}
|
|
46
|
+
const parsed = parseServerMessage(payload);
|
|
47
|
+
if (parsed.ok)
|
|
48
|
+
return { valid: true, violations: [] };
|
|
49
|
+
const violations = parsed.issues.map((issue) => ({
|
|
50
|
+
field: issue.path.length === 0 ? 'payload' : `payload.${issue.path.join('.')}`,
|
|
51
|
+
message: issue.message,
|
|
52
|
+
expected: 'A2UI ServerMessage (v0.9 write-path union)',
|
|
53
|
+
received: 'malformed',
|
|
54
|
+
}));
|
|
55
|
+
// If Zod produced no issues (defensive — safe-parse always surfaces
|
|
56
|
+
// at least one on failure), synthesize a single catch-all violation
|
|
57
|
+
// so callers still see a rejection reason.
|
|
58
|
+
if (violations.length === 0) {
|
|
59
|
+
violations.push({
|
|
60
|
+
field: 'payload',
|
|
61
|
+
message: 'A2UI preview payload did not match the V1 write-path union',
|
|
62
|
+
expected: 'A2UI ServerMessage (v0.9)',
|
|
63
|
+
received: 'malformed',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return { valid: false, violations };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Returns a reserved-validator map binding `_ggui:preview` to the A2UI
|
|
70
|
+
* adapter. Single-entry by design — each reserved channel gets its own
|
|
71
|
+
* validator; consumers that want to compose more should call
|
|
72
|
+
* {@link mergeReservedValidators}.
|
|
73
|
+
*
|
|
74
|
+
* No parameters today. When the A2UI V1 subset widens (e.g.
|
|
75
|
+
* `updateDataModel`), the adapter follows `parseServerMessage` without
|
|
76
|
+
* touching this export.
|
|
77
|
+
*/
|
|
78
|
+
export function composePreviewReservedValidator() {
|
|
79
|
+
return new Map([[PREVIEW_CHANNEL, a2uiPreviewValidator]]);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Merge two reserved-validator maps into one. Keys present in
|
|
83
|
+
* `override` WIN on conflict — the pattern is "server supplies
|
|
84
|
+
* defaults (A2UI), caller may replace by key".
|
|
85
|
+
*
|
|
86
|
+
* Returns a `ReadonlyMap` so the composed result has the same
|
|
87
|
+
* immutability guarantee as the individual inputs.
|
|
88
|
+
*/
|
|
89
|
+
export function mergeReservedValidators(base, override) {
|
|
90
|
+
if (!base && !override)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (!base)
|
|
93
|
+
return override;
|
|
94
|
+
if (!override)
|
|
95
|
+
return base;
|
|
96
|
+
const merged = new Map(base);
|
|
97
|
+
for (const [key, validator] of override) {
|
|
98
|
+
merged.set(key, validator);
|
|
99
|
+
}
|
|
100
|
+
return merged;
|
|
101
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema compatibility check — verifies that a StackItem's declared
|
|
3
|
+
* `actionSpec` / `streamSpec` schemas line up with the input/output
|
|
4
|
+
* schemas of the tools they reference. Wired at two canonical check
|
|
5
|
+
* points: `ggui_push` validation (defensive, fires when the
|
|
6
|
+
* generator eventually emits contract) and blueprint registration
|
|
7
|
+
* (the console blueprint-try endpoint — the real-world site where a
|
|
8
|
+
* StackItem with pre-declared `actionSpec` / `streamSpec` and tool
|
|
9
|
+
* refs lands on a session).
|
|
10
|
+
*
|
|
11
|
+
* **Algorithmic primitive** lives in `@ggui-ai/protocol`:
|
|
12
|
+
*
|
|
13
|
+
* - `isSchemaSubset(superset, subset)` answers "can every value
|
|
14
|
+
* subset accepts also pass superset?".
|
|
15
|
+
* - `zodToJsonSchema(...)` converts a tool's ZodRawShape
|
|
16
|
+
* `inputSchema` / `outputSchema` to the JsonSchema shape the
|
|
17
|
+
* subset algorithm consumes.
|
|
18
|
+
*
|
|
19
|
+
* **What this module adds.** A small layer that walks a StackItem's
|
|
20
|
+
* `actionSpec` + `streamSpec`, resolves each declared `tool` ref
|
|
21
|
+
* against a toolName → ZodRawShape registry, runs the appropriate
|
|
22
|
+
* subset check, and reports a stable {@link SchemaCompatReport}. The
|
|
23
|
+
* caller (console endpoint, push handler — later) decides whether
|
|
24
|
+
* a non-empty report should throw, warn, or be ignored per the
|
|
25
|
+
* {@link SchemaCompatMode} policy flag.
|
|
26
|
+
*
|
|
27
|
+
* **Direction semantics (load-bearing).**
|
|
28
|
+
*
|
|
29
|
+
* - For `actionSpec[name].schema` + `tool`: the action's payload
|
|
30
|
+
* is sent INTO the tool, so the action schema must be a subset
|
|
31
|
+
* of the tool's `inputSchema`. Compat relation:
|
|
32
|
+
* `isSchemaSubset(toolInputSchema, actionSchema)`.
|
|
33
|
+
* - For `streamSpec[channel].schema` + `tool`: the tool's return
|
|
34
|
+
* value is emitted OUT on the channel. Every value the tool
|
|
35
|
+
* returns MUST be accepted by the channel schema (otherwise a
|
|
36
|
+
* tool-fired refresh would emit a payload that subscribers
|
|
37
|
+
* reject). The channel schema is therefore the PERMISSIVE side
|
|
38
|
+
* and the tool's return is the restricted side. Compat relation:
|
|
39
|
+
* `isSchemaSubset(channelSchema, toolReturnSchema)` — i.e.
|
|
40
|
+
* "every toolReturn-accepted value is also channelSchema-
|
|
41
|
+
* accepted".
|
|
42
|
+
*
|
|
43
|
+
* Note: the `StreamChannelEntry.schema` docstring phrases this
|
|
44
|
+
* as "the tool's returns MUST be a superset of channel schema".
|
|
45
|
+
* That historical phrasing is author-facing and slightly
|
|
46
|
+
* misleading if read literally as set-theoretic superset of
|
|
47
|
+
* accepted-values; the semantic intent is the one encoded here
|
|
48
|
+
* — EVERY tool return passes channel validation.
|
|
49
|
+
*/
|
|
50
|
+
import type { ZodRawShape } from 'zod';
|
|
51
|
+
import { type SubsetViolation, type ActionSpec, type StreamSpec } from '@ggui-ai/protocol';
|
|
52
|
+
/**
|
|
53
|
+
* Stance the host takes when a check surfaces violations.
|
|
54
|
+
*
|
|
55
|
+
* - `'reject'` (default) — throw {@link SchemaCompatError} so the
|
|
56
|
+
* containing request fails before the stack item commits /
|
|
57
|
+
* before the blueprint registers. The canonical enforcement
|
|
58
|
+
* posture for launch.
|
|
59
|
+
* - `'warn'` — return the report without throwing. The caller is
|
|
60
|
+
* expected to log the violations on an observable surface
|
|
61
|
+
* (logger, telemetry). Used during migration windows when an
|
|
62
|
+
* operator wants to surface mismatches without breaking
|
|
63
|
+
* existing flows.
|
|
64
|
+
* - `'off'` — skip the check entirely. Test convenience;
|
|
65
|
+
* explicit opt-out.
|
|
66
|
+
*/
|
|
67
|
+
export type SchemaCompatMode = 'reject' | 'warn' | 'off';
|
|
68
|
+
/**
|
|
69
|
+
* Default mode applied when the host does not override. Matches the
|
|
70
|
+
* Item 5 brief default.
|
|
71
|
+
*/
|
|
72
|
+
export declare const DEFAULT_SCHEMA_COMPAT_MODE: SchemaCompatMode;
|
|
73
|
+
/**
|
|
74
|
+
* Per-action or per-channel compat finding. Preserves the underlying
|
|
75
|
+
* subset violations so downstream messaging can surface any level of
|
|
76
|
+
* detail the operator wants.
|
|
77
|
+
*/
|
|
78
|
+
export interface SchemaCompatFinding {
|
|
79
|
+
/**
|
|
80
|
+
* Which spec side produced this finding. `'action'` for an
|
|
81
|
+
* `actionSpec[name]` tool ref; `'stream'` for a
|
|
82
|
+
* `streamSpec[channel].tool` ref. Consumers can branch on this
|
|
83
|
+
* when rendering the cause.
|
|
84
|
+
*/
|
|
85
|
+
readonly kind: 'action' | 'stream';
|
|
86
|
+
/** Action name or channel name. */
|
|
87
|
+
readonly specName: string;
|
|
88
|
+
/** Tool name the spec referenced. */
|
|
89
|
+
readonly toolName: string;
|
|
90
|
+
/** One of:
|
|
91
|
+
* - `'tool-not-found'` — the ref's tool is not registered on
|
|
92
|
+
* the composed handler set. (Author bug — matching the
|
|
93
|
+
* existing `TOOL_NOT_FOUND` contract semantics, but surfaced
|
|
94
|
+
* before any envelope reaches the agentic loop.)
|
|
95
|
+
* - `'schema-mismatch'` — the subset check failed with at least
|
|
96
|
+
* one violation. See {@link SchemaCompatFinding.violations}.
|
|
97
|
+
*/
|
|
98
|
+
readonly reason: 'tool-not-found' | 'schema-mismatch';
|
|
99
|
+
/** Subset violations carried through for rich error rendering.
|
|
100
|
+
* Empty array when `reason: 'tool-not-found'`. */
|
|
101
|
+
readonly violations: readonly SubsetViolation[];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Aggregate compat report. A `compatible: true` report is the
|
|
105
|
+
* happy path — the caller commits its side effect. A
|
|
106
|
+
* `compatible: false` report enumerates every failing spec entry.
|
|
107
|
+
*/
|
|
108
|
+
export interface SchemaCompatReport {
|
|
109
|
+
readonly compatible: boolean;
|
|
110
|
+
readonly findings: readonly SchemaCompatFinding[];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Narrow shape of what the check helper needs from each registered
|
|
114
|
+
* tool. Satisfied by `SharedHandler<ZodRawShape, ZodRawShape>` — the
|
|
115
|
+
* two schema fields are the only consumed inputs.
|
|
116
|
+
*/
|
|
117
|
+
export interface ToolSchemaRef {
|
|
118
|
+
readonly name: string;
|
|
119
|
+
readonly inputSchema: ZodRawShape;
|
|
120
|
+
readonly outputSchema: ZodRawShape;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Narrow shape of the StackItem subset the checker consumes — the
|
|
124
|
+
* two spec fields PLUS the contract's own tool catalog. Accepts any
|
|
125
|
+
* object carrying them, so both `@ggui-ai/protocol::StackItem` and
|
|
126
|
+
* the console endpoint's manifest contract shape work without a cast.
|
|
127
|
+
*
|
|
128
|
+
* `agentCapabilities.tools` is the contract author's declared catalog
|
|
129
|
+
* of every tool the contract references. Tools listed here but NOT
|
|
130
|
+
* registered on the composing server are CROSS-MCP references — the
|
|
131
|
+
* agent intends to call them on a different MCP server in its
|
|
132
|
+
* toolbox. The compat checker accepts these without a server-side
|
|
133
|
+
* schema check (we can't validate a remote server's tool schema from
|
|
134
|
+
* here; the agent owns the cross-MCP call).
|
|
135
|
+
*/
|
|
136
|
+
export interface StackItemContractShape {
|
|
137
|
+
readonly actionSpec?: ActionSpec;
|
|
138
|
+
readonly streamSpec?: StreamSpec;
|
|
139
|
+
readonly agentCapabilities?: {
|
|
140
|
+
readonly tools?: Readonly<Record<string, unknown>>;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Thrown when `schemaCompatCheck: 'reject'` surfaces at least one
|
|
145
|
+
* finding. Carries the full {@link SchemaCompatReport} so callers
|
|
146
|
+
* can log / surface the detail beyond the `message` string.
|
|
147
|
+
*/
|
|
148
|
+
export declare class SchemaCompatError extends Error {
|
|
149
|
+
readonly report: SchemaCompatReport;
|
|
150
|
+
constructor(report: SchemaCompatReport, context: string);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Check a StackItem's `actionSpec` / `streamSpec` entries against
|
|
154
|
+
* the tool registry. See {@link SchemaCompatMode} for policy
|
|
155
|
+
* semantics.
|
|
156
|
+
*
|
|
157
|
+
* Returns a {@link SchemaCompatReport}. When `mode === 'reject'`
|
|
158
|
+
* AND findings exist, throws {@link SchemaCompatError} instead —
|
|
159
|
+
* the report is still available on `error.report`.
|
|
160
|
+
*
|
|
161
|
+
* `context` is a short string naming the call site (e.g.
|
|
162
|
+
* `"ggui_push"`, `"console blueprint-try:<blueprintId>"`). Shows
|
|
163
|
+
* up in thrown error messages so an operator reading logs sees
|
|
164
|
+
* which ingress surfaced the mismatch.
|
|
165
|
+
*/
|
|
166
|
+
export declare function checkStackItemSchemaCompat(stackItem: StackItemContractShape, tools: Iterable<ToolSchemaRef>, mode: SchemaCompatMode, context: string): SchemaCompatReport;
|
|
167
|
+
//# sourceMappingURL=schema-compat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-compat.d.ts","sourceRoot":"","sources":["../src/schema-compat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,UAAU,EAChB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;AAEzD;;;GAGG;AACH,eAAO,MAAM,0BAA0B,EAAE,gBAA2B,CAAC;AAErE;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACnC,mCAAmC;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACtD;uDACmD;IACnD,QAAQ,CAAC,UAAU,EAAE,SAAS,eAAe,EAAE,CAAC;CACjD;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,mBAAmB,EAAE,CAAC;CACnD;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC;CACpC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC,QAAQ,CAAC,iBAAiB,CAAC,EAAE;QAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;KACpD,CAAC;CACH;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;gBACxB,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM;CAKxD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,sBAAsB,EACjC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,EAC9B,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,MAAM,GACd,kBAAkB,CA0HpB"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { isSchemaSubset, zodToJsonSchema, } from '@ggui-ai/protocol';
|
|
2
|
+
/**
|
|
3
|
+
* Default mode applied when the host does not override. Matches the
|
|
4
|
+
* Item 5 brief default.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_SCHEMA_COMPAT_MODE = 'reject';
|
|
7
|
+
/**
|
|
8
|
+
* Thrown when `schemaCompatCheck: 'reject'` surfaces at least one
|
|
9
|
+
* finding. Carries the full {@link SchemaCompatReport} so callers
|
|
10
|
+
* can log / surface the detail beyond the `message` string.
|
|
11
|
+
*/
|
|
12
|
+
export class SchemaCompatError extends Error {
|
|
13
|
+
report;
|
|
14
|
+
constructor(report, context) {
|
|
15
|
+
super(formatReport(report, context));
|
|
16
|
+
this.name = 'SchemaCompatError';
|
|
17
|
+
this.report = report;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Check a StackItem's `actionSpec` / `streamSpec` entries against
|
|
22
|
+
* the tool registry. See {@link SchemaCompatMode} for policy
|
|
23
|
+
* semantics.
|
|
24
|
+
*
|
|
25
|
+
* Returns a {@link SchemaCompatReport}. When `mode === 'reject'`
|
|
26
|
+
* AND findings exist, throws {@link SchemaCompatError} instead —
|
|
27
|
+
* the report is still available on `error.report`.
|
|
28
|
+
*
|
|
29
|
+
* `context` is a short string naming the call site (e.g.
|
|
30
|
+
* `"ggui_push"`, `"console blueprint-try:<blueprintId>"`). Shows
|
|
31
|
+
* up in thrown error messages so an operator reading logs sees
|
|
32
|
+
* which ingress surfaced the mismatch.
|
|
33
|
+
*/
|
|
34
|
+
export function checkStackItemSchemaCompat(stackItem, tools, mode, context) {
|
|
35
|
+
if (mode === 'off') {
|
|
36
|
+
return { compatible: true, findings: [] };
|
|
37
|
+
}
|
|
38
|
+
// Build a one-pass name → tool map. Callers pass the composed
|
|
39
|
+
// handler list; the map is cheap to rebuild per-check because
|
|
40
|
+
// the input tool list is typically <50 entries.
|
|
41
|
+
const byName = new Map();
|
|
42
|
+
for (const t of tools)
|
|
43
|
+
byName.set(t.name, t);
|
|
44
|
+
// Cross-MCP escape hatch: tools the contract author declared in
|
|
45
|
+
// `agentCapabilities.tools` are accepted even when they don't
|
|
46
|
+
// exist in the server's registry — the agent intends to call them
|
|
47
|
+
// on a different MCP server in its toolbox. We can't validate
|
|
48
|
+
// their inputSchema/outputSchema from here (no access to the
|
|
49
|
+
// remote tool definition); cross-MCP shape compatibility is the
|
|
50
|
+
// agent's responsibility. Same-server tools still get the full
|
|
51
|
+
// server-driven subset check below.
|
|
52
|
+
const contractDeclaredTools = new Set(Object.keys(stackItem.agentCapabilities?.tools ?? {}));
|
|
53
|
+
const findings = [];
|
|
54
|
+
// actionSpec — each action.tool's inputSchema must be a superset
|
|
55
|
+
// of action.schema. A void action (no schema) paired with a tool
|
|
56
|
+
// that requires input is flagged specifically: the wire sends
|
|
57
|
+
// nothing, the tool demands something, so the compat relation is
|
|
58
|
+
// "empty-object ⊆ tool-input" — which fails whenever the tool has
|
|
59
|
+
// required fields.
|
|
60
|
+
const actionSpec = stackItem.actionSpec ?? {};
|
|
61
|
+
for (const [actionName, entry] of Object.entries(actionSpec)) {
|
|
62
|
+
if (!entry || typeof entry !== 'object')
|
|
63
|
+
continue;
|
|
64
|
+
const toolName = entry.nextStep;
|
|
65
|
+
if (typeof toolName !== 'string' || toolName.length === 0)
|
|
66
|
+
continue;
|
|
67
|
+
const tool = byName.get(toolName);
|
|
68
|
+
if (!tool) {
|
|
69
|
+
// Tool not in server registry. Check the cross-MCP escape
|
|
70
|
+
// hatch — if the contract declared it in agentCapabilities.tools,
|
|
71
|
+
// skip the server-side check entirely (agent owns cross-MCP
|
|
72
|
+
// validation).
|
|
73
|
+
if (contractDeclaredTools.has(toolName))
|
|
74
|
+
continue;
|
|
75
|
+
findings.push({
|
|
76
|
+
kind: 'action',
|
|
77
|
+
specName: actionName,
|
|
78
|
+
toolName,
|
|
79
|
+
reason: 'tool-not-found',
|
|
80
|
+
violations: [],
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const toolInput = zodToJsonSchema(tool.inputSchema);
|
|
85
|
+
// Void action (no schema) — model the wire shape as the empty
|
|
86
|
+
// object schema. The subset check reports required-field
|
|
87
|
+
// violations only — an unconstrained tool with no required
|
|
88
|
+
// fields stays compatible.
|
|
89
|
+
const actionSchema = entry.schema ?? {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {},
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
};
|
|
94
|
+
const result = isSchemaSubset(toolInput, actionSchema);
|
|
95
|
+
if (!result.compatible) {
|
|
96
|
+
findings.push({
|
|
97
|
+
kind: 'action',
|
|
98
|
+
specName: actionName,
|
|
99
|
+
toolName,
|
|
100
|
+
reason: 'schema-mismatch',
|
|
101
|
+
violations: result.violations,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// streamSpec — each channel.tool's outputSchema must be a superset
|
|
106
|
+
// of channel.schema (inverted direction from actions).
|
|
107
|
+
const streamSpec = stackItem.streamSpec ?? {};
|
|
108
|
+
for (const [channelName, entry] of Object.entries(streamSpec)) {
|
|
109
|
+
if (!entry || typeof entry !== 'object')
|
|
110
|
+
continue;
|
|
111
|
+
const toolName = entry.tool;
|
|
112
|
+
if (typeof toolName !== 'string' || toolName.length === 0)
|
|
113
|
+
continue;
|
|
114
|
+
const channelSchema = entry.schema;
|
|
115
|
+
if (!channelSchema)
|
|
116
|
+
continue;
|
|
117
|
+
const tool = byName.get(toolName);
|
|
118
|
+
if (!tool) {
|
|
119
|
+
// Cross-MCP escape hatch — symmetric with the actionSpec arm.
|
|
120
|
+
if (contractDeclaredTools.has(toolName))
|
|
121
|
+
continue;
|
|
122
|
+
findings.push({
|
|
123
|
+
kind: 'stream',
|
|
124
|
+
specName: channelName,
|
|
125
|
+
toolName,
|
|
126
|
+
reason: 'tool-not-found',
|
|
127
|
+
violations: [],
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const toolOutput = zodToJsonSchema(tool.outputSchema);
|
|
132
|
+
// Direction: every tool-returned value must be accepted by the
|
|
133
|
+
// channel schema. Channel is the superset / permissive side,
|
|
134
|
+
// tool-return is the restricted side.
|
|
135
|
+
const result = isSchemaSubset(channelSchema, toolOutput);
|
|
136
|
+
if (!result.compatible) {
|
|
137
|
+
findings.push({
|
|
138
|
+
kind: 'stream',
|
|
139
|
+
specName: channelName,
|
|
140
|
+
toolName,
|
|
141
|
+
reason: 'schema-mismatch',
|
|
142
|
+
violations: result.violations,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const report = {
|
|
147
|
+
compatible: findings.length === 0,
|
|
148
|
+
findings,
|
|
149
|
+
};
|
|
150
|
+
if (mode === 'reject' && !report.compatible) {
|
|
151
|
+
throw new SchemaCompatError(report, context);
|
|
152
|
+
}
|
|
153
|
+
return report;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Format a report into a human-readable message suitable for a
|
|
157
|
+
* thrown error or a log line. Output is deterministic — sorted
|
|
158
|
+
* by `kind` then `specName` — so tests can pattern-match on the
|
|
159
|
+
* exact string without ordering flakes.
|
|
160
|
+
*/
|
|
161
|
+
function formatReport(report, context) {
|
|
162
|
+
if (report.compatible) {
|
|
163
|
+
return `${context}: SCHEMA_MISMATCH_ERROR (no findings — internal error)`;
|
|
164
|
+
}
|
|
165
|
+
const sorted = [...report.findings].sort((a, b) => {
|
|
166
|
+
if (a.kind !== b.kind)
|
|
167
|
+
return a.kind < b.kind ? -1 : 1;
|
|
168
|
+
return a.specName < b.specName ? -1 : a.specName > b.specName ? 1 : 0;
|
|
169
|
+
});
|
|
170
|
+
const lines = sorted.map((f) => {
|
|
171
|
+
if (f.reason === 'tool-not-found') {
|
|
172
|
+
return `- ${f.kind} "${f.specName}" references tool "${f.toolName}" which is not registered`;
|
|
173
|
+
}
|
|
174
|
+
const firstViol = f.violations[0];
|
|
175
|
+
const suffix = firstViol
|
|
176
|
+
? ` — ${firstViol.message}` +
|
|
177
|
+
(f.violations.length > 1
|
|
178
|
+
? ` (${f.violations.length - 1} more violation${f.violations.length > 2 ? 's' : ''})`
|
|
179
|
+
: '')
|
|
180
|
+
: '';
|
|
181
|
+
return `- ${f.kind} "${f.specName}" (tool "${f.toolName}") — schema mismatch${suffix}`;
|
|
182
|
+
});
|
|
183
|
+
return [
|
|
184
|
+
`${context}: SCHEMA_MISMATCH_ERROR — ${report.findings.length} finding${report.findings.length > 1 ? 's' : ''}`,
|
|
185
|
+
...lines,
|
|
186
|
+
].join('\n');
|
|
187
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security headers middleware.
|
|
3
|
+
*
|
|
4
|
+
* Sets three defense-in-depth headers on every response:
|
|
5
|
+
*
|
|
6
|
+
* - `X-Frame-Options: DENY` — clickjacking guard.
|
|
7
|
+
* - `Referrer-Policy: strict-origin-when-cross-origin` — drops the
|
|
8
|
+
* full URL on cross-origin navigations; keeps it on same-origin.
|
|
9
|
+
* - `X-Content-Type-Options: nosniff` — content-type sniffing off.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately does NOT set `Content-Security-Policy` — embedded UI
|
|
12
|
+
* routes manage CSP separately so per-page nonces stay correct. Don't
|
|
13
|
+
* double-set: if a header is already on the response when the
|
|
14
|
+
* middleware runs, leave it alone.
|
|
15
|
+
*
|
|
16
|
+
* `skipPathPrefixes` lets the API surface stay headerless. Default
|
|
17
|
+
* skips:
|
|
18
|
+
* - `/mcp` — claude.ai's connector consumes that endpoint cross-
|
|
19
|
+
* origin, and X-Frame-Options/Referrer-Policy aren't needed for
|
|
20
|
+
* a non-rendered JSON wire.
|
|
21
|
+
* - `/r` + `/preview` — the renderer routes are the iframe-target
|
|
22
|
+
* surfaces MCP Apps hosts (claude.ai, our chat shell, anyone
|
|
23
|
+
* embedding the bootstrap meta) must be able to load. Setting
|
|
24
|
+
* `X-Frame-Options: DENY` on these would be a direct protocol
|
|
25
|
+
* violation. Operators tightening for prod can override via
|
|
26
|
+
* {@link SecurityHeadersMiddlewareOptions.skipPathPrefixes} +
|
|
27
|
+
* route-level CSP `frame-ancestors` if they need a stricter
|
|
28
|
+
* allowlist than "any origin can embed".
|
|
29
|
+
*/
|
|
30
|
+
import type { RequestHandler } from 'express';
|
|
31
|
+
export interface SecurityHeadersMiddlewareOptions {
|
|
32
|
+
readonly skipPathPrefixes?: ReadonlyArray<string>;
|
|
33
|
+
}
|
|
34
|
+
/** Returns an Express middleware that sets the standard ggui security
|
|
35
|
+
* headers — only when not already present on the response, and only
|
|
36
|
+
* on paths NOT matched by `skipPathPrefixes`. */
|
|
37
|
+
export declare function createSecurityHeadersMiddleware(opts?: SecurityHeadersMiddlewareOptions): RequestHandler;
|
|
38
|
+
//# sourceMappingURL=security-headers-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-headers-middleware.d.ts","sourceRoot":"","sources":["../src/security-headers-middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAc9C,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACnD;AAED;;kDAEkD;AAClD,wBAAgB,+BAA+B,CAC7C,IAAI,GAAE,gCAAqC,GAC1C,cAAc,CAgBhB"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const HEADERS = [
|
|
2
|
+
['X-Frame-Options', 'DENY'],
|
|
3
|
+
['Referrer-Policy', 'strict-origin-when-cross-origin'],
|
|
4
|
+
['X-Content-Type-Options', 'nosniff'],
|
|
5
|
+
];
|
|
6
|
+
const DEFAULT_SKIP_PATH_PREFIXES = [
|
|
7
|
+
'/mcp',
|
|
8
|
+
'/r',
|
|
9
|
+
'/preview',
|
|
10
|
+
];
|
|
11
|
+
/** Returns an Express middleware that sets the standard ggui security
|
|
12
|
+
* headers — only when not already present on the response, and only
|
|
13
|
+
* on paths NOT matched by `skipPathPrefixes`. */
|
|
14
|
+
export function createSecurityHeadersMiddleware(opts = {}) {
|
|
15
|
+
const skips = opts.skipPathPrefixes ?? DEFAULT_SKIP_PATH_PREFIXES;
|
|
16
|
+
return (req, res, next) => {
|
|
17
|
+
for (const prefix of skips) {
|
|
18
|
+
if (req.path === prefix || req.path.startsWith(prefix + '/')) {
|
|
19
|
+
next();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (const [name, value] of HEADERS) {
|
|
24
|
+
if (res.getHeader(name) === undefined) {
|
|
25
|
+
res.setHeader(name, value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
next();
|
|
29
|
+
};
|
|
30
|
+
}
|