@xemahq/kernel-contracts 0.50.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/dist/biome/lib/biome-manifest.d.ts +9 -0
  2. package/dist/biome/lib/biome-manifest.d.ts.map +1 -1
  3. package/dist/biome/lib/biome-manifest.js +2 -0
  4. package/dist/biome/lib/biome-manifest.js.map +1 -1
  5. package/dist/capability/index.d.ts +3 -1
  6. package/dist/capability/index.d.ts.map +1 -1
  7. package/dist/capability/index.js +3 -1
  8. package/dist/capability/index.js.map +1 -1
  9. package/dist/capability/lib/authority-effect.d.ts +8 -0
  10. package/dist/capability/lib/authority-effect.d.ts.map +1 -0
  11. package/dist/capability/lib/authority-effect.js +17 -0
  12. package/dist/capability/lib/authority-effect.js.map +1 -0
  13. package/dist/capability/lib/capability-contribution.d.ts +4 -0
  14. package/dist/capability/lib/capability-contribution.d.ts.map +1 -1
  15. package/dist/capability/lib/capability-contribution.js +4 -0
  16. package/dist/capability/lib/capability-contribution.js.map +1 -1
  17. package/dist/capability/lib/capability-domain.d.ts +8 -0
  18. package/dist/capability/lib/capability-domain.d.ts.map +1 -0
  19. package/dist/capability/lib/capability-domain.js +24 -0
  20. package/dist/capability/lib/capability-domain.js.map +1 -0
  21. package/dist/capability/lib/capability-reach.d.ts +13 -0
  22. package/dist/capability/lib/capability-reach.d.ts.map +1 -0
  23. package/dist/capability/lib/capability-reach.js +34 -0
  24. package/dist/capability/lib/capability-reach.js.map +1 -0
  25. package/dist/capability/lib/meta-tool.d.ts +51 -14
  26. package/dist/capability/lib/meta-tool.d.ts.map +1 -1
  27. package/dist/capability/lib/meta-tool.js +66 -12
  28. package/dist/capability/lib/meta-tool.js.map +1 -1
  29. package/dist/capability-projection/index.d.ts +4 -0
  30. package/dist/capability-projection/index.d.ts.map +1 -0
  31. package/dist/capability-projection/index.js +20 -0
  32. package/dist/capability-projection/index.js.map +1 -0
  33. package/dist/capability-projection/lib/capability-invocation-binding.d.ts +72 -0
  34. package/dist/capability-projection/lib/capability-invocation-binding.d.ts.map +1 -0
  35. package/dist/capability-projection/lib/capability-invocation-binding.js +101 -0
  36. package/dist/capability-projection/lib/capability-invocation-binding.js.map +1 -0
  37. package/dist/capability-projection/lib/capability-sync-manifest.d.ts +8 -0
  38. package/dist/capability-projection/lib/capability-sync-manifest.d.ts.map +1 -0
  39. package/dist/capability-projection/lib/capability-sync-manifest.js +8 -0
  40. package/dist/capability-projection/lib/capability-sync-manifest.js.map +1 -0
  41. package/dist/capability-projection/lib/projected-capability.d.ts +15 -0
  42. package/dist/capability-projection/lib/projected-capability.d.ts.map +1 -0
  43. package/dist/capability-projection/lib/projected-capability.js +17 -0
  44. package/dist/capability-projection/lib/projected-capability.js.map +1 -0
  45. package/dist/connector/lib/connector-adapter-contribution.d.ts +2 -0
  46. package/dist/connector/lib/connector-adapter-contribution.d.ts.map +1 -1
  47. package/dist/connector/lib/onboarding-manifest.d.ts +2 -0
  48. package/dist/connector/lib/onboarding-manifest.d.ts.map +1 -1
  49. package/dist/connector/lib/onboarding-manifest.js +1 -0
  50. package/dist/connector/lib/onboarding-manifest.js.map +1 -1
  51. package/dist/connector/lib/provider-descriptor.d.ts +2 -0
  52. package/dist/connector/lib/provider-descriptor.d.ts.map +1 -1
  53. package/dist/contribution/lib/contribution-kind.d.ts +1 -6
  54. package/dist/contribution/lib/contribution-kind.d.ts.map +1 -1
  55. package/dist/contribution/lib/contribution-kind.js +1 -6
  56. package/dist/contribution/lib/contribution-kind.js.map +1 -1
  57. package/dist/distribution/index.d.ts +1 -0
  58. package/dist/distribution/index.d.ts.map +1 -1
  59. package/dist/distribution/index.js +1 -0
  60. package/dist/distribution/index.js.map +1 -1
  61. package/dist/distribution/lib/distribution-lock.d.ts +6 -0
  62. package/dist/distribution/lib/distribution-lock.d.ts.map +1 -1
  63. package/dist/distribution/lib/distribution-lock.js +9 -1
  64. package/dist/distribution/lib/distribution-lock.js.map +1 -1
  65. package/dist/distribution/lib/first-party-capability-domains.d.ts +15 -0
  66. package/dist/distribution/lib/first-party-capability-domains.d.ts.map +1 -0
  67. package/dist/distribution/lib/first-party-capability-domains.js +52 -0
  68. package/dist/distribution/lib/first-party-capability-domains.js.map +1 -0
  69. package/dist/object/lib/xema-object-ref.d.ts +0 -5
  70. package/dist/object/lib/xema-object-ref.d.ts.map +1 -1
  71. package/dist/object/lib/xema-object-ref.js +12 -62
  72. package/dist/object/lib/xema-object-ref.js.map +1 -1
  73. package/dist/output-surface-inspect/index.d.ts +1 -0
  74. package/dist/output-surface-inspect/index.d.ts.map +1 -1
  75. package/dist/output-surface-inspect/index.js +1 -0
  76. package/dist/output-surface-inspect/index.js.map +1 -1
  77. package/dist/output-surface-inspect/lib/buffers.d.ts +52 -0
  78. package/dist/output-surface-inspect/lib/buffers.d.ts.map +1 -0
  79. package/dist/output-surface-inspect/lib/buffers.js +132 -0
  80. package/dist/output-surface-inspect/lib/buffers.js.map +1 -0
  81. package/dist/output-surface-inspect/lib/output-surface-inspect.d.ts +6 -1
  82. package/dist/output-surface-inspect/lib/output-surface-inspect.d.ts.map +1 -1
  83. package/dist/output-surface-inspect/lib/output-surface-inspect.js +12 -1
  84. package/dist/output-surface-inspect/lib/output-surface-inspect.js.map +1 -1
  85. package/dist/output-surface-inspect/lib/step-schema.d.ts +2 -1
  86. package/dist/output-surface-inspect/lib/step-schema.d.ts.map +1 -1
  87. package/dist/output-surface-inspect/lib/step-schema.js +9 -2
  88. package/dist/output-surface-inspect/lib/step-schema.js.map +1 -1
  89. package/dist/output-surface-inspect/runtime/index.d.ts +1 -0
  90. package/dist/output-surface-inspect/runtime/index.d.ts.map +1 -1
  91. package/dist/output-surface-inspect/runtime/index.js +1 -0
  92. package/dist/output-surface-inspect/runtime/index.js.map +1 -1
  93. package/dist/search/index.d.ts +1 -0
  94. package/dist/search/index.d.ts.map +1 -1
  95. package/dist/search/index.js +1 -0
  96. package/dist/search/index.js.map +1 -1
  97. package/dist/search/lib/route-template.d.ts +31 -0
  98. package/dist/search/lib/route-template.d.ts.map +1 -0
  99. package/dist/search/lib/route-template.js +142 -0
  100. package/dist/search/lib/route-template.js.map +1 -0
  101. package/dist/search/lib/search-contribution.d.ts +4 -2
  102. package/dist/search/lib/search-contribution.d.ts.map +1 -1
  103. package/dist/search/lib/search-contribution.js +46 -4
  104. package/dist/search/lib/search-contribution.js.map +1 -1
  105. package/dist/search-source/lib/indexable-document.d.ts +22 -0
  106. package/dist/search-source/lib/indexable-document.d.ts.map +1 -1
  107. package/dist/search-source/lib/indexable-document.js +10 -1
  108. package/dist/search-source/lib/indexable-document.js.map +1 -1
  109. package/dist/search-source/lib/search-index-event.d.ts +10 -0
  110. package/dist/search-source/lib/search-index-event.d.ts.map +1 -1
  111. package/dist/search-source/lib/search-ref.d.ts.map +1 -1
  112. package/dist/search-source/lib/search-ref.js +11 -2
  113. package/dist/search-source/lib/search-ref.js.map +1 -1
  114. package/dist/search-source/lib/search-replay.d.ts +10 -0
  115. package/dist/search-source/lib/search-replay.d.ts.map +1 -1
  116. package/dist/service-registry/lib/service-descriptor.d.ts +1 -0
  117. package/dist/service-registry/lib/service-descriptor.d.ts.map +1 -1
  118. package/dist/space/lib/space-ref-parser.d.ts +6 -4
  119. package/dist/space/lib/space-ref-parser.d.ts.map +1 -1
  120. package/dist/space/lib/space-ref-parser.js +90 -126
  121. package/dist/space/lib/space-ref-parser.js.map +1 -1
  122. package/dist/space/lib/space-traversal.d.ts.map +1 -1
  123. package/dist/space/lib/space-traversal.js +9 -13
  124. package/dist/space/lib/space-traversal.js.map +1 -1
  125. package/dist/space/lib/space.d.ts +7 -0
  126. package/dist/space/lib/space.d.ts.map +1 -1
  127. package/dist/space/lib/space.js +48 -29
  128. package/dist/space/lib/space.js.map +1 -1
  129. package/dist/subject/lib/subject.d.ts +1 -0
  130. package/dist/subject/lib/subject.d.ts.map +1 -1
  131. package/dist/subject/lib/subject.js +1 -0
  132. package/dist/subject/lib/subject.js.map +1 -1
  133. package/dist/surface/index.d.ts +4 -0
  134. package/dist/surface/index.d.ts.map +1 -0
  135. package/dist/surface/index.js +20 -0
  136. package/dist/surface/index.js.map +1 -0
  137. package/dist/surface/lib/surface-contribution.d.ts +24 -0
  138. package/dist/surface/lib/surface-contribution.d.ts.map +1 -0
  139. package/dist/surface/lib/surface-contribution.js +23 -0
  140. package/dist/surface/lib/surface-contribution.js.map +1 -0
  141. package/dist/surface/lib/surface-kind.d.ts +23 -0
  142. package/dist/surface/lib/surface-kind.d.ts.map +1 -0
  143. package/dist/surface/lib/surface-kind.js +27 -0
  144. package/dist/surface/lib/surface-kind.js.map +1 -0
  145. package/dist/surface/lib/vista-surface.d.ts +11 -0
  146. package/dist/surface/lib/vista-surface.d.ts.map +1 -0
  147. package/dist/surface/lib/vista-surface.js +13 -0
  148. package/dist/surface/lib/vista-surface.js.map +1 -0
  149. package/dist/widget/lib/chat-widget-envelope.d.ts.map +1 -1
  150. package/dist/widget/lib/chat-widget-envelope.js.map +1 -1
  151. package/dist/workflow/lib/enums.d.ts.map +1 -1
  152. package/dist/workflow/lib/enums.js.map +1 -1
  153. package/package.json +1 -1
  154. package/src/biome/lib/biome-manifest.ts +41 -1
  155. package/src/capability/index.ts +3 -1
  156. package/src/capability/lib/authority-effect.ts +88 -0
  157. package/src/capability/lib/capability-contribution.ts +44 -0
  158. package/src/capability/lib/capability-domain.ts +51 -0
  159. package/src/capability/lib/capability-grant.ts +1 -1
  160. package/src/capability/lib/capability-reach.ts +119 -0
  161. package/src/capability/lib/meta-tool.ts +172 -39
  162. package/src/capability-projection/index.ts +3 -0
  163. package/src/capability-projection/lib/capability-invocation-binding.ts +223 -0
  164. package/src/capability-projection/lib/capability-sync-manifest.ts +52 -0
  165. package/src/capability-projection/lib/projected-capability.ts +63 -0
  166. package/src/connector/lib/onboarding-manifest.ts +16 -0
  167. package/src/contribution/lib/contribution-kind.ts +165 -61
  168. package/src/distribution/index.ts +1 -0
  169. package/src/distribution/lib/distribution-lock.ts +36 -0
  170. package/src/distribution/lib/first-party-capability-domains.ts +166 -0
  171. package/src/object/lib/xema-object-ref.ts +27 -82
  172. package/src/output-surface-inspect/index.ts +2 -0
  173. package/src/output-surface-inspect/lib/buffers.ts +252 -0
  174. package/src/output-surface-inspect/lib/output-surface-inspect.ts +65 -6
  175. package/src/output-surface-inspect/lib/step-schema.ts +36 -3
  176. package/src/output-surface-inspect/runtime/index.ts +1 -0
  177. package/src/search/index.ts +1 -0
  178. package/src/search/lib/route-template.ts +381 -0
  179. package/src/search/lib/search-contribution.ts +138 -31
  180. package/src/search-source/lib/indexable-document.ts +44 -0
  181. package/src/search-source/lib/search-ref.ts +22 -9
  182. package/src/service-registry/lib/service-descriptor.ts +16 -0
  183. package/src/space/lib/skill-mirror-path.ts +1 -1
  184. package/src/space/lib/space-ref-parser.ts +190 -165
  185. package/src/space/lib/space-traversal.ts +34 -17
  186. package/src/space/lib/space.ts +110 -35
  187. package/src/subject/lib/subject.ts +16 -0
  188. package/src/surface/index.ts +3 -0
  189. package/src/surface/lib/surface-contribution.ts +101 -0
  190. package/src/surface/lib/surface-kind.ts +65 -0
  191. package/src/surface/lib/vista-surface.ts +44 -0
  192. package/src/widget/lib/chat-widget-envelope.ts +6 -2
  193. package/src/workflow/lib/enums.ts +6 -7
  194. package/src/capability/lib/first-party-capability-domains.ts +0 -77
@@ -0,0 +1,63 @@
1
+ import { z } from 'zod';
2
+
3
+ import {
4
+ CapabilityContributionManifestSchema,
5
+ type CapabilityContributionManifest,
6
+ } from '../../capability';
7
+ import {
8
+ CapabilityInvocationBindingSchema,
9
+ type CapabilityInvocationBinding,
10
+ } from './capability-invocation-binding';
11
+
12
+ /**
13
+ * Where a projected capability came from.
14
+ *
15
+ * `sourceKey` scopes the projector's PRUNE set: a projector may only ever
16
+ * delete refs it previously projected, and a ref is attributed to exactly one
17
+ * source per reconcile. `biomeId` / `biomeVersion` are the same provenance the
18
+ * contribution path stamps — authors never declare them inline.
19
+ */
20
+ export interface ProjectedCapabilityProvenance {
21
+ readonly sourceKey: string;
22
+ readonly biomeId: string;
23
+ readonly biomeVersion: string;
24
+ }
25
+
26
+ export const ProjectedCapabilityProvenanceSchema: z.ZodType<ProjectedCapabilityProvenance> =
27
+ z.object({
28
+ sourceKey: z.string().min(1).max(200),
29
+ biomeId: z.string().min(1).max(200),
30
+ biomeVersion: z.string().min(1).max(200),
31
+ }) as unknown as z.ZodType<ProjectedCapabilityProvenance>;
32
+
33
+ /**
34
+ * ─────────────────────────────────────────────────────────────────────────
35
+ * `ProjectedCapability` — one capability DERIVED by a producer, rather than
36
+ * hand-authored as a contribution file.
37
+ *
38
+ * It deliberately EXTENDS {@link CapabilityContributionManifest} instead of
39
+ * restating its fields: the hand-authored path, the decorator path and the
40
+ * projection path must all end up on the SAME `POST /capabilities/register`
41
+ * wire shape, so there is exactly one place that shape is described. A
42
+ * projection adds only the two things a hand-authored descriptor does not
43
+ * carry:
44
+ *
45
+ * - {@link CapabilityInvocationBinding} — HOW to invoke it, as data, so the
46
+ * router resolves a target by lookup instead of a hardcoded switch;
47
+ * - {@link ProjectedCapabilityProvenance} — WHICH source owns it, which is
48
+ * what scopes the reconcile engine's prune set.
49
+ * ─────────────────────────────────────────────────────────────────────────
50
+ */
51
+ export interface ProjectedCapability extends CapabilityContributionManifest {
52
+ readonly invocation: CapabilityInvocationBinding;
53
+ readonly provenance: ProjectedCapabilityProvenance;
54
+ }
55
+
56
+ export const ProjectedCapabilitySchema: z.ZodType<ProjectedCapability> = z
57
+ .intersection(
58
+ CapabilityContributionManifestSchema,
59
+ z.object({
60
+ invocation: CapabilityInvocationBindingSchema,
61
+ provenance: ProjectedCapabilityProvenanceSchema,
62
+ }),
63
+ ) as unknown as z.ZodType<ProjectedCapability>;
@@ -99,6 +99,21 @@ export interface CredentialFieldSpec {
99
99
  readonly type: CredentialFieldType;
100
100
  /** When true, the wizard refuses submission unless the field is filled. */
101
101
  readonly required: boolean;
102
+ /**
103
+ * DECLARED secrecy of the field's value. `true` ⇒ the value is a secret and
104
+ * MUST travel to credential custody (the broker's opaque `secretValue`
105
+ * bundle); `false` ⇒ the value is non-secret configuration and is stored in
106
+ * the provision's plain JSON config.
107
+ *
108
+ * This is a DECLARATION, never an inference. Secrecy was previously derived
109
+ * from `type === CredentialFieldType.PASSWORD`, which made a *rendering*
110
+ * affordance load-bearing for a *custody* decision: a secret rendered as
111
+ * `TEXT` (an API key an author wanted visible) silently landed in
112
+ * non-secret storage, and a non-secret masked for ergonomics silently landed
113
+ * in custody. `type` now means "how to render it" and `isSecret` means
114
+ * "where it may be stored" — the two are independent and both required.
115
+ */
116
+ readonly isSecret: boolean;
102
117
  /** Optional placeholder copy. */
103
118
  readonly placeholder?: string;
104
119
  /** Optional secondary hint shown beneath the input. */
@@ -119,6 +134,7 @@ export const CredentialFieldSpecSchema = z.strictObject({
119
134
  label: z.string().min(1),
120
135
  type: z.nativeEnum(CredentialFieldType),
121
136
  required: z.boolean(),
137
+ isSecret: z.boolean(),
122
138
  placeholder: z.string().optional(),
123
139
  hint: z.string().optional(),
124
140
  transform: z.nativeEnum(CredentialFieldTransform),
@@ -28,12 +28,23 @@ import { z } from 'zod';
28
28
  * adapter projecting the `MountSourceKindRegistry`; NO biome-host
29
29
  * parser.
30
30
  * 3. RESERVED — declared for protocol/catalog completeness but with NO
31
- * contribution-protocol ingestion path today. Where the underlying feature
32
- * exists it ships via the convention CONTENT-DIRECTORY seeder (on-disk
33
- * presencesee the manifest reference), NOT the enum-dispatched
34
- * contribution protocol; a few members have no delivery path at all yet.
35
- * Do NOT assume a biome-host parser exists for a RESERVED member, and do
36
- * not ship a biome contribution of that kind expecting it to be ingested.
31
+ * contribution-protocol ingestion path today. Every remaining RESERVED
32
+ * member has a REAL, verified delivery path that is deliberately NOT the
33
+ * contribution protocol either a biome-host-api HTTP content endpoint
34
+ * that reads the biome's convention directory off disk ON DEMAND
35
+ * (`Icon`, `BiomeInstallSchema`, `ProvisioningScaffold`), or a build-time
36
+ * bake into the agent-runtime image (`OpenCodeTool`, `OpenCodePlugin`).
37
+ * None of them is "seeded": nothing walks those directories at boot.
38
+ * A member with NO delivery path at all is DELETED rather than left as
39
+ * decoration — `GateAction`, `ChartRuntime`, `WorkspaceSpecOverlay` and
40
+ * `SystemOverlayContribution` in the first cleanup; `McpTool` and
41
+ * `WorkspaceManifestTemplate` in the second (each had zero readers, zero
42
+ * on-disk instances anywhere in the tree, no manifest schema, and a live
43
+ * superseding kind — `McpCatalog` and `AgentSkill` respectively). Do NOT
44
+ * assume a biome-host parser exists for a RESERVED member, and do not
45
+ * ship a biome contribution of that kind expecting it to be ingested —
46
+ * biome-host's `assertContributionKindsAreIngestable` coverage guard
47
+ * rejects it fail-fast, naming the file.
37
48
  *
38
49
  * This doc-comment is kept HONEST by review: a member with no biome-host parser
39
50
  * MUST be annotated with its true category (2 or 3). An earlier revision falsely
@@ -46,7 +57,16 @@ import { z } from 'zod';
46
57
  * Enum string values match the slug spoken by the rest of the protocol
47
58
  * (Capability References, manifest declarations) so that round-tripping
48
59
  * between the enum and the wire is lossless. The values themselves are a WIRE
49
- * CONTRACT — never rename or delete a member without a migration plan.
60
+ * CONTRACT — never rename or delete a member without a migration plan. The
61
+ * migration plan for the two deletions above is EMPTY BY CONSTRUCTION, and that
62
+ * is the bar any future deletion must clear: neither value had a producer (no
63
+ * biome ships the directory, no parser emits the envelope), a consumer (no
64
+ * routing-table row, no sync handler), or a persisted row (the contribution-sync
65
+ * store's `kind` column is an open `string`, and biome-host's coverage guard has
66
+ * been rejecting envelopes of both kinds fail-fast). The only downstream churn is
67
+ * mechanical regeneration: biome-host-api's `openapi.public.json`, the generated
68
+ * `@xemahq/biome-host-api-client` `contributionKind` model, and the frontend
69
+ * mirror of both (via `pnpm sync:frontend`).
50
70
  */
51
71
  export enum ContributionKind {
52
72
  // -- v1 seed ---------------------------------------------------------------
@@ -58,27 +78,16 @@ export enum ContributionKind {
58
78
  * stays the authoritative owner and read path; there is NO biome-host parser.
59
79
  */
60
80
  MountSource = 'mount-source',
61
- /**
62
- * RESERVED (category 3). No contribution-protocol parser. MCP tool
63
- * descriptors ship today via the convention `mcpTools/` content directory
64
- * (on-disk presence seeder). The enum-dispatched contribution path for MCP is
65
- * `McpCatalog`, which HAS a parser do not expect one here.
66
- */
67
- McpTool = 'mcp-tool',
81
+ // `McpTool = 'mcp-tool'` was DELETED here. It claimed delivery through a
82
+ // `mcp-tools/` convention directory that NO biome ships and NO code reads:
83
+ // the only references were the `CONTENT_KIND_DIR` / `CONTENT_KIND_EXTENSIONS`
84
+ // table rows and biome-host's generic presence loop. There was no manifest
85
+ // schema (kernel or SDK), no owning registry, and no sync handler. The live
86
+ // MCP contribution path is `McpCatalog` (parser + routes to BOTH
87
+ // llm-registry-api and mcp-gateway-api); the live per-biome code-tool
88
+ // declaration is the manifest block `xema.mcpTools[]`, which is unrelated to
89
+ // this enum despite the shared name.
68
90
  WorkflowStep = 'workflow-step',
69
- /**
70
- * RESERVED (category 3). No ingestion path today — no parser, no
71
- * content-directory seeder, no domain-service handler. The only in-tree use
72
- * is a deliberate PLACEHOLDER reuse of this value in the kernel
73
- * `inquiry-kind-adapter` (its own comment flags it as a placeholder, not an
74
- * equivalence). A biome contribution of this kind is NOT ingested.
75
- */
76
- GateAction = 'gate-action',
77
- /**
78
- * RESERVED (category 3). No ingestion path today; referenced only as a worked
79
- * "a third party adds a new kind" example in the contribution test-suite.
80
- */
81
- ChartRuntime = 'chart-runtime',
82
91
  AgentSkill = 'agent-skill',
83
92
  AgentKernel = 'agent-kernel',
84
93
  /**
@@ -91,6 +100,28 @@ export enum ContributionKind {
91
100
  */
92
101
  ModelResolutionDimension = 'model-resolution-dimension',
93
102
  WidgetKind = 'widget-kind',
103
+ /**
104
+ * Category 1 (biome-host `KindParser` — parser pending). A VISTA SURFACE
105
+ * KIND contributed by a biome: the render kind a Vista preview tab can hold,
106
+ * declared as a bare biome-local slug plus a version, display name, summary,
107
+ * and an opaque JSON `payloadSchema`. The catalogue namespaces the slug to
108
+ * `<biomeId>:<kind>` at ingest so a biome can never shadow a first-party
109
+ * `SurfaceKind`.
110
+ *
111
+ * The kernel owns this member AND the manifest schema
112
+ * (`SurfaceContributionManifestSchema`, exported from
113
+ * `@xemahq/kernel-contracts/surface`) — the 2-file rule. The per-kind
114
+ * ingestion parser belongs to the consuming service (`biome-host-api`), not
115
+ * the kernel, and is NOT shipped yet: a later Vista unit adds
116
+ * `surface-kind.parser.ts` alongside the catalogue endpoint. Until then this
117
+ * member is declared but not ingested — do not ship a biome contribution of
118
+ * this kind expecting it to be picked up.
119
+ *
120
+ * Structurally identical to {@link ContributionKind.WidgetKind} on purpose:
121
+ * the in-chat widget plane and the Vista render plane share ONE contribution
122
+ * shape, one frontend registry core, and one three-layer renderer fallback.
123
+ */
124
+ SurfaceKind = 'surface-kind',
94
125
 
95
126
  // -- Registry gaps closed --------------------------------------------------
96
127
  /**
@@ -117,34 +148,48 @@ export enum ContributionKind {
117
148
  /** A role capability bundle contributed by a biome. */
118
149
  RoleCapability = 'role-capability',
119
150
  /**
120
- * RESERVED (category 3). No contribution-protocol parser. The install-wizard
121
- * resource-selection schema ships via the convention `biomeInstallSchema/`
122
- * content directory (on-disk presence seeder), not the enum-dispatched path.
151
+ * RESERVED (category 3). No parser, and none is wanted: the install-wizard
152
+ * resource-selection schema is an OPAQUE, browser-facing JSON Schema that
153
+ * biome-host-api serves on demand from the biome's convention
154
+ * `install-schema/` directory (`install.json` plus an optional
155
+ * `ui-schema.json`) via `GET /platform/biomes/server/:id/install-schema`. It
156
+ * is not seeded at boot and never crosses a service boundary as a
157
+ * contribution — the only consumer is the frontend install wizard, which
158
+ * pulls it over HTTP and renders it with rjsf. There is nothing for a kernel
159
+ * manifest schema to validate: the body is an arbitrary draft-2020-12 JSON
160
+ * Schema, and a parser could only pass it through verbatim to a registry
161
+ * that does not exist.
123
162
  */
124
163
  BiomeInstallSchema = 'biome-install-schema',
125
164
  /**
126
- * RESERVED (category 3). No parser. Icon assets ship via the convention
127
- * `icons/` content directory (on-disk presence seeder).
165
+ * RESERVED (category 3). No parser, and none is wanted: icons are BYTE
166
+ * ASSETS, not manifests — biome-host-api serves each one straight off disk
167
+ * from the biome's convention `icons/` directory via the public CDN-asset
168
+ * route `GET /platform/biomes/server/:id/icons/:slug.svg`, discovered by
169
+ * on-disk presence and never indexed. Nothing queries "which icons exist":
170
+ * the caller already knows the slug (it is the workflow activity kind) and
171
+ * builds the URL deterministically, so a third-party biome overrides an icon
172
+ * simply by shipping the file. Routing SVG bytes through the contribution
173
+ * outbox would mean base64-in-JSON into a database row only to re-serve it.
128
174
  */
129
175
  Icon = 'icon',
130
176
  /** A project-kit bundle contributed by a biome. */
131
177
  ProjectKit = 'project-kit',
132
178
  /**
133
- * RESERVED (category 3). No parser. Provisioning scaffolds ship via the
134
- * convention `provisioning/` content directory (declared in
135
- * `xema.provisioning[]`, parity-checked at boot), not the contribution path.
179
+ * RESERVED (category 3). No parser: a provisioning scaffold is declared in
180
+ * the manifest's `xema.provisioning[]` and authored as
181
+ * `provisioning/<id>.yaml` plus an optional `provisioning/<id>/` template
182
+ * tree, and agent-session-api's `provisioning-plan-resolver` PULLS it over
183
+ * HTTP per session bootstrap from biome-host-api's
184
+ * `GET /platform/biomes/server/:id/provisioning-scaffolds`. The render
185
+ * depends on that session's runtime `customConfig`, so there is no static
186
+ * descriptor to upsert into a registry, and agent-session-api has no
187
+ * contribution-sync seam. NOTE the declaration-vs-disk parity check is LAZY,
188
+ * not boot-time: it runs inside `loadProvisioningScaffolds` when the
189
+ * scaffolds are fetched, so a biome whose `provisioning[]` disagrees with its
190
+ * `provisioning/` directory boots green and fails at first provision.
136
191
  */
137
192
  ProvisioningScaffold = 'provisioning-scaffold',
138
- /**
139
- * RESERVED (category 3). No ingestion path today — no parser, no
140
- * content-directory seeder, no domain-service handler.
141
- */
142
- WorkspaceSpecOverlay = 'workspace-spec-overlay',
143
- /**
144
- * RESERVED (category 3). No ingestion path today — no parser, no
145
- * content-directory seeder, no domain-service handler.
146
- */
147
- SystemOverlayContribution = 'system-overlay-contribution',
148
193
  /** A concrete connector provider contributed by a biome inside an
149
194
  * adapter kind. */
150
195
  ConnectorAdapter = 'connector-adapter',
@@ -156,29 +201,88 @@ export enum ContributionKind {
156
201
  DeliverableSpec = 'deliverable-spec',
157
202
  /** A workspace manifest contributed by a biome. */
158
203
  WorkspaceManifest = 'workspace-manifest',
159
- /**
160
- * RESERVED (category 3). No parser (distinct from `WorkspaceManifest`, which
161
- * HAS one). Reusable workspace-manifest templates ship via the convention
162
- * `workspaceManifestTemplates/` content directory (on-disk presence seeder).
163
- */
164
- WorkspaceManifestTemplate = 'workspace-manifest-template',
204
+ // `WorkspaceManifestTemplate = 'workspace-manifest-template'` was DELETED
205
+ // here. It claimed delivery through a `workspace-manifest-templates/`
206
+ // convention directory that NOTHING reads no seeder, no endpoint, and no
207
+ // `walkContent` call for that kind exists anywhere. The manifest templates a
208
+ // workspace actually renders (`spec.seedFiles[].template`) are resolved at
209
+ // compose time by `buildSkillBundleTemplateResolver` from SKILL BUNDLE
210
+ // resources at `assets/templates/<name>.hbs`, fetched from
211
+ // skill-registry-api — i.e. they ship as `AgentSkill`, which HAS a parser and
212
+ // a route. Re-adding a second delivery path for the same artifact would fork
213
+ // ownership away from skill-registry (bundle fetch, 5-tier scope ladder,
214
+ // shared Handlebars compile) for no gain.
165
215
  /** A tool-profile contributed by a biome. */
166
216
  ToolProfile = 'tool-profile',
167
- /** An MCP catalog entry contributed by a biome. Distinct from `McpTool`,
168
- * which is a single tool descriptor; an `McpCatalog` entry is a catalog
169
- * manifest. */
217
+ /** An MCP catalog entry contributed by a biome a catalog manifest naming a
218
+ * connectable MCP service and the tools it exposes. This is the ONLY
219
+ * enum-dispatched MCP contribution path. */
170
220
  McpCatalog = 'mcp-catalog',
171
221
  /**
172
- * RESERVED (category 3). No contribution-protocol parser. Agent-runtime
173
- * custom tools ship via the convention `openCodeTools/` content directory,
174
- * baked into the opencode-worker image at build time — not the
175
- * enum-dispatched contribution path.
222
+ * RESERVED (category 3). No parser, and one CANNOT be shipped without first
223
+ * closing the build-time gap tracked as H10 in
224
+ * `.claude/plans/canopy-universal-agent.md` §11.1, which carries the full,
225
+ * component-by-component write-down of what a runtime path would require;
226
+ * this note is the summary. Agent-runtime custom tools are EXECUTABLE
227
+ * `.ts`/`.js` files that reach an agent ONLY by being copied into the
228
+ * agent-runtime worker image at build time — every kernel biome that ships a
229
+ * tools directory has its own explicit `COPY` line in that image's
230
+ * Dockerfile, landing the files in the runtime's tool slot, which the runtime
231
+ * auto-discovers when its process starts. So a third-party biome tool costs a
232
+ * Dockerfile edit plus an image rebuild. The one runtime seam that exists is
233
+ * a WHOLE-DIRECTORY swap, not a merge: when the templates read-only volume is
234
+ * mounted, workspace-proxy deletes the image-baked tool directory and
235
+ * symlinks that volume's copy in its place — and the volume is populated
236
+ * entirely out of band.
237
+ *
238
+ * Do NOT ship a parser that fakes the missing half. A real runtime path needs
239
+ * FOUR things, none of which exists:
240
+ * 1. An owning registry that stores tool SOURCE and can be reconciled by
241
+ * the contribution outbox. It cannot be a plain descriptor table: the
242
+ * payload is code, so the store has to carry the bytes AND the trust
243
+ * decision (signature verification + static analysis — the pipeline
244
+ * `biome-fetcher-api` already runs for biome bundles), not just a row.
245
+ * 2. Materialization in the workspace composer, which starts with a new
246
+ * `WorkspaceMount` slot: that enum is CLOSED and has no tools entry, and
247
+ * workspace-proxy rejects a mount whose slot is not in it. The composer
248
+ * (`@xemahq/agent-session-runtime`) already writes the skill-bundle and
249
+ * slash-command slots the same way; a tools slot additionally needs
250
+ * biome-NAMESPACED filenames (two biomes must not collide on a tool
251
+ * name) that stay FLAT — the runtime's discovery glob is one level deep,
252
+ * so a nested directory is invisible — and it must compose with, not be
253
+ * clobbered by, the templates-volume symlink above.
254
+ * 3. A per-session tool allow/deny decision. Filtering is static today,
255
+ * baked into the image's runtime config, so a dynamically mounted tool
256
+ * has no visibility story.
257
+ * 4. Sandboxing/permission review, because a contributed tool executes in
258
+ * the agent's workspace with the agent's credentials.
259
+ *
260
+ * Timing is the one part already solved: discovery runs once per runtime
261
+ * PROCESS, and workspace-proxy already restarts that process after every
262
+ * session-bundle apply.
176
263
  */
177
264
  OpenCodeTool = 'opencode-tool',
178
265
  /**
179
- * RESERVED (category 3). No parser the contribution-registrar explicitly
180
- * does NOT route this kind. Agent-runtime plugins ship via the convention
181
- * `openCodePlugins/` content directory, baked into the opencode-worker image.
266
+ * RESERVED (category 3). No parser, and the contribution-registrar
267
+ * deliberately does NOT route this kind its own unit test uses this member
268
+ * as the canonical "not routed" fixture. Plugins are baked harder than tools:
269
+ * a kernel biome's plugin directory is installed under a read-only `/opt`
270
+ * prefix during the image build, with its dependency tree resolved there, and
271
+ * the generated runtime config references it by ABSOLUTE IMAGE PATH. That
272
+ * `/opt` location is load-bearing — plugins once lived under the workspace
273
+ * root, where the per-session volume mount SHADOWED them and both shipped
274
+ * plugins silently failed to load.
275
+ *
276
+ * On top of the four prerequisites listed on
277
+ * {@link ContributionKind.OpenCodeTool}, a runtime plugin path additionally
278
+ * needs (a) the plugin list in the generated runtime config to become
279
+ * per-session paths instead of image constants, (b) a dependency story, since
280
+ * a plugin's dependencies are installed at image-build time and the runtime
281
+ * workspace has neither an install step nor network egress for one, and (c) a
282
+ * process restart, because the runtime resolves plugins once at process
283
+ * start. Until all of that lands, treat plugins as an image concern — tools
284
+ * are the cheaper half to make runtime-loadable first. See
285
+ * `.claude/plans/canopy-universal-agent.md` §11.1.
182
286
  */
183
287
  OpenCodePlugin = 'opencode-plugin',
184
288
  // -- Capability self-registration ------------------------------------------
@@ -3,3 +3,4 @@ export * from './lib/distribution-selector';
3
3
  export * from './lib/distribution';
4
4
  export * from './lib/distribution-lock';
5
5
  export * from './lib/image-lock';
6
+ export * from './lib/first-party-capability-domains';
@@ -9,6 +9,7 @@ import {
9
9
  type BiomeOrigin,
10
10
  type BiomeAudience,
11
11
  } from '../../biome';
12
+ import { CapabilityDomainSchema } from '../../capability';
12
13
  import {
13
14
  PlatformServiceTierSchema,
14
15
  type PlatformServiceTier,
@@ -164,10 +165,44 @@ export const LockedPlatformServiceSchema = z.object({
164
165
  * kernel (kernel contracts hold no clock); both optional so the type is usable
165
166
  * mid-resolution.
166
167
  */
168
+ /**
169
+ * One platform-reserved capability domain and the biome that owns it.
170
+ *
171
+ * Resolved from the FULL biome index the release pipeline discovered — NOT
172
+ * from `DistributionLock.biomes`. A reservation is a statement about who
173
+ * AUTHORED the domain, so it holds in every edition whether or not that
174
+ * edition preloads the owning biome. See `first-party-capability-domains.ts`
175
+ * for why per-edition ownership would be a fail-open hole.
176
+ */
177
+ export interface CapabilityDomainOwner {
178
+ /** The `<domain>` segment of a `CapabilityRef`. */
179
+ domain: string;
180
+ /** `xema.id` of the biome whose manifest claims it. */
181
+ biomeId: string;
182
+ }
183
+
184
+ export const CapabilityDomainOwnerSchema = z
185
+ .object({
186
+ domain: CapabilityDomainSchema,
187
+ biomeId: z.string().min(1),
188
+ })
189
+ .strict() as z.ZodType<CapabilityDomainOwner>;
190
+
167
191
  export interface DistributionLock {
168
192
  schemaVersion: 1;
169
193
  distributionId: string;
170
194
  biomes: readonly LockedBiome[];
195
+ /**
196
+ * Platform-reserved capability domains — the input `capability-registry-api`
197
+ * derives its org-provider-binding shadow refusal from, and the router its
198
+ * third-party authority-effect rule.
199
+ *
200
+ * REQUIRED, deliberately. An absent or empty table means "no domain is
201
+ * first-party", which is fail-OPEN: every platform ref would become
202
+ * org-bindable. A lock that predates this field must therefore fail to parse
203
+ * rather than silently disarm the guard.
204
+ */
205
+ capabilityDomainOwners: readonly CapabilityDomainOwner[];
171
206
  /** Non-biome substrate services that ship with this distribution (the deploy "init system"). */
172
207
  platformServices: readonly LockedPlatformService[];
173
208
  /** ISO-8601 timestamp the resolver stamped (tooling-supplied). */
@@ -180,6 +215,7 @@ export const DistributionLockSchema = z.object({
180
215
  schemaVersion: z.literal(1),
181
216
  distributionId: z.string().min(1),
182
217
  biomes: z.array(LockedBiomeSchema).readonly(),
218
+ capabilityDomainOwners: z.array(CapabilityDomainOwnerSchema).readonly(),
183
219
  platformServices: z.array(LockedPlatformServiceSchema).readonly(),
184
220
  resolvedAt: z.string().min(1).optional(),
185
221
  inputHash: z.string().min(1).optional(),
@@ -0,0 +1,166 @@
1
+ import {
2
+ capabilityDomainOf,
3
+ isCapabilityDomain,
4
+ type CapabilityDomain,
5
+ } from '../../capability';
6
+ import type { DistributionLock } from './distribution-lock';
7
+
8
+ /**
9
+ * ═══════════════════════════════════════════════════════════════════════════
10
+ * First-party capability domains — DERIVED from the distribution lock
11
+ * ═══════════════════════════════════════════════════════════════════════════
12
+ *
13
+ * A capability ref is `<domain>:<resource>.<verb>@<major>`. A DOMAIN is
14
+ * "first-party" when an in-the-box platform biome owns every ref under it.
15
+ *
16
+ * WHY THIS MATTERS (the security job):
17
+ *
18
+ * `capability-registry-api` REFUSES an org `CapabilityProviderBinding` whose
19
+ * ref domain is first-party. Without that refusal an org admin could bind
20
+ * `kb:page.read@1` — or `web:page.fetch@1` — to a URL they control and
21
+ * silently shadow the platform data plane for every user, agent and app in
22
+ * the org. The router is data-driven off the invocation binding, so a
23
+ * shadowing binding is a live redirect, not a theoretical one.
24
+ *
25
+ * WHY THE DISTRIBUTION LOCK IS THE RIGHT SOURCE, AND THE OTHER TWO WERE NOT:
26
+ *
27
+ * • NOT `exposesCapabilities` on a live manifest — that is SELF-DECLARED at
28
+ * install time, so a third-party biome an org admin installs could mint
29
+ * itself first-party and then bind the domain it just claimed. Circular.
30
+ * • NOT capability-record provenance in the registry DB — that is written by
31
+ * an eventually-consistent contribution-sync retry, so a derivation from it
32
+ * would report "not first-party" for a genuinely first-party domain during
33
+ * the exact window (boot, resync) the refusal exists to cover. FAIL-OPEN.
34
+ * • The distribution lock is neither. It is RESOLVED by the release pipeline
35
+ * from a pinned checkout, COMMITTED, drift-checked in CI (`dist-check`),
36
+ * vendored into the deployment as an immutable ConfigMap, and read once at
37
+ * boot. Nothing an org admin can do at runtime — installing a biome from
38
+ * npm/OCI/git, editing a manifest, publishing a contribution — changes it.
39
+ * Membership is a PLATFORM-AUTHOR decision, exactly as it should be.
40
+ *
41
+ * ── OWNERSHIP IS RESERVATION-WIDE, NOT PER-EDITION ─────────────────────────
42
+ *
43
+ * `DistributionLock.capabilityDomainOwners` is resolved from the FULL biome
44
+ * index — every first-party biome the release pipeline discovered in its own
45
+ * checkout — NOT from the subset of biomes this edition happens to ship.
46
+ *
47
+ * That distinction is load-bearing. If ownership were per-edition, a trimmed
48
+ * edition that does not ship `knowledge-base` would leave `kb` unowned, so an
49
+ * org admin could install `knowledge-base` from the store and THEN bind
50
+ * `kb:page.read@1` to a URL they control — shadowing it for the whole org.
51
+ * Being authored by the platform is what makes a domain un-shadowable;
52
+ * whether a given edition preloads the owning biome is a packaging choice and
53
+ * must not change the security answer. The domains are RESERVED by the
54
+ * platform in every edition, shipped or not.
55
+ *
56
+ * This is also why the field is on the LOCK and not on `LockedBiome`: a
57
+ * reservation exists whether or not the owning biome appears in `biomes[]`.
58
+ *
59
+ * COLLISIONS FAIL FAST. Two biomes claiming the same domain is a packaging
60
+ * error that would make ownership ambiguous, so this class throws rather than
61
+ * picking a winner. The distribution resolver runs the same check before it
62
+ * writes, so the failure surfaces at build time; the boot-time throw is the
63
+ * backstop.
64
+ *
65
+ * UNDECIDABLE INPUT IS FIRST-PARTY, NOT THIRD-PARTY. `has()` on a string that
66
+ * is not a syntactically valid domain returns `true` (⇒ refuse the binding).
67
+ * A guard must never treat "I could not parse this" as "safe to shadow".
68
+ */
69
+ export class FirstPartyCapabilityDomains {
70
+ private constructor(
71
+ private readonly owners: ReadonlyMap<CapabilityDomain, string>,
72
+ ) {}
73
+
74
+ /**
75
+ * Build the ownership index from a parsed distribution lock's
76
+ * `capabilityDomainOwners` reservation table.
77
+ *
78
+ * @throws when two biomes claim the same domain, or when a claimed value is
79
+ * not a syntactically valid capability domain. Both are packaging errors;
80
+ * neither may be tolerated silently in a security guard's input.
81
+ */
82
+ static fromDistributionLock(
83
+ lock: DistributionLock,
84
+ ): FirstPartyCapabilityDomains {
85
+ return FirstPartyCapabilityDomains.fromOwnerEntries(
86
+ lock.capabilityDomainOwners.map(
87
+ (owner) => [owner.domain, owner.biomeId] as const,
88
+ ),
89
+ `Distribution "${lock.distributionId}"`,
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Build the ownership index directly from `(domain → owning biome id)` pairs.
95
+ * For consumers that receive the ownership projection over the wire instead
96
+ * of reading the lock file themselves. Same validation, same collision rule.
97
+ */
98
+ static fromOwnerEntries(
99
+ entries: Iterable<readonly [string, string]>,
100
+ sourceLabel: string,
101
+ ): FirstPartyCapabilityDomains {
102
+ const owners = new Map<CapabilityDomain, string>();
103
+ for (const [claimed, biomeId] of entries) {
104
+ if (!isCapabilityDomain(claimed)) {
105
+ throw new Error(
106
+ `${sourceLabel}: "${claimed}" is not a valid capability domain (^[a-z][a-z0-9-]*$).`,
107
+ );
108
+ }
109
+ const incumbent = owners.get(claimed);
110
+ if (incumbent !== undefined && incumbent !== biomeId) {
111
+ throw new Error(
112
+ `${sourceLabel}: capability domain "${claimed}" is claimed by BOTH "${incumbent}" and "${biomeId}". ` +
113
+ 'A domain has exactly one owner — ownership decides whether an org may bind refs in it, ' +
114
+ 'so an ambiguous claim is refused rather than resolved to a winner.',
115
+ );
116
+ }
117
+ owners.set(claimed, biomeId);
118
+ }
119
+ return new FirstPartyCapabilityDomains(owners);
120
+ }
121
+
122
+ /**
123
+ * `true` iff `domain` is owned by a biome in the active distribution — or if
124
+ * `domain` is not a parseable domain at all (undecidable ⇒ protected).
125
+ */
126
+ has(domain: string): boolean {
127
+ return !isCapabilityDomain(domain) || this.owners.has(domain);
128
+ }
129
+
130
+ /**
131
+ * `true` iff the DOMAIN OF `ref` is first-party. A ref whose domain cannot be
132
+ * extracted is undecidable and therefore protected.
133
+ */
134
+ hasRefDomain(ref: string): boolean {
135
+ const domain = capabilityDomainOf(ref);
136
+ return domain === null || this.owners.has(domain);
137
+ }
138
+
139
+ /** The biome that owns `domain`, or `undefined` when nobody claims it. */
140
+ ownerOf(domain: string): string | undefined {
141
+ return this.owners.get(domain);
142
+ }
143
+
144
+ /** Claimed domains, sorted — for boot logs and diagnostics. */
145
+ get domains(): readonly CapabilityDomain[] {
146
+ return [...this.owners.keys()].sort();
147
+ }
148
+
149
+ get size(): number {
150
+ return this.owners.size;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Convenience wrapper — `(domain → owning biome id)` for the whole lock, with
156
+ * the collision + grammar checks applied. The distribution resolver calls this
157
+ * at build time so a bad claim fails the release, not the deployment.
158
+ */
159
+ export function resolveFirstPartyCapabilityDomains(
160
+ lock: DistributionLock,
161
+ ): ReadonlyMap<CapabilityDomain, string> {
162
+ const resolved = FirstPartyCapabilityDomains.fromDistributionLock(lock);
163
+ return new Map(
164
+ resolved.domains.map((domain) => [domain, resolved.ownerOf(domain)!]),
165
+ );
166
+ }