@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.
Files changed (141) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +48 -0
  3. package/dist/admin-blueprints-transport.d.ts +114 -0
  4. package/dist/admin-blueprints-transport.d.ts.map +1 -0
  5. package/dist/admin-blueprints-transport.js +118 -0
  6. package/dist/admin-oauth-providers-transport.d.ts +40 -0
  7. package/dist/admin-oauth-providers-transport.d.ts.map +1 -0
  8. package/dist/admin-oauth-providers-transport.js +263 -0
  9. package/dist/auth.d.ts +39 -0
  10. package/dist/auth.d.ts.map +1 -0
  11. package/dist/auth.js +75 -0
  12. package/dist/build-mcp.d.ts +128 -0
  13. package/dist/build-mcp.d.ts.map +1 -0
  14. package/dist/build-mcp.js +113 -0
  15. package/dist/code-store-fs.d.ts +19 -0
  16. package/dist/code-store-fs.d.ts.map +1 -0
  17. package/dist/code-store-fs.js +98 -0
  18. package/dist/console-auth.d.ts +139 -0
  19. package/dist/console-auth.d.ts.map +1 -0
  20. package/dist/console-auth.js +102 -0
  21. package/dist/console-cache.d.ts +78 -0
  22. package/dist/console-cache.d.ts.map +1 -0
  23. package/dist/console-cache.js +105 -0
  24. package/dist/console-headers.d.ts +124 -0
  25. package/dist/console-headers.d.ts.map +1 -0
  26. package/dist/console-headers.js +49 -0
  27. package/dist/console-llm-trace.d.ts +66 -0
  28. package/dist/console-llm-trace.d.ts.map +1 -0
  29. package/dist/console-llm-trace.js +105 -0
  30. package/dist/console-payloads.d.ts +67 -0
  31. package/dist/console-payloads.d.ts.map +1 -0
  32. package/dist/console-payloads.js +105 -0
  33. package/dist/console-theme-routes.d.ts +111 -0
  34. package/dist/console-theme-routes.d.ts.map +1 -0
  35. package/dist/console-theme-routes.js +202 -0
  36. package/dist/console-timeline.d.ts +45 -0
  37. package/dist/console-timeline.d.ts.map +1 -0
  38. package/dist/console-timeline.js +169 -0
  39. package/dist/console-validator.d.ts +67 -0
  40. package/dist/console-validator.d.ts.map +1 -0
  41. package/dist/console-validator.js +105 -0
  42. package/dist/console-welcome.d.ts +7 -0
  43. package/dist/console-welcome.d.ts.map +1 -0
  44. package/dist/console-welcome.js +221 -0
  45. package/dist/csrf-middleware.d.ts +55 -0
  46. package/dist/csrf-middleware.d.ts.map +1 -0
  47. package/dist/csrf-middleware.js +138 -0
  48. package/dist/email-login.d.ts +174 -0
  49. package/dist/email-login.d.ts.map +1 -0
  50. package/dist/email-login.js +254 -0
  51. package/dist/email-resend.d.ts +29 -0
  52. package/dist/email-resend.d.ts.map +1 -0
  53. package/dist/email-resend.js +71 -0
  54. package/dist/email-sender-from-env.d.ts +34 -0
  55. package/dist/email-sender-from-env.d.ts.map +1 -0
  56. package/dist/email-sender-from-env.js +112 -0
  57. package/dist/email-smtp.d.ts +42 -0
  58. package/dist/email-smtp.d.ts.map +1 -0
  59. package/dist/email-smtp.js +81 -0
  60. package/dist/index.d.ts +102 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +122 -0
  63. package/dist/instructions-presets.d.ts +112 -0
  64. package/dist/instructions-presets.d.ts.map +1 -0
  65. package/dist/instructions-presets.js +195 -0
  66. package/dist/llm-backed-negotiator.d.ts +178 -0
  67. package/dist/llm-backed-negotiator.d.ts.map +1 -0
  68. package/dist/llm-backed-negotiator.js +579 -0
  69. package/dist/logger.d.ts +23 -0
  70. package/dist/logger.d.ts.map +1 -0
  71. package/dist/logger.js +41 -0
  72. package/dist/mcp-apps-inbound.d.ts +86 -0
  73. package/dist/mcp-apps-inbound.d.ts.map +1 -0
  74. package/dist/mcp-apps-inbound.js +278 -0
  75. package/dist/mcp-apps-outbound.d.ts +448 -0
  76. package/dist/mcp-apps-outbound.d.ts.map +1 -0
  77. package/dist/mcp-apps-outbound.js +1163 -0
  78. package/dist/mcp-mounts.d.ts +239 -0
  79. package/dist/mcp-mounts.d.ts.map +1 -0
  80. package/dist/mcp-mounts.js +222 -0
  81. package/dist/oauth-login-types.d.ts +160 -0
  82. package/dist/oauth-login-types.d.ts.map +1 -0
  83. package/dist/oauth-login-types.js +9 -0
  84. package/dist/oauth-login.d.ts +77 -0
  85. package/dist/oauth-login.d.ts.map +1 -0
  86. package/dist/oauth-login.js +455 -0
  87. package/dist/oauth-providers/github.d.ts +17 -0
  88. package/dist/oauth-providers/github.d.ts.map +1 -0
  89. package/dist/oauth-providers/github.js +89 -0
  90. package/dist/oauth-providers/google.d.ts +18 -0
  91. package/dist/oauth-providers/google.d.ts.map +1 -0
  92. package/dist/oauth-providers/google.js +59 -0
  93. package/dist/oauth-providers-store.d.ts +32 -0
  94. package/dist/oauth-providers-store.d.ts.map +1 -0
  95. package/dist/oauth-providers-store.js +291 -0
  96. package/dist/oauth.d.ts +347 -0
  97. package/dist/oauth.d.ts.map +1 -0
  98. package/dist/oauth.js +686 -0
  99. package/dist/pairing-transport.d.ts +99 -0
  100. package/dist/pairing-transport.d.ts.map +1 -0
  101. package/dist/pairing-transport.js +223 -0
  102. package/dist/rate-limit-middleware.d.ts +36 -0
  103. package/dist/rate-limit-middleware.d.ts.map +1 -0
  104. package/dist/rate-limit-middleware.js +57 -0
  105. package/dist/render-gate.d.ts +87 -0
  106. package/dist/render-gate.d.ts.map +1 -0
  107. package/dist/render-gate.js +77 -0
  108. package/dist/render-rate-limit.d.ts +59 -0
  109. package/dist/render-rate-limit.d.ts.map +1 -0
  110. package/dist/render-rate-limit.js +73 -0
  111. package/dist/render-signing.d.ts +98 -0
  112. package/dist/render-signing.d.ts.map +1 -0
  113. package/dist/render-signing.js +113 -0
  114. package/dist/request-context.d.ts +113 -0
  115. package/dist/request-context.d.ts.map +1 -0
  116. package/dist/request-context.js +154 -0
  117. package/dist/reserved-validators.d.ts +22 -0
  118. package/dist/reserved-validators.d.ts.map +1 -0
  119. package/dist/reserved-validators.js +101 -0
  120. package/dist/schema-compat.d.ts +167 -0
  121. package/dist/schema-compat.d.ts.map +1 -0
  122. package/dist/schema-compat.js +187 -0
  123. package/dist/security-headers-middleware.d.ts +38 -0
  124. package/dist/security-headers-middleware.d.ts.map +1 -0
  125. package/dist/security-headers-middleware.js +30 -0
  126. package/dist/server.d.ts +2060 -0
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +6338 -0
  129. package/dist/session-channel.d.ts +651 -0
  130. package/dist/session-channel.d.ts.map +1 -0
  131. package/dist/session-channel.js +1756 -0
  132. package/dist/storage.d.ts +89 -0
  133. package/dist/storage.d.ts.map +1 -0
  134. package/dist/storage.js +171 -0
  135. package/dist/thread-transport.d.ts +118 -0
  136. package/dist/thread-transport.d.ts.map +1 -0
  137. package/dist/thread-transport.js +478 -0
  138. package/dist/user-session-auth.d.ts +167 -0
  139. package/dist/user-session-auth.d.ts.map +1 -0
  140. package/dist/user-session-auth.js +148 -0
  141. 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
+ }