@ggui-ai/protocol 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 (222) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +46 -0
  3. package/dist/bridge/invoke-agent.d.ts +65 -0
  4. package/dist/bridge/invoke-agent.d.ts.map +1 -0
  5. package/dist/bridge/invoke-agent.js +113 -0
  6. package/dist/envelope-adapters.d.ts +24 -0
  7. package/dist/envelope-adapters.d.ts.map +1 -0
  8. package/dist/envelope-adapters.js +14 -0
  9. package/dist/envelopes/builders.d.ts +145 -0
  10. package/dist/envelopes/builders.d.ts.map +1 -0
  11. package/dist/envelopes/builders.js +113 -0
  12. package/dist/errors/unknown-permission-name.d.ts +12 -0
  13. package/dist/errors/unknown-permission-name.d.ts.map +1 -0
  14. package/dist/errors/unknown-permission-name.js +29 -0
  15. package/dist/errors/version-mismatch.d.ts +55 -0
  16. package/dist/errors/version-mismatch.d.ts.map +1 -0
  17. package/dist/errors/version-mismatch.js +52 -0
  18. package/dist/gadgets/resolve-contract-gadgets.d.ts +93 -0
  19. package/dist/gadgets/resolve-contract-gadgets.d.ts.map +1 -0
  20. package/dist/gadgets/resolve-contract-gadgets.js +119 -0
  21. package/dist/gadgets/stdlib-gadgets.d.ts +43 -0
  22. package/dist/gadgets/stdlib-gadgets.d.ts.map +1 -0
  23. package/dist/gadgets/stdlib-gadgets.js +161 -0
  24. package/dist/iframe-bridge.d.ts +63 -0
  25. package/dist/iframe-bridge.d.ts.map +1 -0
  26. package/dist/iframe-bridge.js +166 -0
  27. package/dist/index.d.ts +62 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +79 -0
  30. package/dist/integrations/mcp-apps.d.ts +1218 -0
  31. package/dist/integrations/mcp-apps.d.ts.map +1 -0
  32. package/dist/integrations/mcp-apps.js +427 -0
  33. package/dist/navigation/index.d.ts +3 -0
  34. package/dist/navigation/index.d.ts.map +1 -0
  35. package/dist/navigation/index.js +1 -0
  36. package/dist/navigation/stack-navigation.d.ts +55 -0
  37. package/dist/navigation/stack-navigation.d.ts.map +1 -0
  38. package/dist/navigation/stack-navigation.js +80 -0
  39. package/dist/recommended-prompts.d.ts +56 -0
  40. package/dist/recommended-prompts.d.ts.map +1 -0
  41. package/dist/recommended-prompts.js +55 -0
  42. package/dist/registry/blueprint-key.d.ts +9 -0
  43. package/dist/registry/blueprint-key.d.ts.map +1 -0
  44. package/dist/registry/blueprint-key.js +28 -0
  45. package/dist/registry/canonicalize-contract.d.ts +35 -0
  46. package/dist/registry/canonicalize-contract.d.ts.map +1 -0
  47. package/dist/registry/canonicalize-contract.js +166 -0
  48. package/dist/registry/summarize-contract.d.ts +46 -0
  49. package/dist/registry/summarize-contract.d.ts.map +1 -0
  50. package/dist/registry/summarize-contract.js +63 -0
  51. package/dist/schema-learning/derive-contract.d.ts +67 -0
  52. package/dist/schema-learning/derive-contract.d.ts.map +1 -0
  53. package/dist/schema-learning/derive-contract.js +117 -0
  54. package/dist/schema-learning/merge.d.ts +32 -0
  55. package/dist/schema-learning/merge.d.ts.map +1 -0
  56. package/dist/schema-learning/merge.js +146 -0
  57. package/dist/schemas/blueprint.d.ts +32 -0
  58. package/dist/schemas/blueprint.d.ts.map +1 -0
  59. package/dist/schemas/blueprint.js +92 -0
  60. package/dist/schemas/data-contract.d.ts +750 -0
  61. package/dist/schemas/data-contract.d.ts.map +1 -0
  62. package/dist/schemas/data-contract.js +663 -0
  63. package/dist/schemas/gadget-name-grammar.d.ts +29 -0
  64. package/dist/schemas/gadget-name-grammar.d.ts.map +1 -0
  65. package/dist/schemas/gadget-name-grammar.js +28 -0
  66. package/dist/schemas/handshake-suggestion.d.ts +46 -0
  67. package/dist/schemas/handshake-suggestion.d.ts.map +1 -0
  68. package/dist/schemas/handshake-suggestion.js +107 -0
  69. package/dist/schemas/invoke.d.ts +337 -0
  70. package/dist/schemas/invoke.d.ts.map +1 -0
  71. package/dist/schemas/invoke.js +169 -0
  72. package/dist/schemas/mcp.d.ts +301 -0
  73. package/dist/schemas/mcp.d.ts.map +1 -0
  74. package/dist/schemas/mcp.js +373 -0
  75. package/dist/schemas/ops-blueprint.d.ts +176 -0
  76. package/dist/schemas/ops-blueprint.d.ts.map +1 -0
  77. package/dist/schemas/ops-blueprint.js +259 -0
  78. package/dist/schemas/sync-check.d.ts +11 -0
  79. package/dist/schemas/sync-check.d.ts.map +1 -0
  80. package/dist/schemas/sync-check.js +60 -0
  81. package/dist/screen-blueprints/define.d.ts +22 -0
  82. package/dist/screen-blueprints/define.d.ts.map +1 -0
  83. package/dist/screen-blueprints/define.js +3 -0
  84. package/dist/screen-blueprints/index.d.ts +4 -0
  85. package/dist/screen-blueprints/index.d.ts.map +1 -0
  86. package/dist/screen-blueprints/index.js +3 -0
  87. package/dist/screen-blueprints/match.d.ts +35 -0
  88. package/dist/screen-blueprints/match.d.ts.map +1 -0
  89. package/dist/screen-blueprints/match.js +51 -0
  90. package/dist/screen-blueprints/types.d.ts +164 -0
  91. package/dist/screen-blueprints/types.d.ts.map +1 -0
  92. package/dist/screen-blueprints/types.js +1 -0
  93. package/dist/stream/stream-parser.d.ts +62 -0
  94. package/dist/stream/stream-parser.d.ts.map +1 -0
  95. package/dist/stream/stream-parser.js +199 -0
  96. package/dist/transport/websocket.d.ts +178 -0
  97. package/dist/transport/websocket.d.ts.map +1 -0
  98. package/dist/transport/websocket.js +1 -0
  99. package/dist/types/app-config.d.ts +61 -0
  100. package/dist/types/app-config.d.ts.map +1 -0
  101. package/dist/types/app-config.js +1 -0
  102. package/dist/types/auth.d.ts +61 -0
  103. package/dist/types/auth.d.ts.map +1 -0
  104. package/dist/types/auth.js +1 -0
  105. package/dist/types/blueprint.d.ts +206 -0
  106. package/dist/types/blueprint.d.ts.map +1 -0
  107. package/dist/types/blueprint.js +1 -0
  108. package/dist/types/canvas-lifecycle.d.ts +105 -0
  109. package/dist/types/canvas-lifecycle.d.ts.map +1 -0
  110. package/dist/types/canvas-lifecycle.js +38 -0
  111. package/dist/types/capabilities.d.ts +40 -0
  112. package/dist/types/capabilities.d.ts.map +1 -0
  113. package/dist/types/capabilities.js +19 -0
  114. package/dist/types/contract-inference.d.ts +401 -0
  115. package/dist/types/contract-inference.d.ts.map +1 -0
  116. package/dist/types/contract-inference.js +44 -0
  117. package/dist/types/credential.d.ts +41 -0
  118. package/dist/types/credential.d.ts.map +1 -0
  119. package/dist/types/credential.js +32 -0
  120. package/dist/types/data-bindings.d.ts +322 -0
  121. package/dist/types/data-bindings.d.ts.map +1 -0
  122. package/dist/types/data-bindings.js +29 -0
  123. package/dist/types/data-contract.d.ts +1296 -0
  124. package/dist/types/data-contract.d.ts.map +1 -0
  125. package/dist/types/data-contract.js +111 -0
  126. package/dist/types/events.d.ts +182 -0
  127. package/dist/types/events.d.ts.map +1 -0
  128. package/dist/types/events.js +8 -0
  129. package/dist/types/feedback.d.ts +24 -0
  130. package/dist/types/feedback.d.ts.map +1 -0
  131. package/dist/types/feedback.js +7 -0
  132. package/dist/types/gadget.d.ts +121 -0
  133. package/dist/types/gadget.d.ts.map +1 -0
  134. package/dist/types/gadget.js +24 -0
  135. package/dist/types/handshake-suggestion.d.ts +264 -0
  136. package/dist/types/handshake-suggestion.d.ts.map +1 -0
  137. package/dist/types/handshake-suggestion.js +70 -0
  138. package/dist/types/host-context.d.ts +163 -0
  139. package/dist/types/host-context.d.ts.map +1 -0
  140. package/dist/types/host-context.js +142 -0
  141. package/dist/types/interface-context.d.ts +105 -0
  142. package/dist/types/interface-context.d.ts.map +1 -0
  143. package/dist/types/interface-context.js +115 -0
  144. package/dist/types/invoke.d.ts +28 -0
  145. package/dist/types/invoke.d.ts.map +1 -0
  146. package/dist/types/invoke.js +7 -0
  147. package/dist/types/live-channel.d.ts +613 -0
  148. package/dist/types/live-channel.d.ts.map +1 -0
  149. package/dist/types/live-channel.js +1 -0
  150. package/dist/types/llm.d.ts +61 -0
  151. package/dist/types/llm.d.ts.map +1 -0
  152. package/dist/types/llm.js +186 -0
  153. package/dist/types/mcp-proxy.d.ts +67 -0
  154. package/dist/types/mcp-proxy.d.ts.map +1 -0
  155. package/dist/types/mcp-proxy.js +46 -0
  156. package/dist/types/mcp.d.ts +637 -0
  157. package/dist/types/mcp.d.ts.map +1 -0
  158. package/dist/types/mcp.js +30 -0
  159. package/dist/types/openrouter-models.d.ts +22 -0
  160. package/dist/types/openrouter-models.d.ts.map +1 -0
  161. package/dist/types/openrouter-models.js +4843 -0
  162. package/dist/types/region.d.ts +26 -0
  163. package/dist/types/region.d.ts.map +1 -0
  164. package/dist/types/region.js +36 -0
  165. package/dist/types/session.d.ts +419 -0
  166. package/dist/types/session.d.ts.map +1 -0
  167. package/dist/types/session.js +1 -0
  168. package/dist/types/thread.d.ts +207 -0
  169. package/dist/types/thread.d.ts.map +1 -0
  170. package/dist/types/thread.js +57 -0
  171. package/dist/types/ui-generator.d.ts +100 -0
  172. package/dist/types/ui-generator.d.ts.map +1 -0
  173. package/dist/types/ui-generator.js +53 -0
  174. package/dist/validation/ajv-runtime.d.ts +140 -0
  175. package/dist/validation/ajv-runtime.d.ts.map +1 -0
  176. package/dist/validation/ajv-runtime.js +452 -0
  177. package/dist/validation/content-hash.d.ts +3 -0
  178. package/dist/validation/content-hash.d.ts.map +1 -0
  179. package/dist/validation/content-hash.js +21 -0
  180. package/dist/validation/contract-validator.d.ts +244 -0
  181. package/dist/validation/contract-validator.d.ts.map +1 -0
  182. package/dist/validation/contract-validator.js +711 -0
  183. package/dist/validation/cross-references.d.ts +105 -0
  184. package/dist/validation/cross-references.d.ts.map +1 -0
  185. package/dist/validation/cross-references.js +164 -0
  186. package/dist/validation/hygiene-rules.d.ts +250 -0
  187. package/dist/validation/hygiene-rules.d.ts.map +1 -0
  188. package/dist/validation/hygiene-rules.js +564 -0
  189. package/dist/validation/lint-contract.d.ts +130 -0
  190. package/dist/validation/lint-contract.d.ts.map +1 -0
  191. package/dist/validation/lint-contract.js +225 -0
  192. package/dist/validation/name-invariants.d.ts +117 -0
  193. package/dist/validation/name-invariants.d.ts.map +1 -0
  194. package/dist/validation/name-invariants.js +172 -0
  195. package/dist/validation/reserved-channels.d.ts +156 -0
  196. package/dist/validation/reserved-channels.d.ts.map +1 -0
  197. package/dist/validation/reserved-channels.js +356 -0
  198. package/dist/validation/resolve-stream-channel.d.ts +78 -0
  199. package/dist/validation/resolve-stream-channel.d.ts.map +1 -0
  200. package/dist/validation/resolve-stream-channel.js +64 -0
  201. package/dist/validation/sanitize-error.d.ts +46 -0
  202. package/dist/validation/sanitize-error.d.ts.map +1 -0
  203. package/dist/validation/sanitize-error.js +88 -0
  204. package/dist/validation/schema-compat-invariants.d.ts +140 -0
  205. package/dist/validation/schema-compat-invariants.d.ts.map +1 -0
  206. package/dist/validation/schema-compat-invariants.js +220 -0
  207. package/dist/validation/schema-meta-validation.d.ts +60 -0
  208. package/dist/validation/schema-meta-validation.d.ts.map +1 -0
  209. package/dist/validation/schema-meta-validation.js +131 -0
  210. package/dist/validation/schema-subset.d.ts +165 -0
  211. package/dist/validation/schema-subset.d.ts.map +1 -0
  212. package/dist/validation/schema-subset.js +295 -0
  213. package/dist/validation/ui-security.d.ts +54 -0
  214. package/dist/validation/ui-security.d.ts.map +1 -0
  215. package/dist/validation/ui-security.js +138 -0
  216. package/dist/validation/zod-to-json-schema.d.ts +63 -0
  217. package/dist/validation/zod-to-json-schema.d.ts.map +1 -0
  218. package/dist/validation/zod-to-json-schema.js +126 -0
  219. package/dist/version.d.ts +1458 -0
  220. package/dist/version.d.ts.map +1 -0
  221. package/dist/version.js +1459 -0
  222. package/package.json +113 -0
@@ -0,0 +1,564 @@
1
+ /**
2
+ * Authoring-hygiene rules for `DataContract`. Lint rule registry —
3
+ * warnings only, never thrown by {@link validateContract}; surfaced
4
+ * by {@link lintContract} so authoring tools can offer "your contract
5
+ * is technically valid, but here are the polish items" feedback.
6
+ *
7
+ * Ships the most universally applicable subset:
8
+ *
9
+ * - `LINT_ORPHAN_AGENT_TOOL` — `agentCapabilities.tools[X]` is
10
+ * declared but never referenced from any `actionSpec[*].nextStep`
11
+ * or `streamSpec[*].source.tool`. The entry is dead weight — either
12
+ * wire it up or drop it. (Catches the common "agent dropped a
13
+ * reference but left the catalog entry behind" drift.)
14
+ *
15
+ * - `LINT_MISSING_USAGE` — `agentCapabilities.tools[*]` or
16
+ * `clientCapabilities.gadgets[*]` lacks a `usage` field.
17
+ * `usage` is the free-form LLM-targeted prose that bare
18
+ * `description` lacks — when omitted, the agent's reasoning
19
+ * loop loses important context-of-use information.
20
+ *
21
+ * - `LINT_MISSING_EXAMPLE` — `agentCapabilities.tools[*]` lacks an
22
+ * `example`. Examples ground the agent's invocation patterns; a
23
+ * tool without one is harder to use correctly on the first call.
24
+ *
25
+ * - `LINT_GADGET_DUPLICATE_EXPORT` — two `clientCapabilities.gadgets[*]`
26
+ * entries declare the same export name (a `hook` name or a
27
+ * `component` name). The boilerplate generator emits one import
28
+ * per export name; a collision is unresolvable in module scope.
29
+ * Keyed on the export name alone.
30
+ *
31
+ * Gadget lints split by timing into two surfaces:
32
+ *
33
+ * - **Wire-side** ({@link checkHygiene}, input `DataContract`):
34
+ * `checkGadgetHookNames` + `checkDuplicateGadgetHooks`.
35
+ * - **Registry-side** ({@link lintGadgetCatalog}, input
36
+ * `readonly GadgetDescriptor[]`): permission + immutability +
37
+ * duplicate-hook + unscoped-package checks, run at registration
38
+ * time. Some codes are fatal — see {@link FATAL_CATALOG_LINT_CODES}.
39
+ *
40
+ * Pure checks; return violations rather than throwing. The wire-side
41
+ * set is wired into `lintContract` via `phaseHygiene`; consumers that
42
+ * want strict gates layer their own assertions on top.
43
+ */
44
+ import { STDLIB_GADGETS_PACKAGE, STDLIB_GADGET_HOOKS, } from '../gadgets/stdlib-gadgets.js';
45
+ import { gadgetExportName, listContractGadgets, } from '../gadgets/resolve-contract-gadgets.js';
46
+ import { HOOK_NAME_RE } from '../schemas/gadget-name-grammar.js';
47
+ /**
48
+ * Stable codes for hygiene rules. Each is a `LINT_*` rather than a
49
+ * `CTR_*` — the convention: errors are `CTR_*`, warnings are `LINT_*`.
50
+ *
51
+ * The gadget lints split into two timing buckets:
52
+ *
53
+ * - **Wire-side** (run on a `DataContract` by {@link checkHygiene}):
54
+ * `LINT_GADGET_UNKNOWN_HOOK`, `LINT_GADGET_DUPLICATE_EXPORT`.
55
+ * - **Registry-side** (run on an `App.gadgets` catalog at
56
+ * registration time by {@link lintGadgetCatalog}):
57
+ * `LINT_GADGET_MISSING_PERMISSION`, `LINT_GADGET_UNKNOWN_PERMISSION`,
58
+ * `LINT_GADGET_UNSCOPED_PACKAGE`, `LINT_GADGET_IMMUTABLE_MUTATION`,
59
+ * `LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG`.
60
+ */
61
+ export const LINT_ORPHAN_AGENT_TOOL = 'LINT_ORPHAN_AGENT_TOOL';
62
+ export const LINT_MISSING_USAGE = 'LINT_MISSING_USAGE';
63
+ export const LINT_MISSING_EXAMPLE = 'LINT_MISSING_EXAMPLE';
64
+ export const LINT_GADGET_UNKNOWN_HOOK = 'LINT_GADGET_UNKNOWN_HOOK';
65
+ export const LINT_GADGET_DUPLICATE_EXPORT = 'LINT_GADGET_DUPLICATE_EXPORT';
66
+ export const LINT_CONTRACT_RETIRED_FIELD = 'LINT_CONTRACT_RETIRED_FIELD';
67
+ // ── Registry-side (lintGadgetCatalog) ──
68
+ export const LINT_GADGET_MISSING_PERMISSION = 'LINT_GADGET_MISSING_PERMISSION';
69
+ export const LINT_GADGET_UNKNOWN_PERMISSION = 'LINT_GADGET_UNKNOWN_PERMISSION';
70
+ export const LINT_GADGET_UNSCOPED_PACKAGE = 'LINT_GADGET_UNSCOPED_PACKAGE';
71
+ export const LINT_GADGET_IMMUTABLE_MUTATION = 'LINT_GADGET_IMMUTABLE_MUTATION';
72
+ export const LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG = 'LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG';
73
+ export const LINT_GADGET_DUPLICATE_PACKAGE = 'LINT_GADGET_DUPLICATE_PACKAGE';
74
+ /**
75
+ * Registry-side lint codes that denote a HARD integrity violation —
76
+ * registration handlers MUST reject the catalog (not just warn) when
77
+ * {@link lintGadgetCatalog} emits one of these. The lint function
78
+ * itself stays pure (returns warnings); severity classification is
79
+ * the caller's, so this set is the single source of truth for "which
80
+ * codes are fatal."
81
+ *
82
+ * - `LINT_GADGET_IMMUTABLE_MUTATION` — two descriptors share a
83
+ * `(package, version)` tuple but disagree on `bundleSri`. The
84
+ * same immutable bundle cannot have two hashes; cached blueprints
85
+ * keyed on that version would silently break.
86
+ * - `LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG` — two descriptors
87
+ * export the same name (a `hook` name or a `component` name). The
88
+ * boilerplate generator emits one
89
+ * `import { <name> } from '<package>'` per export; a name
90
+ * collision in module scope is unresolvable.
91
+ */
92
+ export const FATAL_CATALOG_LINT_CODES = new Set([
93
+ LINT_GADGET_IMMUTABLE_MUTATION,
94
+ LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG,
95
+ LINT_GADGET_DUPLICATE_PACKAGE,
96
+ ]);
97
+ /**
98
+ * Retired top-level `DataContract` field names. The contract schema
99
+ * is `.passthrough()` at the type system level (forward-compat
100
+ * hedge), but these specific names denote fields that have a known
101
+ * replacement in the current protocol. Carrying one of them is a
102
+ * caller bug — silent pass-through would mask the migration.
103
+ *
104
+ * Replacements (kept here so the lint message can teach the fix):
105
+ * - `libraries` → `clientCapabilities.gadgets`
106
+ * - `dispatch` → `agentCapabilities.tools` + `actionSpec[*].nextStep`
107
+ * - `wiredTools` → `agentCapabilities.tools`
108
+ * - `clientTools` → `clientCapabilities.gadgets`
109
+ * - `broadcast` → `streamSpec[ch].source`
110
+ * - `capabilities` → `agentCapabilities` + `clientCapabilities`
111
+ *
112
+ * Push-gate handlers re-use this list to hard-reject; surfacing it
113
+ * here keeps the wire vocabulary single-sourced.
114
+ */
115
+ export const RETIRED_CONTRACT_FIELDS = {
116
+ libraries: 'clientCapabilities.gadgets',
117
+ dispatch: 'agentCapabilities.tools + actionSpec[*].nextStep',
118
+ wiredTools: 'agentCapabilities.tools',
119
+ clientTools: 'clientCapabilities.gadgets',
120
+ broadcast: 'streamSpec[ch].source',
121
+ capabilities: 'agentCapabilities + clientCapabilities',
122
+ };
123
+ /**
124
+ * Sourced from {@link STDLIB_GADGET_HOOKS} — the canonical
125
+ * hook-name set the first-party `@ggui-ai/gadgets` package
126
+ * exports. Local alias keeps existing call sites stable while the
127
+ * source of truth lives in `registries/stdlib-gadgets.ts`.
128
+ */
129
+ const KNOWN_STDLIB_HOOKS = STDLIB_GADGET_HOOKS;
130
+ /**
131
+ * Default package for gadget hooks. Sourced from the
132
+ * stdlib-libraries registry — when an entry's `package` is omitted,
133
+ * the hygiene linter assumes it resolves to the first-party
134
+ * `@ggui-ai/gadgets` package and runs hook-registry +
135
+ * permission checks against the stdlib catalog. Third-party packages
136
+ * skip those checks.
137
+ */
138
+ const DEFAULT_GADGET_PACKAGE = STDLIB_GADGETS_PACKAGE;
139
+ /**
140
+ * Permission strings the Web Permissions API ratifies, plus the
141
+ * MCP Apps `_meta.ui.permissions` enum members for host
142
+ * passthrough.
143
+ *
144
+ * Exported as a tuple + literal-union type so
145
+ * `strictGadgetDescriptorSchema.permission` can use
146
+ * `z.enum(KNOWN_PERMISSION_NAMES)` for a hard reject at parse time:
147
+ * typos (`'geolocaiton'`) and unsupported values fail at the wire
148
+ * boundary instead of being demoted to a soft warning. Forward-compat
149
+ * additions land via a protocol version bump.
150
+ */
151
+ export const KNOWN_PERMISSION_NAMES = [
152
+ // Web Permissions API names
153
+ 'geolocation',
154
+ 'notifications',
155
+ 'microphone',
156
+ 'camera',
157
+ 'persistent-storage',
158
+ 'midi',
159
+ 'clipboard-read',
160
+ 'clipboard-write',
161
+ 'speaker-selection',
162
+ 'storage-access',
163
+ 'background-sync',
164
+ 'accelerometer',
165
+ 'gyroscope',
166
+ 'magnetometer',
167
+ 'ambient-light-sensor',
168
+ 'screen-wake-lock',
169
+ // MCP Apps `_meta.ui.permissions` mirror (matches the Web Permissions
170
+ // API names today; future spec additions go here).
171
+ ];
172
+ const KNOWN_PERMISSION_NAMES_SET = new Set(KNOWN_PERMISSION_NAMES);
173
+ /**
174
+ * Stdlib hooks whose Web Permissions API name is well-known. A
175
+ * registered descriptor for one of these hooks SHOULD declare the
176
+ * matching `permission` so {@link lintGadgetCatalog} can surface the
177
+ * "this UI will prompt for X" context to the agent's reasoning loop.
178
+ * Maps hook name → expected permission.
179
+ */
180
+ const KNOWN_PERMISSION_HOOKS = {
181
+ useGeolocation: 'geolocation',
182
+ useNotifications: 'notifications',
183
+ useMicrophone: 'microphone',
184
+ useCamera: 'camera',
185
+ useClipboardPaste: 'clipboard-read',
186
+ useClipboardWrite: 'clipboard-write',
187
+ };
188
+ /**
189
+ * Collect every `agentCapabilities.tools[*]` key that's referenced from
190
+ * `actionSpec[*].nextStep` or `streamSpec[*].source.tool`. Used to
191
+ * detect orphans (declared but unreferenced) and is exported because
192
+ * future rules / tooling may want the same accounting.
193
+ */
194
+ function collectReferencedAgentTools(contract) {
195
+ const referenced = new Set();
196
+ const actionSpec = contract.actionSpec ?? {};
197
+ for (const entry of Object.values(actionSpec)) {
198
+ if (!entry || typeof entry !== 'object')
199
+ continue;
200
+ const nextStep = entry.nextStep;
201
+ if (typeof nextStep === 'string' && nextStep.length > 0) {
202
+ referenced.add(nextStep);
203
+ }
204
+ }
205
+ const streamSpec = contract.streamSpec ?? {};
206
+ for (const entry of Object.values(streamSpec)) {
207
+ if (!entry || typeof entry !== 'object')
208
+ continue;
209
+ const source = entry.source;
210
+ if (!source)
211
+ continue;
212
+ const tool = source.tool;
213
+ if (typeof tool === 'string' && tool.length > 0) {
214
+ referenced.add(tool);
215
+ }
216
+ }
217
+ return referenced;
218
+ }
219
+ /**
220
+ * Find agentCapabilities.tools entries that are declared but never
221
+ * referenced from actionSpec or streamSpec. Each orphan is dead
222
+ * weight — either wire it up or drop it from the catalog.
223
+ */
224
+ export function checkOrphanAgentTools(contract) {
225
+ const tools = contract.agentCapabilities?.tools;
226
+ if (!tools)
227
+ return [];
228
+ const referenced = collectReferencedAgentTools(contract);
229
+ const warnings = [];
230
+ for (const name of Object.keys(tools)) {
231
+ if (referenced.has(name))
232
+ continue;
233
+ warnings.push({
234
+ code: LINT_ORPHAN_AGENT_TOOL,
235
+ path: `agentCapabilities.tools.${name}`,
236
+ message: `agentCapabilities.tools.${name} is declared but never referenced from actionSpec[*].nextStep or streamSpec[*].source.tool. Dead-weight catalog entry — either wire it up or remove it.`,
237
+ fixHint: `Add a reference like 'actionSpec.<action>.nextStep = "${name}"' or 'streamSpec.<channel>.source.tool = "${name}"', or delete the catalog entry.`,
238
+ });
239
+ }
240
+ return warnings;
241
+ }
242
+ /**
243
+ * Find `agentCapabilities.tools` entries missing the `usage` field.
244
+ * `usage` is the LLM-targeted "when / why / by-whom" prose; without it
245
+ * the agent's reasoning loop loses context-of-use information.
246
+ *
247
+ * Scope is `agentCapabilities.tools` ONLY. `clientCapabilities.gadgets`
248
+ * is intentionally NOT linted here: `GadgetExportUse.usage` is an
249
+ * OPTIONAL intent-OVERRIDE, and the SPEC-documented canonical wire
250
+ * form is the bare identity reference `gadgets[<pkg>][<export>] = {}`.
251
+ * Push-time resolution inherits the registered descriptor's `usage`,
252
+ * and the registry-side `lintGadgetCatalog` (via
253
+ * `strictGadgetExportSchema`) already enforces real teaching text at
254
+ * registration time. Flagging an empty wire-side use object would
255
+ * false-positive the documented happy path.
256
+ */
257
+ export function checkMissingUsage(contract) {
258
+ const warnings = [];
259
+ const tools = contract.agentCapabilities?.tools;
260
+ if (tools) {
261
+ for (const [name, entry] of Object.entries(tools)) {
262
+ if (!entry || typeof entry !== 'object')
263
+ continue;
264
+ const usage = entry.usage;
265
+ if (typeof usage === 'string' && usage.length > 0)
266
+ continue;
267
+ warnings.push({
268
+ code: LINT_MISSING_USAGE,
269
+ path: `agentCapabilities.tools.${name}.usage`,
270
+ message: `agentCapabilities.tools.${name} has no 'usage' prose. The agent's reasoning loop reads usage as context-of-use; tools without it tend to get invoked at the wrong time.`,
271
+ fixHint: `Add 'usage: "..."' describing when / why / by whom this tool is invoked.`,
272
+ });
273
+ }
274
+ }
275
+ return warnings;
276
+ }
277
+ /**
278
+ * Find agentCapabilities.tools entries missing the `example` field.
279
+ * Examples ground the agent's invocation patterns; a tool without
280
+ * one is harder to use correctly on the first call.
281
+ */
282
+ export function checkMissingExample(contract) {
283
+ const tools = contract.agentCapabilities?.tools;
284
+ if (!tools)
285
+ return [];
286
+ const warnings = [];
287
+ for (const [name, entry] of Object.entries(tools)) {
288
+ if (!entry || typeof entry !== 'object')
289
+ continue;
290
+ if (entry.example !== undefined)
291
+ continue;
292
+ warnings.push({
293
+ code: LINT_MISSING_EXAMPLE,
294
+ path: `agentCapabilities.tools.${name}.example`,
295
+ message: `agentCapabilities.tools.${name} has no 'example'. Examples ground the agent's invocation patterns; tools without one are harder to use correctly on the first call.`,
296
+ fixHint: `Add 'example: { input: {...}, output: ... }' with a representative call shape.`,
297
+ });
298
+ }
299
+ return warnings;
300
+ }
301
+ /**
302
+ * Wire-side gadget hook-name lint. For every
303
+ * `clientCapabilities.gadgets[*]` whose `package` is the first-party
304
+ * stdlib (`@ggui-ai/gadgets`), the `hook` MUST be one the stdlib
305
+ * actually exports — catches typos (`useGeoLocation`) + stale
306
+ * references against a constant catalog.
307
+ *
308
+ * Third-party packages (any `package !== DEFAULT_GADGET_PACKAGE`) are
309
+ * NOT checked here — the lint can't know an operator's own hook
310
+ * names. The registry-side {@link lintGadgetCatalog} + the push-time
311
+ * {@link assertGadgetsRegistered} gate cover third-party resolution.
312
+ *
313
+ * Permission checks live on the registry-side `lintGadgetCatalog`
314
+ * (the wire gadget reference carries no `permission` field). This
315
+ * function is the pure wire-only residue: a constant-catalog
316
+ * hook-name check.
317
+ */
318
+ export function checkGadgetHookNames(contract) {
319
+ const warnings = [];
320
+ for (const gadget of listContractGadgets(contract)) {
321
+ if (gadget.package !== DEFAULT_GADGET_PACKAGE)
322
+ continue;
323
+ const path = `clientCapabilities.gadgets.${gadget.package}.exports.${gadget.name}`;
324
+ // The first-party stdlib ships hooks only — a component export
325
+ // pinning `@ggui-ai/gadgets` is a mistake. Kind is read off the
326
+ // export-name grammar (`use`-prefixed hook vs PascalCase component).
327
+ if (!HOOK_NAME_RE.test(gadget.name)) {
328
+ warnings.push({
329
+ code: LINT_GADGET_UNKNOWN_HOOK,
330
+ path,
331
+ message: `clientCapabilities.gadgets declares a component '${gadget.name}' from '${gadget.package}', but the first-party stdlib ships hooks only.`,
332
+ fixHint: `Use a hook from the v1 catalog, or reference a third-party gadget package that exports the component.`,
333
+ });
334
+ continue;
335
+ }
336
+ if (!KNOWN_STDLIB_HOOKS.has(gadget.name)) {
337
+ warnings.push({
338
+ code: LINT_GADGET_UNKNOWN_HOOK,
339
+ path,
340
+ message: `clientCapabilities.gadgets references hook '${gadget.name}' from '${gadget.package}', which doesn't ship that hook. Known: ${[
341
+ ...KNOWN_STDLIB_HOOKS,
342
+ ].join(', ')}.`,
343
+ fixHint: `Pick a hook from the v1 catalog, or reference a third-party hook package the lint doesn't know about.`,
344
+ });
345
+ }
346
+ }
347
+ return warnings;
348
+ }
349
+ /**
350
+ * Find `clientCapabilities.gadgets` exports that declare the same
351
+ * export NAME from two different packages.
352
+ *
353
+ * The wire is package-keyed, so the same name cannot repeat WITHIN a
354
+ * package (object-key uniqueness). The hazard is cross-package: two
355
+ * packages each exporting `useCheckout`. The boilerplate generator
356
+ * emits one `import { <name> } from '<package>'` per export; two
357
+ * imports of the same name — from different packages — produce an
358
+ * unresolvable identifier collision in the generated module scope.
359
+ *
360
+ * Keys on the export name alone, matching the push-time hard gate
361
+ * `assertNoDuplicateGadgetHooks`. Soft mirror of that gate so
362
+ * authoring tools surface the issue before a push round-trip.
363
+ */
364
+ export function checkDuplicateGadgetHooks(contract) {
365
+ const seen = new Map(); // export name → first package
366
+ const warnings = [];
367
+ for (const gadget of listContractGadgets(contract)) {
368
+ const prior = seen.get(gadget.name);
369
+ if (prior !== undefined) {
370
+ warnings.push({
371
+ code: LINT_GADGET_DUPLICATE_EXPORT,
372
+ path: `clientCapabilities.gadgets.${gadget.package}.exports.${gadget.name}`,
373
+ message: `clientCapabilities.gadgets declares export '${gadget.name}' from both '${prior}' and '${gadget.package}'. The boilerplate generator emits one import per export name — two packages exporting the same name collide in module scope.`,
374
+ fixHint: `Drop one of the gadgets, or have the operator register the second under an aliased export name.`,
375
+ });
376
+ continue;
377
+ }
378
+ seen.set(gadget.name, gadget.package);
379
+ }
380
+ return warnings;
381
+ }
382
+ /**
383
+ * Registry-side catalog lint — runs on an `App.gadgets` descriptor
384
+ * array at registration time (ggui.json load, `ops_register_gadget`,
385
+ * registry install). Pure function; returns warnings. The caller
386
+ * (registration handler) treats any code in
387
+ * {@link FATAL_CATALOG_LINT_CODES} as a hard reject.
388
+ *
389
+ * Checks:
390
+ *
391
+ * - `LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG` (fatal) — two
392
+ * descriptors export the same name (a `hook` name or a
393
+ * `component` name). Each export name is unique per app; the
394
+ * boilerplate's per-export import would collide.
395
+ * - `LINT_GADGET_IMMUTABLE_MUTATION` (fatal) — two descriptors
396
+ * carry the same `(package, version)` tuple but different
397
+ * `bundleSri`. The same immutable bundle cannot have two hashes;
398
+ * a cached blueprint pinned to that version would break.
399
+ * - `LINT_GADGET_MISSING_PERMISSION` — a known-permission stdlib
400
+ * hook (geolocation, camera, …) registered without a
401
+ * `permission` field. The agent's reasoning loop reads it to
402
+ * surface "this UI prompts for X."
403
+ * - `LINT_GADGET_UNKNOWN_PERMISSION` — `permission` set to a value
404
+ * outside the Web Permissions API set. (The strict registry
405
+ * schema enum-checks this too; the lint is defence-in-depth for
406
+ * permissively-parsed catalogs.)
407
+ * - `LINT_GADGET_UNSCOPED_PACKAGE` — `package` lacks an `@scope/`
408
+ * prefix. Soft recommendation: scoped names avoid registry
409
+ * squatting + name collisions.
410
+ */
411
+ export function lintGadgetCatalog(descriptors) {
412
+ const warnings = [];
413
+ // export name → first occurrence path (catalog-wide uniqueness).
414
+ const seenExportName = new Map();
415
+ // package name → first occurrence path (one descriptor per package).
416
+ const seenPackage = new Map();
417
+ const sriByVersionTuple = new Map();
418
+ descriptors.forEach((descriptor, index) => {
419
+ const path = `gadgets[${index}]`;
420
+ // ── Immutable-bundle mutation (fatal) — package-level ──
421
+ // Two descriptors for the same (package, version) MUST agree on
422
+ // bundleSri — they reference the same immutable artifact.
423
+ if (typeof descriptor.bundleSri === 'string' &&
424
+ descriptor.bundleSri.length > 0) {
425
+ const tupleKey = `${descriptor.package}\t${descriptor.version}`;
426
+ const prior = sriByVersionTuple.get(tupleKey);
427
+ if (prior !== undefined && prior.sri !== descriptor.bundleSri) {
428
+ warnings.push({
429
+ code: LINT_GADGET_IMMUTABLE_MUTATION,
430
+ path: `${path}.bundleSri`,
431
+ message: `gadgets[${index}] (${descriptor.package}@${descriptor.version}) declares bundleSri '${descriptor.bundleSri}' but ${prior.path} declares '${prior.sri}' for the same package+version. A published version is immutable — one bundle, one hash.`,
432
+ fixHint: `Bump the version on whichever descriptor ships the changed bundle, or correct the mismatched SRI.`,
433
+ });
434
+ }
435
+ else if (prior === undefined) {
436
+ sriByVersionTuple.set(tupleKey, { path, sri: descriptor.bundleSri });
437
+ }
438
+ }
439
+ // ── Duplicate package (fatal) — package-level ──
440
+ // `(name, package)` ref resolution requires at most ONE descriptor
441
+ // per package in an app's catalog. Two descriptors sharing a
442
+ // package name make `filterDescriptorsToContract` silently pick
443
+ // one — `version` is no longer on the wire to disambiguate.
444
+ const priorPackagePath = seenPackage.get(descriptor.package);
445
+ if (priorPackagePath !== undefined) {
446
+ warnings.push({
447
+ code: LINT_GADGET_DUPLICATE_PACKAGE,
448
+ path: `${path}.package`,
449
+ message: `gadgets[${index}].package is '${descriptor.package}', already registered by ${priorPackagePath}. An app's catalog MUST hold at most one descriptor per package — the wire references a package by name and the server resolves exactly one descriptor (no version to disambiguate).`,
450
+ fixHint: `Register a single descriptor per package; drop or merge the duplicate.`,
451
+ });
452
+ }
453
+ else {
454
+ seenPackage.set(descriptor.package, path);
455
+ }
456
+ // ── Unscoped package (soft) — package-level ──
457
+ if (!descriptor.package.startsWith('@')) {
458
+ warnings.push({
459
+ code: LINT_GADGET_UNSCOPED_PACKAGE,
460
+ path: `${path}.package`,
461
+ message: `gadgets[${index}].package is '${descriptor.package}' — an unscoped npm name. Scoped names ('@org/name') avoid registry squatting + cross-publisher collisions.`,
462
+ fixHint: `Publish under an '@scope/' prefix.`,
463
+ });
464
+ }
465
+ // ── Per-export checks ──
466
+ descriptor.exports.forEach((exp, exportIndex) => {
467
+ const exportName = gadgetExportName(exp);
468
+ const exportPath = `${path}.exports[${exportIndex}]`;
469
+ // Duplicate export name (fatal) — catalog-wide. The boilerplate
470
+ // generator emits one import per export name; a collision is
471
+ // unresolvable in module scope.
472
+ const priorPath = seenExportName.get(exportName);
473
+ if (priorPath !== undefined) {
474
+ warnings.push({
475
+ code: LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG,
476
+ path: exportPath,
477
+ message: `${exportPath} exports '${exportName}', already exported by ${priorPath}. Each gadget export name MUST be unique within an app's catalog — the boilerplate generator emits one import per name.`,
478
+ fixHint: `Drop the duplicate, or publish one gadget under an aliased export name.`,
479
+ });
480
+ }
481
+ else {
482
+ seenExportName.set(exportName, exportPath);
483
+ }
484
+ // Missing permission (soft) — a known-permission stdlib hook.
485
+ // `GadgetExport` is a type-exclusive union (`hook?: never` on the
486
+ // component member); discrimination is by VALUE presence, not the
487
+ // `in` operator, since `hook` is now an optional key of both.
488
+ if (exp.hook !== undefined) {
489
+ const expectedPermission = KNOWN_PERMISSION_HOOKS[exp.hook];
490
+ if (expectedPermission !== undefined &&
491
+ (typeof exp.permission !== 'string' || exp.permission.length === 0)) {
492
+ warnings.push({
493
+ code: LINT_GADGET_MISSING_PERMISSION,
494
+ path: `${exportPath}.permission`,
495
+ message: `${exportPath} registers hook '${exp.hook}' without a 'permission' field. The agent's reasoning loop reads permission to surface "this UI will prompt for ${expectedPermission}" context.`,
496
+ fixHint: `Add 'permission: "${expectedPermission}"' (Web Permissions API name).`,
497
+ });
498
+ }
499
+ }
500
+ // Unknown permission (soft).
501
+ if (typeof exp.permission === 'string' &&
502
+ exp.permission.length > 0 &&
503
+ !KNOWN_PERMISSION_NAMES_SET.has(exp.permission)) {
504
+ warnings.push({
505
+ code: LINT_GADGET_UNKNOWN_PERMISSION,
506
+ path: `${exportPath}.permission`,
507
+ message: `${exportPath}.permission is '${exp.permission}', which isn't a known Web Permissions API name. Catches typos + flags non-standard permissions for review.`,
508
+ fixHint: `Pick a name from the Web Permissions API spec (e.g., 'geolocation', 'notifications', 'microphone'), or document the custom value if intentional.`,
509
+ });
510
+ }
511
+ });
512
+ });
513
+ return warnings;
514
+ }
515
+ /**
516
+ * Find top-level retired-field carriers on the contract. The schema
517
+ * is `.passthrough()`, so a stray `libraries`/`dispatch`/`wiredTools`/
518
+ * `clientTools`/`broadcast`/`capabilities` slips through silently. The
519
+ * push-gate hard-rejects these (see
520
+ * `mcp-server-handlers/.../assert-contract-no-retired-fields.ts`); this
521
+ * lint surface keeps authoring tools symmetric — show the warning before
522
+ * the push call so the author can fix it without a server round-trip.
523
+ */
524
+ export function checkRetiredContractFields(contract) {
525
+ // Cast to a generic record so we can probe the keys the passthrough
526
+ // schema lets ride. The contract surface here is post-parse so all
527
+ // typed fields are already covered; we're specifically looking for
528
+ // siblings the type system can't see.
529
+ const raw = contract;
530
+ const warnings = [];
531
+ for (const [retired, replacement] of Object.entries(RETIRED_CONTRACT_FIELDS)) {
532
+ if (raw[retired] === undefined)
533
+ continue;
534
+ warnings.push({
535
+ code: LINT_CONTRACT_RETIRED_FIELD,
536
+ path: retired,
537
+ message: `contract.${retired} is retired. Use ${replacement} instead — the field rides through .passthrough() but the push gate hard-rejects it as a structural error.`,
538
+ fixHint: `Delete contract.${retired}; move its data to ${replacement}.`,
539
+ });
540
+ }
541
+ return warnings;
542
+ }
543
+ /**
544
+ * Run every WIRE-side hygiene rule on a `DataContract`. Aggregates
545
+ * warnings; order is stable (orphans → usage → example → gadget
546
+ * hook-names → duplicate-hook → retired fields) so authoring tools
547
+ * render a predictable checklist.
548
+ *
549
+ * Registry-side gadget lints (`lintGadgetCatalog`) are NOT run here:
550
+ * they need an `App.gadgets` descriptor array, not a contract, and
551
+ * fire at registration time rather than push time.
552
+ * Call {@link lintGadgetCatalog} separately at the registration
553
+ * boundary.
554
+ */
555
+ export function checkHygiene(contract) {
556
+ return [
557
+ ...checkOrphanAgentTools(contract),
558
+ ...checkMissingUsage(contract),
559
+ ...checkMissingExample(contract),
560
+ ...checkGadgetHookNames(contract),
561
+ ...checkDuplicateGadgetHooks(contract),
562
+ ...checkRetiredContractFields(contract),
563
+ ];
564
+ }