@xemahq/kernel-contracts 0.7.0 → 0.9.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 (69) hide show
  1. package/dist/agent-composition/index.d.ts +3 -3
  2. package/dist/agent-composition/index.d.ts.map +1 -1
  3. package/dist/agent-composition/index.js +3 -3
  4. package/dist/agent-composition/index.js.map +1 -1
  5. package/dist/agent-composition/lib/agent-limits-schema.d.ts +4 -0
  6. package/dist/agent-composition/lib/agent-limits-schema.d.ts.map +1 -0
  7. package/dist/agent-composition/lib/agent-limits-schema.js +13 -0
  8. package/dist/agent-composition/lib/agent-limits-schema.js.map +1 -0
  9. package/dist/agent-composition/lib/agent-workspace-config.d.ts +37 -0
  10. package/dist/agent-composition/lib/agent-workspace-config.d.ts.map +1 -0
  11. package/dist/agent-composition/lib/agent-workspace-config.js +9 -0
  12. package/dist/agent-composition/lib/agent-workspace-config.js.map +1 -0
  13. package/dist/agent-composition/lib/agent.d.ts +65 -0
  14. package/dist/agent-composition/lib/agent.d.ts.map +1 -0
  15. package/dist/agent-composition/lib/agent.js +18 -0
  16. package/dist/agent-composition/lib/agent.js.map +1 -0
  17. package/dist/agent-workspace/awp-spec.json +1 -1
  18. package/dist/biome/lib/biome-manifest.d.ts +880 -27
  19. package/dist/biome/lib/biome-manifest.d.ts.map +1 -1
  20. package/dist/biome/lib/biome-manifest.js +507 -20
  21. package/dist/biome/lib/biome-manifest.js.map +1 -1
  22. package/dist/distribution/lib/distribution-selector.d.ts +2 -0
  23. package/dist/distribution/lib/distribution-selector.d.ts.map +1 -1
  24. package/dist/distribution/lib/distribution-selector.js +6 -2
  25. package/dist/distribution/lib/distribution-selector.js.map +1 -1
  26. package/dist/resource/index.d.ts +7 -0
  27. package/dist/resource/index.d.ts.map +1 -0
  28. package/dist/resource/index.js +23 -0
  29. package/dist/resource/index.js.map +1 -0
  30. package/dist/resource/lib/plan.d.ts +31 -0
  31. package/dist/resource/lib/plan.d.ts.map +1 -0
  32. package/dist/resource/lib/plan.js +32 -0
  33. package/dist/resource/lib/plan.js.map +1 -0
  34. package/dist/resource/lib/resource-declaration.d.ts +15 -0
  35. package/dist/resource/lib/resource-declaration.d.ts.map +1 -0
  36. package/dist/resource/lib/resource-declaration.js +32 -0
  37. package/dist/resource/lib/resource-declaration.js.map +1 -0
  38. package/dist/resource/lib/resource-kind.d.ts +20 -0
  39. package/dist/resource/lib/resource-kind.d.ts.map +1 -0
  40. package/dist/resource/lib/resource-kind.js +24 -0
  41. package/dist/resource/lib/resource-kind.js.map +1 -0
  42. package/dist/resource/lib/resource-managed-by.d.ts +9 -0
  43. package/dist/resource/lib/resource-managed-by.d.ts.map +1 -0
  44. package/dist/resource/lib/resource-managed-by.js +13 -0
  45. package/dist/resource/lib/resource-managed-by.js.map +1 -0
  46. package/dist/resource/lib/stack.d.ts +22 -0
  47. package/dist/resource/lib/stack.d.ts.map +1 -0
  48. package/dist/resource/lib/stack.js +23 -0
  49. package/dist/resource/lib/stack.js.map +1 -0
  50. package/dist/resource/lib/xema-manifest.d.ts +11 -0
  51. package/dist/resource/lib/xema-manifest.d.ts.map +1 -0
  52. package/dist/resource/lib/xema-manifest.js +47 -0
  53. package/dist/resource/lib/xema-manifest.js.map +1 -0
  54. package/package.json +1 -1
  55. package/src/agent-composition/index.ts +3 -3
  56. package/src/agent-composition/lib/{composition-limits-schema.ts → agent-limits-schema.ts} +1 -1
  57. package/src/agent-composition/lib/{composition.ts → agent.ts} +1 -1
  58. package/src/biome/lib/biome-manifest.ts +1140 -66
  59. package/src/distribution/lib/distribution-selector.ts +16 -4
  60. package/src/invocation/lib/invoke-request.ts +1 -1
  61. package/src/invocation/lib/invoke-response.ts +1 -1
  62. package/src/resource/index.ts +22 -0
  63. package/src/resource/lib/plan.ts +83 -0
  64. package/src/resource/lib/resource-declaration.ts +67 -0
  65. package/src/resource/lib/resource-kind.ts +37 -0
  66. package/src/resource/lib/resource-managed-by.ts +29 -0
  67. package/src/resource/lib/stack.ts +61 -0
  68. package/src/resource/lib/xema-manifest.ts +86 -0
  69. /package/src/agent-composition/lib/{composition-workspace.ts → agent-workspace-config.ts} +0 -0
@@ -1,39 +1,58 @@
1
- import { z } from 'zod';
2
1
  import {
3
- CapabilityRefSchema,
4
- type CapabilityRef,
5
- } from '../../capability';
2
+ CAPABILITY_SLUG_PATTERN,
3
+ FilterExprSchema,
4
+ } from '../../connector';
5
+ import { ContributionKindSchema } from '../../contribution';
6
+ import { CapabilityRefSchema } from '../../capability';
6
7
  import {
7
- ContributionKind,
8
- ContributionKindSchema,
9
- } from '../../contribution';
10
- import { BiomeTrustTier, BiomeTrustTierSchema } from './biome-trust-tier';
11
- import { BiomeEnginesSchema, type BiomeEngines } from './biome-engines';
8
+ ProvisioningGuardSchema,
9
+ ProvisioningStepKindSchema,
10
+ ProvisioningTriggerSchema,
11
+ } from '../../provisioning';
12
12
  import {
13
- BiomePermissionsManifestSchema,
14
- type BiomePermissionsManifest,
15
- } from './biome-permissions';
13
+ MigrationRunnerKind,
14
+ OrgDatabasePurpose,
15
+ } from '../../org-database';
16
+ import {
17
+ DataLocalitySchema,
18
+ RuntimeIsolationLevelSchema,
19
+ RunnerTrustTier,
20
+ } from '../../runner';
21
+ import { z } from 'zod';
22
+ import {
23
+ BiomeAudience,
24
+ BiomeAudienceSchema,
25
+ DEFAULT_BIOME_AUDIENCE,
26
+ } from './biome-audience';
27
+ import {
28
+ BiomeEnginesSchema,
29
+ } from './biome-engines';
16
30
  import {
17
31
  BiomeLifecycleHooksSchema,
18
- type BiomeLifecycleHooks,
19
32
  } from './biome-lifecycle-hooks';
20
- import { BiomeApiManifestSchema, type BiomeApiManifest } from './biome-api';
33
+ import {
34
+ BiomePermissionsManifestSchema,
35
+ } from './biome-permissions';
21
36
 
22
37
  /**
23
- * Canonical filename of the new biome manifest at the root of every
24
- * biome source package (plan §4.1). The legacy `xema-biome.json` name
25
- * is NOT recognised by this contracts package — Phase 6 (plan §17.7)
26
- * renames it in a single coordinated PR, with no dual parser.
38
+ * `xema-biome.json` declarative description of a Xema biome's
39
+ * identity and the contributions it ships. The schema is a discriminated
40
+ * union on `xema.target`.
41
+ *
42
+ * This is the SINGLE canonical biome-manifest schema for the platform: the
43
+ * `{ name, version, xema: { … } }` wrapped shape. The legacy flat
44
+ * `{ schemaVersion, id, name, … }` shape was deleted — there is no dual
45
+ * parser and no backward-compat shim. `@xemahq/biome-sdk/host` re-exports
46
+ * everything in this module; it owns no second copy.
27
47
  */
28
- export const BIOME_MANIFEST_FILENAME = 'xema-biome.json';
29
48
 
30
49
  /**
31
- * Schema-versioning seed for the biome manifest. Bumped via a
32
- * coordinated PR whenever the manifest shape changes incompatibly;
33
- * downstream consumers refuse manifests whose `schemaVersion` they do
34
- * not understand (fail-fast — no silent best-effort parse).
50
+ * Canonical filename of the biome manifest at the root of every biome
51
+ * source package. The platform validates and parses ONLY this file's
52
+ * top-level `{ name, version, xema }` shape; everything else in the
53
+ * bundle is free-form.
35
54
  */
36
- export type BiomeManifestSchemaVersion = 1;
55
+ export const BIOME_MANIFEST_FILENAME = 'xema-biome.json';
37
56
 
38
57
  /**
39
58
  * Declarative event subscription entry of the biome manifest
@@ -53,50 +72,1105 @@ export const BiomeEventSubscriptionSchema = z.object({
53
72
  }) as z.ZodType<BiomeEventSubscription>;
54
73
 
55
74
  /**
56
- * `BiomeManifest` — the top-level shape of `xema-biome.json` (plan §4,
57
- * §12, §24.4, §24.5, §25). This is the NEW shape introduced in Phase
58
- * 1A; the SDK-side parser is still housed under `biome-host-sdk` until
59
- * the Phase 6 rename, but the schema itself lives here from day one.
60
- */
61
- export interface BiomeManifest {
62
- schemaVersion: BiomeManifestSchemaVersion;
63
- id: string;
64
- name: string;
65
- version: string;
66
- trustTier: BiomeTrustTier;
67
- engines: BiomeEngines;
68
- requiresCapabilities: readonly CapabilityRef[];
69
- exposesCapabilities: readonly CapabilityRef[];
70
- contributes: readonly ContributionKind[];
71
- subscribes?: readonly BiomeEventSubscription[];
72
- /** biome-id → semver range. */
73
- requires?: Readonly<Record<string, string>>;
74
- permissions?: BiomePermissionsManifest;
75
- lifecycle?: BiomeLifecycleHooks;
76
- ships?: {
77
- apis?: readonly BiomeApiManifest[];
78
- content?: readonly string[];
79
- };
75
+ * Manifest-shape biome SCOPE — the dependency/boot tier a biome declares in
76
+ * its `xema.scope` field. Value-identical to the kernel `BiomeTier` set
77
+ * (`kernel|system|base|platform`). Kept as a manifest-internal schema (not
78
+ * re-exported) so it does not collide with the kernel `BiomeScope`
79
+ * (installation scope) / `BiomeTier` (dependency tier) enums that the rest
80
+ * of `@xemahq/kernel-contracts/biome` already exposes.
81
+ *
82
+ * Biome tier (Layered Biome Architecture — "Linux-faithful" model):
83
+ * - `kernel` — L1 universal, mandatory. Boots before any other biome.
84
+ * - `system` — L2 system biomes (systemd / dbus / sshd analogues).
85
+ * - `base` — L2.5 foundational user-facing primitives.
86
+ * - `platform` — L3 first-party domain + integration biomes.
87
+ */
88
+ const ManifestBiomeScopeSchema = z.enum([
89
+ 'kernel',
90
+ 'system',
91
+ 'base',
92
+ 'platform',
93
+ ]);
94
+
95
+ /**
96
+ * The Contribution Protocol (`@xemahq/kernel-contracts/contribution`) is the
97
+ * single authoritative enum for kinds a biome may ship.
98
+ *
99
+ * Conventional on-disk directory name per `ships.content[]` / `ships.modules[]`
100
+ * string. Used ONLY for the `ships`-based file-walk seeders that have not yet
101
+ * migrated to the `contributions/` directory protocol. New code MUST use the
102
+ * `xema.contributions` block (or inline contributions) instead of
103
+ * `ships.content` / `ships.modules`.
104
+ *
105
+ * Keys are the camelCase tokens accepted in `xema-biome.json`; the mapping
106
+ * returns `null` for any key it does not recognise so callers can fail fast.
107
+ */
108
+ const LEGACY_SHIPS_CONTENT_DIR: Readonly<Record<string, string>> = {
109
+ agents: 'agents',
110
+ workflowConfig: 'workflow-config',
111
+ deliverableSpecs: 'deliverable-specs',
112
+ workspaceManifests: 'workspace-manifests',
113
+ workspaceManifestTemplates: 'workspace-manifest-templates',
114
+ toolProfiles: 'tool-profiles',
115
+ mcpCatalog: 'mcp-catalog',
116
+ mcpTools: 'mcp-tools',
117
+ openCodeSkills: 'skills',
118
+ openCodeTools: 'opencode-tools',
119
+ openCodePlugins: 'opencode-plugins',
120
+ roleCapabilities: 'role-capabilities',
121
+ artifactTypes: 'artifact-types',
122
+ biomeInstallSchema: 'install-schema',
123
+ icons: 'icons',
124
+ projectKits: 'project-kits',
125
+ provisioningScaffolds: 'provisioning',
126
+ };
127
+
128
+ const LEGACY_SHIPS_MODULES_DIR: Readonly<Record<string, string>> = {
129
+ agents: 'agents',
130
+ actions: 'actions',
131
+ mountSourceKinds: 'mount-source-kinds',
132
+ deliverableSpecKinds: 'deliverable-spec-kinds',
133
+ runtimeMountKinds: 'runtime-mount-kinds',
134
+ workspaceSpecOverlay: 'workspace-spec-overlay',
135
+ systemOverlayContributions: 'system-overlay-contributions',
136
+ adapterKinds: 'adapter-kinds',
137
+ integrationProviders: 'integration-providers',
138
+ };
139
+
140
+ /**
141
+ * Resolve a legacy `ships.content[]` value to its conventional directory
142
+ * name, or `null` for unknown tokens. Used by the legacy file-walk
143
+ * catalog/seeders; new code should use the contributions protocol.
144
+ */
145
+ export function contentKindToDir(kind: string): string {
146
+ const dir = LEGACY_SHIPS_CONTENT_DIR[kind];
147
+ if (!dir) {
148
+ throw new Error(
149
+ `biome-host-sdk: unknown legacy ships.content[] value "${kind}". ` +
150
+ `Either migrate the biome to xema.contributions or extend LEGACY_SHIPS_CONTENT_DIR.`,
151
+ );
152
+ }
153
+ return dir;
154
+ }
155
+
156
+ /**
157
+ * Resolve a legacy `ships.modules[]` value to its conventional directory
158
+ * name. Same legacy-only scope as `contentKindToDir`.
159
+ */
160
+ export function moduleKindToDir(kind: string): string {
161
+ const dir = LEGACY_SHIPS_MODULES_DIR[kind];
162
+ if (!dir) {
163
+ throw new Error(
164
+ `biome-host-sdk: unknown legacy ships.modules[] value "${kind}". ` +
165
+ `Either migrate the biome to xema.contributions or extend LEGACY_SHIPS_MODULES_DIR.`,
166
+ );
167
+ }
168
+ return dir;
80
169
  }
81
170
 
82
- export const BiomeManifestSchema = z.object({
83
- schemaVersion: z.literal(1),
84
- id: z.string().min(1),
85
- name: z.string().min(1),
86
- version: z.string().min(1),
87
- trustTier: BiomeTrustTierSchema,
88
- engines: BiomeEnginesSchema,
89
- requiresCapabilities: z.array(CapabilityRefSchema).readonly(),
90
- exposesCapabilities: z.array(CapabilityRefSchema).readonly(),
91
- contributes: z.array(ContributionKindSchema).readonly(),
92
- subscribes: z.array(BiomeEventSubscriptionSchema).readonly().optional(),
171
+ /**
172
+ * Org-managed database declaration for biomes that need a dedicated
173
+ * schema in a provisioned org database.
174
+ */
175
+ export const BiomeDatabaseDeclarationSchema = z
176
+ .object({
177
+ purpose: z.literal(OrgDatabasePurpose.Biome),
178
+ runnerKind: z.literal(MigrationRunnerKind.Prisma),
179
+ })
180
+ .strict();
181
+ export type BiomeDatabaseDeclaration = z.infer<typeof BiomeDatabaseDeclarationSchema>;
182
+
183
+ /**
184
+ * Per-API declaration shipped under `xema.ships.apis[]` (Phase 9).
185
+ * Each entry becomes one subdomain (`<name>.api.<base-domain>`)
186
+ * served by a biome-shipped NestJS service. biome-host-api applies
187
+ * the Helm sub-chart at install time and writes a route-registry
188
+ * entry the ingress shim consults. Validated structurally here;
189
+ * detailed contract semantics live in `@xemahq/biome-api-sdk`.
190
+ */
191
+ export const BiomeApiDeclarationSchema = z
192
+ .object({
193
+ name: z
194
+ .string()
195
+ .min(1)
196
+ .regex(
197
+ /^[a-z][a-z0-9-]*$/,
198
+ 'API name must be lowercase kebab-case; becomes the subdomain slug',
199
+ ),
200
+ basePath: z
201
+ .string()
202
+ .regex(/^\/.*$/)
203
+ .optional(),
204
+ /** Workspace package name + port — the deployment image. */
205
+ image: z
206
+ .object({
207
+ package: z.string().min(1),
208
+ port: z.number().int().min(1).max(65_535),
209
+ })
210
+ .strict(),
211
+ openapiSpec: z.string().optional(),
212
+ scopes: z.array(z.enum(['public', 'org', 'project', 'installation'])).min(1),
213
+ /**
214
+ * Relative directory (from the manifest) of this API's source tree, e.g.
215
+ * `./api/mailops-api`. Used by `tooling/codegen/generate-service-bootstrap.mjs`
216
+ * to locate where to emit the `<name>.bootstrap.generated.ts` descriptor.
217
+ */
218
+ path: z.string().min(1).optional(),
219
+ /**
220
+ * Service-bootstrap SSOT fields (consumed by `XemaServiceModule.forBiome`
221
+ * via the generated `BiomeServiceDescriptor`). Declaring them HERE — once,
222
+ * in the manifest — is what removes the duplicate string literals from each
223
+ * service's `app.module.ts`. All optional with safe codegen defaults.
224
+ */
225
+ displayName: z.string().min(1).optional(),
226
+ serviceKind: z
227
+ .enum(['platform-service', 'biome-api', 'cli'])
228
+ .optional(),
229
+ /** Registry names this API calls (→ descriptor.requiresServices + consumes). */
230
+ requiresServices: z.array(z.string().min(1)).optional(),
231
+ /** Capability refs this API exposes (→ descriptor.exposesCapabilities). */
232
+ exposesCapabilities: z.array(z.string().min(1)).optional(),
233
+ })
234
+ .strict();
235
+ export type BiomeApiDeclaration = z.infer<typeof BiomeApiDeclarationSchema>;
236
+
237
+ /**
238
+ * Phase-C Contribution Protocol entry. The body of an
239
+ * `*.contribution.json` file under a biome's `contributions/` directory,
240
+ * or an inline entry under `xema.contributions.inline[]` in
241
+ * `xema-biome.json`.
242
+ *
243
+ * Validated structurally here (kind + slug + opaque manifest). The
244
+ * authoritative per-kind manifest validation lives in the kind-specific
245
+ * contracts package and is exercised at boot by the generic
246
+ * `BootstrapContributionsService` template (see
247
+ * `bootstrap-contributions-service.ts`).
248
+ *
249
+ * See `.claude/plans/following-up-the-xema-os-plan-md-abstract-stream.md` §6.C.1.
250
+ */
251
+ export const BiomeContributionEntrySchema = z
252
+ .object({
253
+ kind: ContributionKindSchema,
254
+ /**
255
+ * Kind-local slug; uniqueness is per `(kind, biomeId)`. The full
256
+ * `<namespace>/<local-slug>` slug expected by the kernel
257
+ * `ContributionSlugSchema` is composed at install time by the biome
258
+ * host (`namespace = biomeId`).
259
+ */
260
+ id: z
261
+ .string()
262
+ .min(1)
263
+ .regex(
264
+ /^[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*$/,
265
+ 'Contribution id must be lowercase kebab/dot-case, optionally with nested `/` segments',
266
+ ),
267
+ manifest: z.unknown(),
268
+ })
269
+ .strict();
270
+ export type BiomeContributionEntry = z.infer<typeof BiomeContributionEntrySchema>;
271
+
272
+ /**
273
+ * Per-biome Contribution Protocol declaration. Either points at a
274
+ * `contributions/` directory containing one `*.contribution.json` per
275
+ * entry, OR carries the entries inline. Both forms are allowed in one
276
+ * manifest — directory entries are walked first, inline entries layered
277
+ * on top with last-write-wins on `(kind, id)` collision (the host fails
278
+ * fast on duplicates at boot).
279
+ *
280
+ * Phase C plan-of-record: §6.C.1.
281
+ */
282
+ export const BiomeContributionsSchema = z
283
+ .object({
284
+ /**
285
+ * Directory under the biome root (relative path). Defaults to
286
+ * `./contributions` when omitted but the block is present. The host
287
+ * silently ignores a missing directory — empty contribution sets are
288
+ * legal.
289
+ */
290
+ directory: z.string().min(1).optional(),
291
+ /** Inline contribution entries. */
292
+ inline: z.array(BiomeContributionEntrySchema).optional(),
293
+ })
294
+ .strict();
295
+ export type BiomeContributions = z.infer<typeof BiomeContributionsSchema>;
296
+
297
+ export const BiomeShipsSchema = z
298
+ .object({
299
+ /**
300
+ * @deprecated PHASE-C-SWEEP: superseded by top-level
301
+ * `xema.contributions`. Kept as a free-form string array until the
302
+ * codemod sweep migrates every biome to the Contribution Protocol.
303
+ * Values are matched against `ContributionKind` at registration time.
304
+ */
305
+ content: z.array(z.string().min(1)).optional(),
306
+ /**
307
+ * @deprecated PHASE-C-SWEEP: superseded by top-level
308
+ * `xema.contributions`. See `content` above — same rationale.
309
+ */
310
+ modules: z.array(z.string().min(1)).optional(),
311
+ apis: z.array(BiomeApiDeclarationSchema).optional(),
312
+ })
313
+ .strict();
314
+ export type BiomeShips = z.infer<typeof BiomeShipsSchema>;
315
+
316
+ /**
317
+ * Manifest-shape hosting/trust posture for a server biome. Used by
318
+ * integration-host plumbing (Phase 7) to decide whether to load
319
+ * adapter-contribution modules in-process or via a sidecar deployment.
320
+ * Manifests authored by Xema and audited third parties may set
321
+ * `first-party`; everything else defaults to `third-party`.
322
+ *
323
+ * Kept manifest-internal (not re-exported) so it does not collide with the
324
+ * kernel `BiomeTrustTier` provenance enum (`first-party | verified-store |
325
+ * community-store | …`) already exposed by this package.
326
+ */
327
+ const ManifestBiomeTrustTierSchema = z.enum(['first-party', 'third-party']);
328
+
329
+ /**
330
+ * AdapterKind slug — accepts a built-in slug verbatim, plus any
331
+ * lowercase kebab-case slug a biome-contributed kind could register.
332
+ * The runtime registry in integration-adapters-api is the authority on
333
+ * which slugs actually resolve; manifest parsing only enforces shape.
334
+ */
335
+ const AdapterKindRefSchema = z
336
+ .string()
337
+ .min(1)
338
+ .regex(/^[a-z][a-z0-9-]*$/, 'adapterKind must be lowercase kebab-case');
339
+
340
+ /**
341
+ * Execution mode for a shipped agent. Mirrors the `mode:` field in the
342
+ * agent .md frontmatter (and the `executionMode` column in
343
+ * llm-registry's `agent_definitions` table). Closed enum — every agent
344
+ * is either the session's primary or a delegate available via the
345
+ * `task` tool.
346
+ *
347
+ * The on-disk convention for backwards compatibility is that a missing
348
+ * frontmatter `mode:` is treated as `primary`. The manifest declaration
349
+ * is the authoritative answer; the cross-validator
350
+ * (`crossValidateAgentDeclarations`) accepts a declared `primary` against
351
+ * a missing frontmatter mode but rejects any other mismatch.
352
+ */
353
+ export const BiomeAgentModeSchema = z.enum(['primary', 'subagent']);
354
+ export type BiomeAgentMode = z.infer<typeof BiomeAgentModeSchema>;
355
+
356
+ /**
357
+ * Declaration of a single agent shipped by a biome. Path is implicit:
358
+ * `<biomeRoot>/agents/<slug>.md` (the convention enforced by
359
+ * `enumerateContentAcrossBiomeTree`). The slug also becomes the
360
+ * agent's canonical id in llm-registry, so it must match the filename.
361
+ *
362
+ * `mode` is duplicated between the manifest and the .md frontmatter on
363
+ * purpose: the manifest field makes the agent roster greppable and
364
+ * lets biome-host refuse to boot on drift; the frontmatter is what
365
+ * the agent-definition seeder consumes when materialising the agent
366
+ * into the registry. The boot-time cross-validator ensures both agree.
367
+ */
368
+ export const BiomeAgentDeclarationSchema = z
369
+ .object({
370
+ slug: z
371
+ .string()
372
+ .min(1)
373
+ .regex(
374
+ /^[a-z][a-z0-9-_]*$/,
375
+ 'agent slug must be lowercase kebab/underscore-case and match the .md basename',
376
+ ),
377
+ mode: BiomeAgentModeSchema,
378
+ })
379
+ .strict();
380
+ export type BiomeAgentDeclaration = z.infer<typeof BiomeAgentDeclarationSchema>;
381
+
382
+ const CapabilitySlugSchema = z
383
+ .string()
384
+ .min(1)
385
+ .regex(CAPABILITY_SLUG_PATTERN, 'capability slug must be <domain>.<verb-qualifier>');
386
+
387
+ /**
388
+ * Declared integration dependency. The install wizard surfaces these
389
+ * as a consent screen; `biome-host-api` refuses to activate a
390
+ * `BiomeInstallation` whose non-optional requirements are not bound.
391
+ * `purpose` is mandatory (one short sentence) so users see *why* the
392
+ * biome needs the integration.
393
+ */
394
+ export const IntegrationRequirementSchema = z
395
+ .object({
396
+ adapterKind: AdapterKindRefSchema,
397
+ optional: z.boolean().optional(),
398
+ purpose: z.string().min(1),
399
+ capabilities: z.array(CapabilitySlugSchema).min(1),
400
+ })
401
+ .strict();
402
+ export type IntegrationRequirement = z.infer<typeof IntegrationRequirementSchema>;
403
+
404
+ /**
405
+ * Per-(workflow, event) filter expression. The workflow YAML's
406
+ * `on.webhook[].event` value must match `event` here; the workflow
407
+ * compiler enforces this at publish time. `workflowId` is the
408
+ * shipped-workflow id (the `id` field inside the YAML), not a runtime
409
+ * `WorkflowDefinition.id`. At install time `biome-host-api` resolves
410
+ * the bundle and copies the relevant entries onto the
411
+ * `WorkflowDefinition.biomeWebhookFilters` column so the dispatcher
412
+ * can evaluate without re-reading the manifest.
413
+ */
414
+ export const WebhookFilterSchema = z
415
+ .object({
416
+ workflowId: z
417
+ .string()
418
+ .min(1)
419
+ .regex(/^[a-z][a-z0-9-]*$/, 'workflowId must be lowercase kebab-case'),
420
+ event: z.string().min(1),
421
+ /**
422
+ * Optional canonical entity kind the filter targets (e.g. `push`,
423
+ * `change_request`, `item`). When set, the manifest cross-validator
424
+ * narrows `$envelope.*` path checking to that entity kind's
425
+ * payload — typos against the specific kind fail fast at boot.
426
+ * Omitted = union path check across every entity kind under the
427
+ * referenced workflow's required adapter kinds (over-permissive
428
+ * but still catches the obvious typo class).
429
+ */
430
+ entityKind: z.string().min(1).optional(),
431
+ predicate: FilterExprSchema,
432
+ })
433
+ .strict();
434
+ export type WebhookFilter = z.infer<typeof WebhookFilterSchema>;
435
+
436
+ /**
437
+ * Server-side capability declarations. Validated at boot for shape +
438
+ * closed-set membership; runtime enforcement is Phase 4 (network
439
+ * allow-list, MCP ACL, secret scope). Declaring intent now means every
440
+ * biome already states it — no migration when enforcement turns on.
441
+ */
442
+ export const ServerBiomeCapabilitiesSchema = z
443
+ .object({
444
+ mcp: z.array(z.string().min(1)).optional(),
445
+ network: z
446
+ .object({ allowList: z.array(z.string().min(1)) })
447
+ .strict()
448
+ .optional(),
449
+ secrets: z.array(z.string().min(1)).optional(),
450
+ })
451
+ .strict();
452
+ export type ServerBiomeCapabilities = z.infer<typeof ServerBiomeCapabilitiesSchema>;
453
+
454
+ /** Web-side capability declarations. Same Phase-4 timing as server. */
455
+ export const WebBiomeCapabilitiesSchema = z
456
+ .object({
457
+ slots: z.array(z.string().min(1)).optional(),
458
+ apiClients: z.array(z.string().min(1)).optional(),
459
+ })
460
+ .strict();
461
+ export type WebBiomeCapabilities = z.infer<typeof WebBiomeCapabilitiesSchema>;
462
+
463
+ /**
464
+ * How the `provisioning-plan-resolver` expands one scaffold declaration
465
+ * into resolved steps. Closed enum — `each-app-target` evaluates
466
+ * `selector.configPointer` against the session's `customConfig` and emits
467
+ * one step per matched array element (webapp-studio's `/appTargets`).
468
+ * Adding a strategy is a one-line schema change here plus a matching
469
+ * branch in the resolver.
470
+ */
471
+ export const ProvisioningScaffoldMatchKindSchema = z.enum(['each-app-target']);
472
+ export type ProvisioningScaffoldMatchKind = z.infer<
473
+ typeof ProvisioningScaffoldMatchKindSchema
474
+ >;
475
+
476
+ /**
477
+ * Declaration of one provisioning scaffold a biome ships. The scaffold
478
+ * *content* (command spec + template files) lives at
479
+ * `<biomeRoot>/provisioning/<id>.yaml`; this manifest entry is the
480
+ * lightweight "I ship this scaffold, and here is when/whether it runs"
481
+ * declaration the resolver reads.
482
+ *
483
+ * `selector` is what makes provisioning "run only if instructed" — a
484
+ * scaffold whose `configPointer` resolves to nothing in the session's
485
+ * `customConfig` never enters the plan.
486
+ */
487
+ export const ProvisioningScaffoldDeclarationSchema = z
488
+ .object({
489
+ id: z
490
+ .string()
491
+ .min(1)
492
+ .regex(
493
+ /^[a-z][a-z0-9-]*$/,
494
+ 'scaffold id must be lowercase kebab-case and match the provisioning/<id>.yaml basename',
495
+ ),
496
+ kind: ProvisioningStepKindSchema,
497
+ triggers: z.array(ProvisioningTriggerSchema).min(1),
498
+ guard: ProvisioningGuardSchema,
499
+ selector: z
500
+ .object({
501
+ /**
502
+ * JSON Pointer (RFC 6901) into the session's `customConfig`. The
503
+ * resolver expands the scaffold only when this resolves; for
504
+ * `each-app-target` it must resolve to an array.
505
+ */
506
+ configPointer: z.string().min(1),
507
+ matchKind: ProvisioningScaffoldMatchKindSchema,
508
+ })
509
+ .strict(),
510
+ })
511
+ .strict();
512
+ export type ProvisioningScaffoldDeclaration = z.infer<
513
+ typeof ProvisioningScaffoldDeclarationSchema
514
+ >;
515
+
516
+ const BiomeIdSchema = z
517
+ .string()
518
+ .min(1)
519
+ .regex(/^[a-z][a-z0-9-]*$/, 'biome id must be kebab-case');
520
+
521
+ /**
522
+ * Per-handler declarative event subscription. Type-aliased to
523
+ * `BiomeEventSubscription` so the SDK's manifest field and the
524
+ * `xema-biome.json` parser share one shape. See plan §12.4.
525
+ */
526
+ export type BiomeManifestSubscribe = BiomeEventSubscription;
527
+
528
+ /**
529
+ * `requires`: biome id → semver range. Same shape as the kernel biome
530
+ * manifest's `requires` block (plan §12.4); the platform refuses to
531
+ * enable a biome/biome whose `requires` cannot be satisfied by the
532
+ * other enabled biomes/biomes in the org.
533
+ */
534
+ export type BiomeManifestRequires = Readonly<Record<string, string>>;
535
+
536
+ /**
537
+ * Additive XSI fields layered onto the per-target `xema` block in Phase
538
+ * 1A (plan §17.2 item 2). Pure additive. All fields are OPTIONAL.
539
+ *
540
+ * - `subscribes[]` — declarative event wiring (§12.4). Each entry binds
541
+ * one CloudEvents `type` to a handler module path.
542
+ * - `requires` — typed semver dependency declaration on other
543
+ * biomes/biomes (§12.4).
544
+ * - `contributes[]` — closed `ContributionKind` enum from
545
+ * `@xemahq/kernel-contracts/contribution`. The biome-host uses this to know
546
+ * which slot registries to walk on install (§12.1).
547
+ * - `requiresCapabilities[]` — capability refs the biome needs the host
548
+ * to grant before any of its handlers/contributions execute (§30.2).
549
+ * - `exposesCapabilities[]` — capability refs the biome itself
550
+ * implements and offers to others (§30.2).
551
+ * - `permissions` — install-time consent metadata (default profile +
552
+ * per-ref hints + UX groupings) for `requiresCapabilities[]` (§30.2).
553
+ * - `lifecycle` — module paths the host invokes on
554
+ * install/upgrade/enable/disable/uninstall transitions (§24.5).
555
+ * - `engines.xema` — semver range the host enforces against the running
556
+ * platform version (§24.4).
557
+ *
558
+ * Kernel-owned types are referenced through the sibling biome contract
559
+ * modules — never redefined here.
560
+ */
561
+ const XsiManifestExtensionsShape = {
562
+ subscribes: z.array(BiomeEventSubscriptionSchema).optional(),
93
563
  requires: z.record(z.string().min(1), z.string().min(1)).optional(),
564
+ contributes: z.array(ContributionKindSchema).optional(),
565
+ /**
566
+ * Phase-C Contribution Protocol entries (the actual envelopes, not just
567
+ * the kind whitelist). Replaces the legacy `ships.content` +
568
+ * `ships.modules` per-kind file-walk seeders with a single generic
569
+ * `BootstrapContributionsService` template; see Phase C.1 of the
570
+ * plan-of-record. Either inline entries or a pointer to a
571
+ * `contributions/` directory at the biome root.
572
+ */
573
+ contributions: BiomeContributionsSchema.optional(),
574
+ requiresCapabilities: z.array(CapabilityRefSchema).optional(),
575
+ exposesCapabilities: z.array(CapabilityRefSchema).optional(),
94
576
  permissions: BiomePermissionsManifestSchema.optional(),
95
577
  lifecycle: BiomeLifecycleHooksSchema.optional(),
96
- ships: z
97
- .object({
98
- apis: z.array(BiomeApiManifestSchema).readonly().optional(),
99
- content: z.array(z.string().min(1)).readonly().optional(),
100
- })
101
- .optional(),
102
- }) as z.ZodType<BiomeManifest>;
578
+ engines: BiomeEnginesSchema.optional(),
579
+ /**
580
+ * WHO the biome is for. `org` (default) — a normal biome available to
581
+ * organizations and eligible for the store. `operator` — Xema platform
582
+ * operators only: distribution selectors skip it (it ships only when
583
+ * explicitly named), it is never store-listed, hidden from org UIs, and
584
+ * gated to `platform_admin`. See `BiomeAudience` in `./biome-audience`.
585
+ */
586
+ audience: BiomeAudienceSchema.optional(),
587
+ /**
588
+ * Whether the biome appears in the store catalog. Defaults to `true` for
589
+ * `org`-audience biomes; an `operator` biome is NEVER store-listed
590
+ * regardless of this flag. Set `false` for a normal biome that should ship
591
+ * and be installable but stay out of the store (the lighter "hidden"
592
+ * option). Resolve via `isBiomeStoreListed` — never read raw.
593
+ */
594
+ storeListed: z.boolean().optional(),
595
+ } as const;
596
+
597
+ /**
598
+ * `runtimeRequirements` — what a biome NEEDS from the runtime/runner that
599
+ * executes it. This is the *requirements* half of the scheduling contract:
600
+ * the package states constraints; the installer decides who installs it;
601
+ * the runtime binding decides where it may run; the router schedules onto a
602
+ * matching healthy runner.
603
+ *
604
+ * HARD RULE — requirements, NEVER placement. A manifest MAY declare
605
+ * required/preferred labels, resource hints, allowed isolation levels,
606
+ * allowed data localities, and a minimum runner trust tier. It MUST NOT
607
+ * name a concrete target: `runtimeId`, `runnerId`, `runnerName`, `host`,
608
+ * `serverUrl`, or any infra address. Those are placement decisions owned by
609
+ * the scheduler, never the package. Enforced two ways: this object is
610
+ * `.strict()` (an unknown key — including any placement field — fails the
611
+ * parse fast) AND `tooling/boundaries/check-biome-no-placement.mjs` scans
612
+ * the raw manifest for forbidden keys anywhere in the tree with an
613
+ * actionable message.
614
+ *
615
+ * Enums are reused from `@xemahq/kernel-contracts/runner` so the requirement side
616
+ * (this manifest) and the capability side (a runtime/runner descriptor)
617
+ * speak the same closed vocabulary and can never drift.
618
+ */
619
+ const RuntimeRequirementsSchema = z
620
+ .object({
621
+ /**
622
+ * Capability/tag labels the executing runner must (`required`) or
623
+ * preferably should (`preferred`) advertise — e.g.
624
+ * `xema.runner/docker`, `xema.runner/gpu`, `region`. Matched against a
625
+ * runner's advertised `labels` by the router's selector.
626
+ */
627
+ labels: z
628
+ .object({
629
+ required: z.array(z.string().min(1)).optional(),
630
+ preferred: z.array(z.string().min(1)).optional(),
631
+ })
632
+ .strict()
633
+ .optional(),
634
+ /**
635
+ * Coarse resource hints (Kubernetes-style quantity strings). Advisory
636
+ * scheduling input, not a hard guarantee at this phase.
637
+ */
638
+ resources: z
639
+ .object({
640
+ cpu: z.string().min(1).optional(),
641
+ memory: z.string().min(1).optional(),
642
+ })
643
+ .strict()
644
+ .optional(),
645
+ /**
646
+ * Isolation boundaries the biome ACCEPTS. A runtime whose provided
647
+ * isolation is not in this set is ineligible. Omit to accept any.
648
+ */
649
+ isolation: z
650
+ .object({
651
+ allowed: z.array(RuntimeIsolationLevelSchema).min(1),
652
+ })
653
+ .strict()
654
+ .optional(),
655
+ /**
656
+ * Data localities the biome ACCEPTS (e.g. pin to `customer-private`).
657
+ * A runtime/runner whose locality is not in this set is ineligible.
658
+ */
659
+ locality: z
660
+ .object({
661
+ allowed: z.array(DataLocalitySchema).min(1),
662
+ })
663
+ .strict()
664
+ .optional(),
665
+ /**
666
+ * Minimum attestation trust tier the executing runner must meet.
667
+ */
668
+ trustTier: z
669
+ .object({
670
+ minimum: z.nativeEnum(RunnerTrustTier),
671
+ })
672
+ .strict()
673
+ .optional(),
674
+ })
675
+ .strict();
676
+
677
+ export type RuntimeRequirements = z.infer<typeof RuntimeRequirementsSchema>;
678
+
679
+ /**
680
+ * Optional presentation metadata a biome declares for host-shell surfaces
681
+ * (launchpad tiles, portal biome rails, the portal-admin biome picker). It is
682
+ * purely cosmetic — never consulted for resolution, enablement, or routing —
683
+ * so every field is optional and a host may fall back to its own defaults.
684
+ *
685
+ * - `icon` is a stable key the host resolves to a glyph (the closed set of
686
+ * renderable keys lives in the host shell, not here, so new biomes are never
687
+ * blocked by an enum bump).
688
+ * - `category` groups biomes in the picker/rail (e.g. "Finance"). It is also
689
+ * the seed source for distribution default portals.
690
+ * - `summary` is a short one-liner for tiles, distinct from the longer
691
+ * freeform `description`.
692
+ * - `accent` is an optional CSS color token for the biome's glyph/card.
693
+ */
694
+ const BiomeDisplaySchema = z
695
+ .object({
696
+ icon: z.string().min(1).optional(),
697
+ category: z.string().min(1).optional(),
698
+ summary: z.string().min(1).optional(),
699
+ accent: z.string().min(1).optional(),
700
+ })
701
+ .strict();
702
+
703
+ export type BiomeDisplay = z.infer<typeof BiomeDisplaySchema>;
704
+
705
+ const ServerBiomeXemaSchema = z
706
+ .object({
707
+ id: BiomeIdSchema,
708
+ displayName: z.string().min(1),
709
+ description: z.string().optional(),
710
+ display: BiomeDisplaySchema.optional(),
711
+ scope: ManifestBiomeScopeSchema,
712
+ target: z.literal('server'),
713
+ /**
714
+ * What this biome needs from the runtime/runner that executes it
715
+ * (labels, resources, isolation, locality, trust). Requirements only —
716
+ * a manifest may NOT name a concrete runtime/runner/host (see
717
+ * {@link RuntimeRequirementsSchema}). Server biomes only; web biomes are
718
+ * static bundles served by the host shell and never run on a runner.
719
+ */
720
+ runtimeRequirements: RuntimeRequirementsSchema.optional(),
721
+ /**
722
+ * Server biome ids this biome depends on. Biome-host topologically
723
+ * sorts the server set at boot and fails fast on cycles or unsatisfied
724
+ * dependencies. A kernel biome may only depend on other kernel
725
+ * biomes (enforced by biome-host).
726
+ */
727
+ dependencies: z.array(BiomeIdSchema).optional(),
728
+ /**
729
+ * Biome id(s) this biome EXTENDS. An extending biome is the
730
+ * same-or-upgraded variant of the biome(s) it extends and MAY re-ship
731
+ * their content (agent slugs, workspace manifests, skills); when both
732
+ * are installed the extending biome's version takes precedence. This is
733
+ * the machine-readable form of the "the commercial cultivar extends this
734
+ * community reference biome" relationship — e.g. `document-buddy`
735
+ * (platform cultivar) extends `document-buddy-reference` (community
736
+ * reference). Distinct from `dependencies`: an extended biome need NOT be
737
+ * installed (the extension is self-contained), whereas a dependency must
738
+ * resolve at boot. `extends` is what lets the agent-slug uniqueness
739
+ * invariant tolerate the re-shipped slug — see
740
+ * tooling/boundaries/check-agent-duplication.mjs.
741
+ */
742
+ extends: z.union([BiomeIdSchema, z.array(BiomeIdSchema)]).optional(),
743
+ ships: BiomeShipsSchema.optional(),
744
+ capabilities: ServerBiomeCapabilitiesSchema.optional(),
745
+ /**
746
+ * Hosting posture for adapter-contribution modules (Phase 7). Other
747
+ * biome kinds ignore this field. Defaults to `third-party`.
748
+ */
749
+ trustTier: ManifestBiomeTrustTierSchema.optional(),
750
+ /**
751
+ * Integration dependencies the biome needs to be useful. Surfaced
752
+ * to the install wizard as a consent screen. Non-optional entries
753
+ * gate the `BiomeInstallation` state transition to `active`.
754
+ */
755
+ integrationRequirements: z.array(IntegrationRequirementSchema).optional(),
756
+ /**
757
+ * Per-(workflow, event) filter expressions evaluated by the
758
+ * workflow-engine dispatcher at webhook time. Each entry MUST
759
+ * resolve to a workflow shipped by this biome and an event that
760
+ * workflow declares under `on.webhook[].event`. The DSL compiler
761
+ * cross-validates at publish time; manifest parsing only enforces
762
+ * shape.
763
+ */
764
+ webhookFilters: z.array(WebhookFilterSchema).optional(),
765
+ /**
766
+ * Workflows the biome wants exposed as MCP tools. The MCP gateway
767
+ * (`mcp-gateway-api`) projects each entry into the catalog-mode
768
+ * tool listing surfaced to agents via the agent runtime. When an agent
769
+ * invokes the tool, the gateway dispatches the workflow run
770
+ * (auto-attaching the calling installation's id, JWT-derived org
771
+ * + actor context) and returns the named deliverable plus an
772
+ * optional workspace mount entry so the result lands on disk
773
+ * automatically.
774
+ *
775
+ * Each `key` is unique within the biome and becomes the MCP tool
776
+ * id (`<biomeId>__<key>`). `workflowSlug` MUST resolve to a
777
+ * workflow this biome ships; the workflow-engine cross-validates
778
+ * at publish time. `outputProjection.deliverable.slug` picks one
779
+ * of the workflow's declared `produces:` entries to surface as the
780
+ * tool's primary content payload. The optional `mount` block
781
+ * issues a `POST /workspace/mounts/apply` on the calling agent's
782
+ * session so the result file appears under `/workspace/<slot>/...`
783
+ * — seamless from the agent's point of view.
784
+ */
785
+ mcpWorkflowTools: z
786
+ .array(
787
+ z
788
+ .object({
789
+ key: z
790
+ .string()
791
+ .min(1)
792
+ .regex(/^[a-z][a-z0-9-]*$/, 'key must be lowercase-kebab'),
793
+ workflowSlug: z.string().min(1),
794
+ displayName: z.string().min(1),
795
+ description: z.string().min(1),
796
+ outputProjection: z
797
+ .object({
798
+ kind: z.literal('deliverable'),
799
+ slug: z.string().min(1),
800
+ })
801
+ .strict(),
802
+ mount: z
803
+ .object({
804
+ slot: z.enum(['inputs', 'references', 'deliverables']),
805
+ as: z.string().min(1),
806
+ })
807
+ .strict()
808
+ .optional(),
809
+ })
810
+ .strict(),
811
+ )
812
+ .optional(),
813
+ /**
814
+ * Plain handler functions the biome's API service exposes as MCP
815
+ * tools. The platform wraps them via the `biome_code_tools`
816
+ * provider — biome authors never see MCP protocol bytes.
817
+ *
818
+ * Each entry's `key` is unique within the biome and maps to a
819
+ * decorated method on the biome's API service via the
820
+ * `@BiomeCodeTool` decorator from `@xemahq/biome-api-sdk`. The
821
+ * full descriptor (inputSchema, displayName, output kind) is
822
+ * authoritative on the biome API side and surfaced through
823
+ * `GET /code-tools-manifest`; this manifest entry is the
824
+ * lightweight "I ship this tool" declaration the platform reads
825
+ * at install time.
826
+ */
827
+ mcpTools: z
828
+ .array(
829
+ z
830
+ .object({
831
+ key: z
832
+ .string()
833
+ .min(1)
834
+ .regex(/^[a-z][a-z0-9-]*$/, 'key must be lowercase-kebab'),
835
+ handler: z
836
+ .object({
837
+ kind: z.literal('biome_api'),
838
+ method: z.enum(['POST']).default('POST'),
839
+ })
840
+ .strict(),
841
+ })
842
+ .strict(),
843
+ )
844
+ .optional(),
845
+ /**
846
+ * Tools the biome recommends are made available when a user
847
+ * adopts it. Surfaced in the install UX as "this biome recommends
848
+ * these tools" — NOT auto-injected into every session. Each entry
849
+ * matches the kernel `ToolSelectionEntry` shape from
850
+ * `@xemahq/kernel-contracts/mcp-tool`. The frontend picker drops these
851
+ * directly into the session's selection on user opt-in.
852
+ *
853
+ * Validation: shape-only at boot. `resourceId` references resolve
854
+ * at install time (e.g. biome-installation-id is materialized by
855
+ * biome-host-api when the installation is created). Cross-tenant
856
+ * misuse is blocked downstream by the resolver (Layer 2) and the
857
+ * PATCH-time validator (Layer 1) when the user actually adopts the
858
+ * selection.
859
+ */
860
+ defaultToolSelection: z
861
+ .array(
862
+ z
863
+ .discriminatedUnion('kind', [
864
+ z
865
+ .object({
866
+ kind: z.literal('provider'),
867
+ providerKind: z.enum([
868
+ 'mcp_server',
869
+ 'catalog',
870
+ 'biome_workflow_tools',
871
+ 'biome_code_tools',
872
+ ]),
873
+ resourceId: z.string().min(1).max(256),
874
+ })
875
+ .strict(),
876
+ z
877
+ .object({
878
+ kind: z.literal('tool'),
879
+ providerKind: z.enum([
880
+ 'mcp_server',
881
+ 'catalog',
882
+ 'biome_workflow_tools',
883
+ 'biome_code_tools',
884
+ ]),
885
+ resourceId: z.string().min(1).max(256),
886
+ toolName: z.string().min(1).max(256),
887
+ })
888
+ .strict(),
889
+ ]),
890
+ )
891
+ .max(64)
892
+ .optional(),
893
+ /**
894
+ * Explicit roster of agents this biome ships. Required when
895
+ * `ships.content` includes `'agents'`; forbidden otherwise. Each
896
+ * entry pins one `agents/<slug>.md` file with its execution mode
897
+ * (primary vs subagent).
898
+ *
899
+ * Why both this field AND the on-disk `agents/` scan exist:
900
+ * - The declaration makes the roster greppable from outside the
901
+ * biome tree (`grep '"slug": "build"' biomes/*\/xema-biome.json`).
902
+ * - It lets biome-host fail fast at boot when an agent file is
903
+ * added or removed without updating the manifest — the prior
904
+ * convention silently accepted drift.
905
+ * - The closed `mode` enum surfaces primary-vs-subagent intent at
906
+ * install time instead of requiring readers to parse YAML
907
+ * frontmatter from every .md.
908
+ *
909
+ * Cross-validated by `crossValidateAgentDeclarations` at
910
+ * biome-host boot; the seeder still reads frontmatter for the rest
911
+ * of each agent's attributes.
912
+ */
913
+ agents: z.array(BiomeAgentDeclarationSchema).optional(),
914
+ /**
915
+ * Workspace-provisioning scaffolds the biome ships (Epic A). Each
916
+ * entry pins one `provisioning/<id>.yaml` recipe and declares when it
917
+ * runs + how the resolver expands it. Required ⟺ `ships.content`
918
+ * includes `provisioningScaffolds`; cross-validated in `superRefine`.
919
+ */
920
+ provisioning: z.array(ProvisioningScaffoldDeclarationSchema).optional(),
921
+ /**
922
+ * Biome-specific org-managed database declaration. When present the
923
+ * host provisions an org database, runs the declared migrations, and
924
+ * injects the org database identifiers into the runtime workload env.
925
+ */
926
+ database: BiomeDatabaseDeclarationSchema.optional(),
927
+ /** Reserved for Phase 4 third-party signing. Validated only when present. */
928
+ signature: z
929
+ .object({
930
+ algorithm: z.string().min(1),
931
+ value: z.string().min(1),
932
+ keyId: z.string().min(1),
933
+ })
934
+ .strict()
935
+ .optional(),
936
+ // XSI Phase 1A additive fields. See `XsiManifestExtensionsShape`
937
+ // doc-comment above for the per-field meaning + plan citations.
938
+ ...XsiManifestExtensionsShape,
939
+ })
940
+ .strict()
941
+ .superRefine((value, ctx) => {
942
+ validateServerPluginWebhookAndIntegrationRequirements(value, ctx);
943
+ validateServerBiomeAgentDeclarations(value, ctx);
944
+ validateServerPluginProvisioningDeclarations(value, ctx);
945
+ });
946
+
947
+ const WebBiomeXemaSchema = z
948
+ .object({
949
+ id: BiomeIdSchema,
950
+ displayName: z.string().min(1),
951
+ description: z.string().optional(),
952
+ display: BiomeDisplaySchema.optional(),
953
+ scope: ManifestBiomeScopeSchema,
954
+ target: z.literal('web'),
955
+ /**
956
+ * A platform/system web surface that renders in the org rail rather
957
+ * than the store. Defaults to `false`. A `systemSurface` web biome is a
958
+ * first-party operational surface (e.g. an admin/console panel) that the
959
+ * host shell mounts in the organization navigation rail; it is NOT a
960
+ * store-listed, org-installable biome. Resolve via `isSystemSurface` —
961
+ * never read raw.
962
+ */
963
+ systemSurface: z.boolean().optional(),
964
+ /**
965
+ * Server biomes this web biome REQUIRES — its backend counterpart(s).
966
+ * Web biomes never depend on other web biomes — intentional, keeps the
967
+ * dependency graph two-tiered (web → server). Cross-target validation at
968
+ * boot ensures every referenced server id resolves in the catalog;
969
+ * otherwise biome-host refuses to start. Per-org, a web biome whose
970
+ * required server biomes are not all enabled resolves to ORPHANED.
971
+ */
972
+ requiresServerBiomes: z.array(BiomeIdSchema).optional(),
973
+ /**
974
+ * Server biomes this web biome can OPTIONALLY integrate with (e.g. a
975
+ * GitHub/Jira connector). The web biome stays active without them; they
976
+ * are NOT part of the cross-target boot validation and never cause an
977
+ * ORPHANED state.
978
+ */
979
+ optionalServerBiomes: z.array(BiomeIdSchema).optional(),
980
+ capabilities: WebBiomeCapabilitiesSchema.optional(),
981
+ signature: z
982
+ .object({
983
+ algorithm: z.string().min(1),
984
+ value: z.string().min(1),
985
+ keyId: z.string().min(1),
986
+ })
987
+ .strict()
988
+ .optional(),
989
+ // XSI Phase 1A additive fields — same shape on both targets so the
990
+ // biome-host can read them uniformly regardless of where the biome
991
+ // executes. See `XsiManifestExtensionsShape` above.
992
+ ...XsiManifestExtensionsShape,
993
+ })
994
+ .strict();
995
+
996
+ export const BiomeXemaSchema = z.discriminatedUnion('target', [
997
+ ServerBiomeXemaSchema,
998
+ WebBiomeXemaSchema,
999
+ ]);
1000
+ export type BiomeXema = z.infer<typeof BiomeXemaSchema>;
1001
+ export type ServerBiomeXema = z.infer<typeof ServerBiomeXemaSchema>;
1002
+ export type WebBiomeXema = z.infer<typeof WebBiomeXemaSchema>;
1003
+
1004
+ function validateServerPluginWebhookAndIntegrationRequirements(
1005
+ value: ServerBiomeXema,
1006
+ ctx: z.RefinementCtx,
1007
+ ): void {
1008
+ if (value.webhookFilters && value.webhookFilters.length > 0) {
1009
+ const requirements = value.integrationRequirements ?? [];
1010
+ if (requirements.length === 0) {
1011
+ ctx.addIssue({
1012
+ code: 'custom',
1013
+ path: ['webhookFilters'],
1014
+ message:
1015
+ 'webhookFilters is set but integrationRequirements is empty — a biome gating workflows on webhook events must declare which integration(s) it requires',
1016
+ });
1017
+ }
1018
+ }
1019
+ if (value.integrationRequirements && value.integrationRequirements.length > 0) {
1020
+ const shipsInstallSchema = value.ships?.content?.includes('biomeInstallSchema') ?? false;
1021
+ if (!shipsInstallSchema) {
1022
+ ctx.addIssue({
1023
+ code: 'custom',
1024
+ path: ['ships', 'content'],
1025
+ message:
1026
+ 'biomes declaring integrationRequirements must also ship a `biomeInstallSchema` content kind so the install wizard can render a resource-selection form',
1027
+ });
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ function validateServerBiomeAgentDeclarations(
1033
+ value: ServerBiomeXema,
1034
+ ctx: z.RefinementCtx,
1035
+ ): void {
1036
+ const shipsAgents = value.ships?.content?.includes('agents') ?? false;
1037
+ const hasAgents = (value.agents?.length ?? 0) > 0;
1038
+ if (shipsAgents && !hasAgents) {
1039
+ ctx.addIssue({
1040
+ code: 'custom',
1041
+ path: ['agents'],
1042
+ message:
1043
+ 'ships.content includes "agents" — manifest must declare each shipped agent under `agents[]` (slug + mode). The boot-time cross-validator enforces parity with on-disk agents/*.md files.',
1044
+ });
1045
+ }
1046
+ if (!shipsAgents && hasAgents) {
1047
+ ctx.addIssue({
1048
+ code: 'custom',
1049
+ path: ['agents'],
1050
+ message:
1051
+ 'agents[] is declared but ships.content does not include "agents" — add "agents" to ships.content or remove the agents[] block',
1052
+ });
1053
+ }
1054
+ if (hasAgents) {
1055
+ const slugs = new Set<string>();
1056
+ value.agents?.forEach((decl, idx) => {
1057
+ if (slugs.has(decl.slug)) {
1058
+ ctx.addIssue({
1059
+ code: 'custom',
1060
+ path: ['agents', idx, 'slug'],
1061
+ message: `duplicate agent slug "${decl.slug}" — each slug maps to a single agents/<slug>.md file`,
1062
+ });
1063
+ }
1064
+ slugs.add(decl.slug);
1065
+ });
1066
+ }
1067
+ }
1068
+
1069
+ function validateServerPluginProvisioningDeclarations(
1070
+ value: ServerBiomeXema,
1071
+ ctx: z.RefinementCtx,
1072
+ ): void {
1073
+ const shipsProvisioning = value.ships?.content?.includes('provisioningScaffolds') ?? false;
1074
+ const hasProvisioning = (value.provisioning?.length ?? 0) > 0;
1075
+ if (shipsProvisioning && !hasProvisioning) {
1076
+ ctx.addIssue({
1077
+ code: 'custom',
1078
+ path: ['provisioning'],
1079
+ message:
1080
+ 'ships.content includes "provisioningScaffolds" — manifest must declare each scaffold under `provisioning[]`',
1081
+ });
1082
+ }
1083
+ if (!shipsProvisioning && hasProvisioning) {
1084
+ ctx.addIssue({
1085
+ code: 'custom',
1086
+ path: ['provisioning'],
1087
+ message:
1088
+ 'provisioning[] is declared but ships.content does not include "provisioningScaffolds" — add it or remove the provisioning[] block',
1089
+ });
1090
+ }
1091
+ }
1092
+
1093
+ const ManifestNameSchema = z
1094
+ .string()
1095
+ .min(1)
1096
+ .regex(/^@[^/]+\/[^/]+$/, 'biome package name must be scoped');
1097
+
1098
+ export const BiomeManifestSchema = z
1099
+ .object({
1100
+ name: ManifestNameSchema,
1101
+ version: z.string().min(1),
1102
+ xema: BiomeXemaSchema,
1103
+ })
1104
+ .passthrough(); // tolerate extra top-level npm fields
1105
+
1106
+ export type BiomeManifest = z.infer<typeof BiomeManifestSchema>;
1107
+ export type ServerBiomeManifest = BiomeManifest & { xema: ServerBiomeXema };
1108
+ export type WebBiomeManifest = BiomeManifest & { xema: WebBiomeXema };
1109
+
1110
+ export class BiomeManifestParseError extends Error {
1111
+ readonly issues: readonly z.core.$ZodIssue[];
1112
+ constructor(issues: readonly z.core.$ZodIssue[]) {
1113
+ super(
1114
+ `Invalid xema-biome.json: ${issues
1115
+ .map((i) => `[${i.path.join('.')}] ${i.message}`)
1116
+ .join('; ')}`,
1117
+ );
1118
+ this.name = 'BiomeManifestParseError';
1119
+ this.issues = issues;
1120
+ }
1121
+ }
1122
+
1123
+ export function parseBiomeManifest(raw: unknown): BiomeManifest {
1124
+ const result = BiomeManifestSchema.safeParse(raw);
1125
+ if (!result.success) {
1126
+ throw new BiomeManifestParseError(result.error.issues);
1127
+ }
1128
+ return result.data;
1129
+ }
1130
+
1131
+ export function isServerBiomeManifest(
1132
+ manifest: BiomeManifest,
1133
+ ): manifest is ServerBiomeManifest {
1134
+ return manifest.xema.target === 'server';
1135
+ }
1136
+
1137
+ export function isWebBiomeManifest(
1138
+ manifest: BiomeManifest,
1139
+ ): manifest is WebBiomeManifest {
1140
+ return manifest.xema.target === 'web';
1141
+ }
1142
+
1143
+ /**
1144
+ * Whether a web biome is a platform/system web surface that renders in the
1145
+ * org rail rather than the store. Applies the default (`false`) when the
1146
+ * field is omitted, and is always `false` for server biomes (the field is
1147
+ * web-only). Never read `xema.systemSurface` raw — go through this so the
1148
+ * default is applied in exactly one place.
1149
+ */
1150
+ export function isSystemSurface(manifest: BiomeManifest): boolean {
1151
+ if (manifest.xema.target !== 'web') {
1152
+ return false;
1153
+ }
1154
+ return manifest.xema.systemSurface ?? false;
1155
+ }
1156
+
1157
+ /**
1158
+ * Effective audience of a biome, applying the default (`org`) when the
1159
+ * manifest omits the field. Never read `xema.audience` raw — go through this so
1160
+ * the default is applied in exactly one place.
1161
+ */
1162
+ export function getBiomeAudience(manifest: BiomeManifest): BiomeAudience {
1163
+ return manifest.xema.audience ?? DEFAULT_BIOME_AUDIENCE;
1164
+ }
1165
+
1166
+ /**
1167
+ * Whether the biome should appear in the store catalog. An `operator`-audience
1168
+ * biome is NEVER store-listed (hidden from orgs by definition); otherwise the
1169
+ * manifest `storeListed` flag wins, defaulting to `true`.
1170
+ */
1171
+ export function isBiomeStoreListed(manifest: BiomeManifest): boolean {
1172
+ if (getBiomeAudience(manifest) === BiomeAudience.Operator) {
1173
+ return false;
1174
+ }
1175
+ return manifest.xema.storeListed ?? true;
1176
+ }