@xemahq/agent-session-runtime 0.11.1 → 0.11.2

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.
@@ -1,632 +0,0 @@
1
- // ═══════════════════════════════════════════════════════════════════════════
2
- // ── Agent → CompiledWorkspaceManifest reconstruction ──
3
- //
4
- // The composer turns a
5
- // `CompiledWorkspaceManifest` into a `WorkspaceMountPlan`; before this
6
- // module the only way to obtain that compiled manifest was to fetch the
7
- // `WorkspaceManifest` row from `workspace-manifests-api` and run
8
- // `compileManifest` on it.
9
- //
10
- // The Agent primitive now carries the user-data MOUNT LAYOUT
11
- // on its `workspace.mountLayout` block: the boot-seeder in `llm-registry-api`
12
- // projects each biome manifest's `extends:`-flattened RAW `spec.mounts` /
13
- // `spec.seedFiles` / `spec.inputs` there. This module reconstructs a
14
- // `WorkspaceManifest` envelope from a resolved composition + its
15
- // `mountLayout`, then runs the SAME `compileManifest` the legacy path used
16
- // — with the REAL run/session bind inputs, so `${input.x}` tokens in
17
- // seed-file vars resolve against the caller's inputs, never against a
18
- // seed-time placeholder.
19
- //
20
- // The result is a `CompiledWorkspaceManifest` indistinguishable from the
21
- // legacy `workspace-manifests-api`-sourced one — the composer interface is
22
- // unchanged.
23
- //
24
- // This module is RUNTIME-AGNOSTIC: it does not import a generated API
25
- // client. The caller (`agent-session-api` today, `workflow-runtime-worker`
26
- // next) maps its resolved-composition shape onto the small input
27
- // interfaces declared here and supplies the bind-input bag.
28
- // ═══════════════════════════════════════════════════════════════════════════
29
-
30
- import {
31
- ManifestSurface,
32
- OutputSurfaceKind,
33
- type AgentRunRole,
34
- type CompiledManifestCredential,
35
- type CompiledManifestPermissions,
36
- type ModelRef,
37
- } from '@xemahq/kernel-contracts/workflow';
38
- import type { PermissionMap } from '@xemahq/kernel-contracts/agent-permission';
39
- import {
40
- compileManifest,
41
- type CompiledWorkspaceManifest,
42
- type ManifestCredential,
43
- type ManifestInputDeclaration,
44
- type ManifestMountsBlock,
45
- type ManifestOutputSurface,
46
- type ManifestPermissions,
47
- type ManifestSeedFile,
48
- type ManifestSubAgent,
49
- type ManifestWorkingFile,
50
- type AgentWorkspaceSpec as DslWorkspaceManifest,
51
- type WorkspaceManifestSpec,
52
- } from '@xemahq/dsl/workspace-manifest';
53
-
54
- /**
55
- * Re-export so callers that consume this module's `compile…` result do not
56
- * need a second import from `@xemahq/dsl/workspace-manifest`.
57
- */
58
- export type { CompiledWorkspaceManifest };
59
-
60
- /**
61
- * Raised when a resolved composition cannot be reconstructed into a
62
- * compilable workspace manifest — a missing `mountLayout`, a malformed
63
- * composition ref, or a `compileManifest` failure. Fail-fast: a session /
64
- * run cannot bootstrap a `/workspace/` tree without a valid mount layout.
65
- */
66
- export class AgentMountLayoutError extends Error {
67
- constructor(agentRef: string, detail: string) {
68
- super(
69
- `Cannot derive a workspace mount layout from Agent ` +
70
- `"${agentRef}": ${detail}`,
71
- );
72
- this.name = 'AgentMountLayoutError';
73
- }
74
- }
75
-
76
- /**
77
- * The agent run-config the seeder projects from the source manifest's
78
- * `spec.agent` block. Mirrors `AgentRunConfigDto` without
79
- * importing the generated client.
80
- */
81
- export interface AgentRunConfigInput {
82
- /** Stage key. */
83
- readonly stage: string;
84
- /** Canonical `AgentRunRole` — drives renderer + system-overlay framing. */
85
- readonly role: string;
86
- /**
87
- * Deliverable-spec ref. May carry a `${input.x}` token — resolved
88
- * downstream against the bind inputs.
89
- */
90
- readonly deliverableSpecRef?: string;
91
- }
92
-
93
- /**
94
- * The `extends:`-flattened RAW user-data mount layout carried on a
95
- * composition's `workspace.mountLayout` block. The seeder owns the wire
96
- * shape; `compileManifest`'s Zod schema re-validates `mounts` / `seedFiles`
97
- * / `inputs` against the manifest DSL's authoritative schema, so the loose
98
- * typing here is checked downstream, not silently trusted.
99
- */
100
- export interface AgentMountLayoutInput {
101
- /** Raw `spec.mounts` block — manifest DSL mount-declaration map. */
102
- readonly mounts: Readonly<Record<string, unknown>>;
103
- /** Projected `spec.agent` run-config. */
104
- readonly agentRunConfig: AgentRunConfigInput;
105
- /** Raw `spec.seedFiles` array — manifest DSL seed-file entries. */
106
- readonly seedFiles: readonly unknown[];
107
- /** Raw `spec.inputs` block — manifest DSL input-declaration map. */
108
- readonly inputs: Readonly<Record<string, unknown>>;
109
- }
110
-
111
- /**
112
- * A single manifest-declared sub-agent binding the composition resolved.
113
- * Mirrors the binding subset the manifest DSL's `ManifestSubAgent` needs.
114
- *
115
- * Carries the {@link NodeOverlayFragment} overlay trio (`modelOverride` /
116
- * `instructions` / `permission`) so a resolved sub-agent's per-node
117
- * specialization survives the manifest reconstruction. Without these the
118
- * worker silently DROPPED a delegate's `modelOverride` (the root fix for the
119
- * worker drift). All optional ⇒ backward-compatible with callers that only
120
- * supply `slug` / `alias`.
121
- */
122
- export interface AgentSubAgentBindingInput {
123
- readonly slug: string;
124
- readonly alias?: string;
125
- /** Per-delegate model override — OVERRIDE on the referenced agent's model. */
126
- readonly modelOverride?: ModelRef;
127
- /** Per-delegate instructions — APPENDED to the referenced agent's prompt. */
128
- readonly instructions?: string;
129
- /** Per-delegate permission — RESTRICT-merged over the agent's permission. */
130
- readonly permission?: PermissionMap;
131
- }
132
-
133
- /**
134
- * The composition's `workspace.outputSurface` block projected onto the
135
- * shape the manifest DSL accepts. Mirrors `ManifestOutputSurface` from
136
- * `@xemahq/dsl/workspace-manifest` but restated here so callers do not
137
- * need a second import.
138
- *
139
- * Carried through the reconstructed manifest's `spec.outputSurface` so
140
- * the compiled output's `CompiledManifestOutputSurface` matches the
141
- * authoritative source — drift detection and runtime defaults read the
142
- * same kind/root/port the bundle-apply path reads from
143
- * `composition.workspace.outputSurface`.
144
- */
145
- export interface AgentOutputSurfaceInput {
146
- readonly kind: 'none' | 'web' | 'static' | 'app' | 'tunnel';
147
- readonly port?: number;
148
- readonly healthPath?: string;
149
- readonly autoOpen?: boolean;
150
- readonly mode?: 'single' | 'multi';
151
- readonly root?: string;
152
- readonly defaultDocument?: string;
153
- }
154
-
155
- /**
156
- * The minimal resolved-composition projection this module reconstructs a
157
- * `WorkspaceManifest` from. The caller maps its richer resolved-composition
158
- * shape onto this — keeping the runtime package free of any API client.
159
- */
160
- export interface AgentManifestSource {
161
- /** Version-pinned `slug@version` of the resolved composition. */
162
- readonly agentRef: string;
163
- /** Root node's agent slug — the manifest's primary agent. */
164
- readonly primaryAgentSlug: string;
165
- /** Manifest-declared sub-agent bindings (descendant nodes). */
166
- readonly subAgentBindings: readonly AgentSubAgentBindingInput[];
167
- /**
168
- * The composition's `workspace.mountLayout` block. `undefined` when the
169
- * composition declared no `workspace` block or the seeder did not project
170
- * a layout — a fail-fast condition.
171
- */
172
- readonly mountLayout: AgentMountLayoutInput | undefined;
173
- /**
174
- * The composition's `workspace.outputSurface` block. Threaded into the
175
- * reconstructed manifest's `spec.outputSurface` so the compiled
176
- * manifest's `outputSurface` matches the source-of-truth (drift
177
- * detection + non-bundle consumers like `workflow-runtime-worker` read
178
- * the same value the bundle-apply path reads). `undefined` when the
179
- * composition declared no surface — the DSL compile then yields the
180
- * `{ kind: 'none' }` default.
181
- */
182
- readonly outputSurface?: AgentOutputSurfaceInput;
183
- /**
184
- * The composition's `workspace.credentials` block. Threaded into the
185
- * reconstructed manifest's `spec.credentials` so the compiled manifest's
186
- * `credentials` matches the source-of-truth — the EnvironmentResolver
187
- * reads the same `{name, kind, sourceRef, required}` entries the
188
- * authored composition declared (BUG-1 carry-through: authored
189
- * `spec.credentials` were silently dropped on reconstruction). Carried
190
- * as the kernel `CompiledManifestCredential[]` because that is the shape
191
- * `composition.workspace.credentials` holds (the seeder projects the
192
- * authored entries through the manifest compiler). `undefined` when the
193
- * composition declared none — the DSL compile then yields `[]`.
194
- */
195
- readonly credentials?: readonly CompiledManifestCredential[];
196
- /**
197
- * The composition's `workspace.permissions` block. Threaded into the
198
- * reconstructed manifest's `spec.permissions` so the compiled manifest's
199
- * `permissions` matches the source-of-truth — the EnvironmentResolver
200
- * reads the same `{tools: {allow, deny}}` the authored composition
201
- * declared (BUG-1 carry-through: authored `spec.permissions` were
202
- * silently dropped on reconstruction). Carried as the kernel
203
- * `CompiledManifestPermissions` because that is the shape
204
- * `composition.workspace.permissions` holds. `undefined` when the
205
- * composition declared none — the DSL compile then yields
206
- * `{ tools: { allow: [], deny: [] } }`.
207
- */
208
- readonly permissions?: CompiledManifestPermissions;
209
- /**
210
- * The composition's `workspace.workingFiles` block. Threaded into the
211
- * reconstructed manifest's `spec.workingFiles` so the compiled manifest —
212
- * and therefore `context.json.workingFiles` + the write authority on
213
- * `context.json.authority.mayWriteWorkspace[]` — matches the
214
- * source-of-truth. Carried as the DSL `ManifestWorkingFile[]` (raw, with
215
- * `${input.x}` tokens intact); `compileManifest` interpolates them against
216
- * the bind inputs, exactly like the bundle-apply path's `toBundleWorkingFiles`.
217
- * `undefined` when the composition declared none — the DSL compile then
218
- * yields `[]`. (BUG-2 carry-through: authored `spec.workingFiles` were
219
- * silently dropped on reconstruction, so a Document Buddy session came up
220
- * with NO bound working file — the agent read `context.json`, saw an empty
221
- * `workingFiles[]`, and asked the user where to write instead of editing the
222
- * bound page. Symmetric with the credentials/permissions BUG-1 fix above.)
223
- */
224
- readonly workingFiles?: readonly ManifestWorkingFile[];
225
- }
226
-
227
- /**
228
- * The implicit inputs every reconstructed manifest is bound against, on top
229
- * of the caller-supplied explicit inputs.
230
- *
231
- * `sessionId` is the run-scoped execution identity. The agent-session-api
232
- * caller supplies the `Session.id`; the workflow-runtime-worker caller —
233
- * which has no session — supplies the workflow run id. Both are
234
- * run-scoped; only manifests that declare a `sessionId` input ever read
235
- * it, and no first-party manifest does today. Optional so a caller with
236
- * no run-scoped id at all can omit it.
237
- */
238
- export interface AgentManifestImplicitInputs {
239
- readonly orgId: string;
240
- readonly projectId: string;
241
- readonly sessionId?: string;
242
- }
243
-
244
- /** Manifest input value primitives accepted by the manifest DSL. */
245
- type ManifestInputValue =
246
- | string
247
- | number
248
- | boolean
249
- | readonly string[]
250
- | readonly Readonly<Record<string, unknown>>[];
251
-
252
- /**
253
- * Reconstruct a `WorkspaceManifest` envelope from a resolved composition
254
- * and run `compileManifest` against the supplied bind inputs.
255
- *
256
- * The reconstructed envelope carries NO `extends:` — the boot-seeder
257
- * already flattened the source manifest's `extends:` chain before
258
- * projecting `mountLayout`, so the layout is the effective post-merge
259
- * shape.
260
- *
261
- * `bindInputs` is the resolved manifest input bag (explicit inputs ∪
262
- * implicit inputs), built by {@link buildAgentManifestBindInputs}.
263
- * The compile fails fast on an unresolved required input or an enum
264
- * violation — exactly the legacy behaviour.
265
- */
266
- export function compileAgentWorkspaceManifest(
267
- source: AgentManifestSource,
268
- bindInputs: Readonly<Record<string, unknown>>,
269
- ): CompiledWorkspaceManifest {
270
- const mountLayout = source.mountLayout;
271
- if (!mountLayout) {
272
- throw new AgentMountLayoutError(
273
- source.agentRef,
274
- 'the composition declares no `workspace.mountLayout` block — its ' +
275
- 'source workspace manifest was not projected by the llm-registry-api ' +
276
- 'composition seeder. Re-seed compositions (boot llm-registry-api) or ' +
277
- 'fix the source manifest.',
278
- );
279
- }
280
-
281
- const [slug, version] = splitAgentRef(source.agentRef);
282
-
283
- // Sub-agents the composition resolved become the manifest agent block's
284
- // `subAgents[]`. The composer + EnvironmentResolver surface these on the
285
- // resolved environment snapshot (drift detection). The intrinsic floor
286
- // is still merged separately downstream — this is the manifest-declared
287
- // layer only, exactly as a legacy manifest's `agent.subAgents` was.
288
- const subAgents: ManifestSubAgent[] = source.subAgentBindings.map(
289
- (binding) => ({
290
- slug: binding.slug,
291
- ...(binding.alias !== undefined ? { alias: binding.alias } : {}),
292
- // Pass the overlay trio through to the manifest agent block so a
293
- // resolved delegate's per-node `modelOverride` / `instructions` /
294
- // `permission` are NOT dropped on reconstruction (the worker bug).
295
- // `ManifestSubAgent.defaultModel` is the manifest DSL's name for the
296
- // kernel `modelOverride` slot.
297
- ...(binding.modelOverride !== undefined
298
- ? { defaultModel: binding.modelOverride }
299
- : {}),
300
- ...(binding.instructions !== undefined
301
- ? { instructions: binding.instructions }
302
- : {}),
303
- ...(binding.permission !== undefined
304
- ? { permission: binding.permission }
305
- : {}),
306
- }),
307
- );
308
-
309
- // Raw fragments carried verbatim by the seeder. They are typed loosely
310
- // on the wire (the seeder owns the shape); `compileManifest`'s Zod schema
311
- // below re-validates them against the manifest DSL's authoritative
312
- // `WorkspaceManifestSchema`, so the casts here are checked downstream,
313
- // not silently trusted.
314
- const outputSurface = projectOutputSurface(source.outputSurface);
315
- const credentials = projectCredentials(source.credentials);
316
- const permissions = projectPermissions(source.permissions);
317
- const spec: WorkspaceManifestSpec = {
318
- mounts: mountLayout.mounts as unknown as ManifestMountsBlock,
319
- agent: {
320
- slug: source.primaryAgentSlug,
321
- stage: mountLayout.agentRunConfig.stage,
322
- role: mountLayout.agentRunConfig.role as AgentRunRole,
323
- ...(mountLayout.agentRunConfig.deliverableSpecRef !== undefined
324
- ? { deliverableSpecRef: mountLayout.agentRunConfig.deliverableSpecRef }
325
- : {}),
326
- ...(subAgents.length > 0 ? { subAgents } : {}),
327
- },
328
- ...(Object.keys(mountLayout.inputs).length > 0
329
- ? {
330
- inputs: mountLayout.inputs as unknown as Record<
331
- string,
332
- ManifestInputDeclaration
333
- >,
334
- }
335
- : {}),
336
- ...(mountLayout.seedFiles.length > 0
337
- ? {
338
- seedFiles:
339
- mountLayout.seedFiles as unknown as readonly ManifestSeedFile[],
340
- }
341
- : {}),
342
- ...(outputSurface === undefined ? {} : { outputSurface }),
343
- ...(credentials === undefined ? {} : { credentials }),
344
- ...(permissions === undefined ? {} : { permissions }),
345
- // Carry the authored working files through to `spec.workingFiles` so the
346
- // compiled manifest the composer reads (`req.manifest.workingFiles`) binds
347
- // the document file into `context.json.workingFiles` + narrows write
348
- // authority. Without this the reconstruction path silently drops them and
349
- // the agent sees no bound working file. `compileManifest`'s Zod schema
350
- // re-validates the entries (and interpolates `${input.x}`).
351
- ...(source.workingFiles && source.workingFiles.length > 0
352
- ? { workingFiles: source.workingFiles }
353
- : {}),
354
- };
355
-
356
- const envelope: DslWorkspaceManifest = {
357
- apiVersion: 'xema.dev/workspace/v1',
358
- kind: 'WorkspaceManifest',
359
- metadata: {
360
- slug,
361
- version: toManifestVersion(source.agentRef, version),
362
- // A manifest RECONSTRUCTED from a bare `agentRef` carries no inherent
363
- // surface restriction — the SAME agent (e.g. `demo-runner`,
364
- // `requirements-coordinator`) is legitimately driven on BOTH the
365
- // agent-session surface (interactive bootstrap) AND the workflow surface
366
- // (pipeline `xema/agent@v1` jobs). Defaulting to AGENT_SESSION only made
367
- // every workflow agent run fail `assertSurfaceCompat` with
368
- // "surfaceCompat=[agent-session] but was resolved on 'workflow'". Mirror
369
- // the DSL compiler's permissive `DEFAULT_SURFACE_COMPAT` ([workflow,
370
- // agent-session]); the runtime EnvironmentResolver still re-asserts the
371
- // ACTUAL surface, so widening the reconstructed set cannot let an agent
372
- // run somewhere its real manifest forbids — an authored manifest that
373
- // explicitly narrows `surfaceCompat` is parsed via the DSL path, not here.
374
- surfaceCompat: [ManifestSurface.WORKFLOW, ManifestSurface.AGENT_SESSION],
375
- },
376
- spec,
377
- };
378
-
379
- const result = compileManifest(envelope, bindInputs);
380
- if (!result.ok) {
381
- throw new AgentMountLayoutError(
382
- source.agentRef,
383
- `reconstructed manifest failed to compile: ${result.errors
384
- .map((e) => `${e.path}: ${e.message}`)
385
- .join('; ')}`,
386
- );
387
- }
388
- return result.compiled;
389
- }
390
-
391
- /**
392
- * Project the composition's `workspace.outputSurface` block (carried on
393
- * the runtime-agnostic `AgentManifestSource`) into the manifest DSL's
394
- * `ManifestOutputSurface` shape so the reconstructed envelope's
395
- * `spec.outputSurface` carries the same kind/root/port as the source. The
396
- * `kind` value strings match the DSL enum 1:1 (`none` / `web` / `static` /
397
- * `app` / `tunnel`) so the cast is exact, not coerced.
398
- *
399
- * Returns `undefined` when the composition declared no surface — the DSL
400
- * `compileManifest` then defaults the compiled `outputSurface` to
401
- * `{ kind: 'none', autoOpen: false, mode: 'single' }`.
402
- */
403
- function projectOutputSurface(
404
- surface: AgentOutputSurfaceInput | undefined,
405
- ): ManifestOutputSurface | undefined {
406
- if (!surface) return undefined;
407
- return {
408
- kind: surface.kind as OutputSurfaceKind,
409
- ...(surface.port === undefined ? {} : { port: surface.port }),
410
- ...(surface.healthPath === undefined
411
- ? {}
412
- : { healthPath: surface.healthPath }),
413
- ...(surface.autoOpen === undefined ? {} : { autoOpen: surface.autoOpen }),
414
- ...(surface.mode === undefined ? {} : { mode: surface.mode }),
415
- ...(surface.root === undefined ? {} : { root: surface.root }),
416
- ...(surface.defaultDocument === undefined
417
- ? {}
418
- : { defaultDocument: surface.defaultDocument }),
419
- };
420
- }
421
-
422
- /**
423
- * Project the composition's `workspace.credentials` block (carried on the
424
- * runtime-agnostic `AgentManifestSource` as the kernel
425
- * `CompiledManifestCredential[]`) into the manifest DSL's authored
426
- * `ManifestCredential[]` shape so the reconstructed envelope's
427
- * `spec.credentials` carries the same entries as the source.
428
- *
429
- * The compiled shape is a structural superset of the authored shape
430
- * (`{name, kind, sourceRef}` plus a now-required `required` boolean), so the
431
- * projection is a 1:1 field copy — `compileManifest`'s Zod schema below
432
- * re-validates each entry and re-defaults `required` idempotently. The map
433
- * is explicit (not a pass-through cast) so a malformed entry fails fast at
434
- * the manifest compile boundary, mirroring {@link projectOutputSurface}.
435
- *
436
- * Returns `undefined` when the composition declared no credentials — the DSL
437
- * `compileManifest` then defaults the compiled `credentials` to `[]`.
438
- */
439
- function projectCredentials(
440
- credentials: readonly CompiledManifestCredential[] | undefined,
441
- ): readonly ManifestCredential[] | undefined {
442
- if (credentials === undefined) return undefined;
443
- return credentials.map((credential) => ({
444
- name: credential.name,
445
- kind: credential.kind,
446
- sourceRef: credential.sourceRef,
447
- required: credential.required,
448
- }));
449
- }
450
-
451
- /**
452
- * Project the composition's `workspace.permissions` block (carried on the
453
- * runtime-agnostic `AgentManifestSource` as the kernel
454
- * `CompiledManifestPermissions`) into the manifest DSL's authored
455
- * `ManifestPermissions` shape so the reconstructed envelope's
456
- * `spec.permissions` carries the same tool allow/deny lists as the source.
457
- *
458
- * The compiled shape is a structural superset of the authored shape (the
459
- * compiled `tools.allow` / `tools.deny` are always present; the authored
460
- * ones are optional), so the projection is a 1:1 field copy —
461
- * `compileManifest`'s Zod schema below re-validates and re-defaults the
462
- * lists idempotently. Explicit (not a pass-through cast) so a malformed
463
- * block fails fast at the manifest compile boundary, mirroring
464
- * {@link projectOutputSurface}.
465
- *
466
- * Returns `undefined` when the composition declared no permissions — the DSL
467
- * `compileManifest` then defaults the compiled `permissions` to
468
- * `{ tools: { allow: [], deny: [] } }`.
469
- */
470
- function projectPermissions(
471
- permissions: CompiledManifestPermissions | undefined,
472
- ): ManifestPermissions | undefined {
473
- if (permissions === undefined) return undefined;
474
- return {
475
- tools: {
476
- allow: permissions.tools.allow,
477
- deny: permissions.tools.deny,
478
- },
479
- };
480
- }
481
-
482
- /**
483
- * Build the manifest bind-input bag from the caller's explicit input bag
484
- * and the implicit inputs, validated against the composition's declared
485
- * `mountLayout.inputs`.
486
- *
487
- * • an undeclared explicit input key fails fast;
488
- * • a non-primitive / mixed-array input value fails fast;
489
- * • a declared input with neither an explicit value nor an implicit
490
- * value is simply omitted — the compiler then applies the input's
491
- * own `default` (or fails fast if it is `required`).
492
- *
493
- * Returns `{}` when the composition declared no inputs.
494
- */
495
- export function buildAgentManifestBindInputs(
496
- source: AgentManifestSource,
497
- explicitInputs: Record<string, unknown> | null,
498
- implicitInputs: AgentManifestImplicitInputs,
499
- ): Readonly<Record<string, unknown>> {
500
- const declared = new Set(Object.keys(source.mountLayout?.inputs ?? {}));
501
- if (declared.size === 0) {
502
- return {};
503
- }
504
-
505
- if (
506
- explicitInputs !== null &&
507
- (typeof explicitInputs !== 'object' || Array.isArray(explicitInputs))
508
- ) {
509
- throw new AgentMountLayoutError(
510
- source.agentRef,
511
- 'explicit manifest inputs must be an object when provided',
512
- );
513
- }
514
- const explicit = explicitInputs ?? {};
515
- for (const [key, value] of Object.entries(explicit)) {
516
- if (!declared.has(key)) {
517
- throw new AgentMountLayoutError(
518
- source.agentRef,
519
- `manifest inputs contain undeclared key "${key}"`,
520
- );
521
- }
522
- assertManifestInputValue(source.agentRef, key, value);
523
- }
524
-
525
- const implicit: Record<string, ManifestInputValue> = {
526
- orgId: implicitInputs.orgId,
527
- projectId: implicitInputs.projectId,
528
- ...(implicitInputs.sessionId !== undefined
529
- ? { sessionId: implicitInputs.sessionId }
530
- : {}),
531
- };
532
-
533
- const out: Record<string, unknown> = {};
534
- for (const key of declared) {
535
- if (Object.hasOwn(explicit, key)) {
536
- out[key] = explicit[key];
537
- continue;
538
- }
539
- if (Object.hasOwn(implicit, key)) {
540
- out[key] = implicit[key];
541
- }
542
- }
543
- return out;
544
- }
545
-
546
- /** An authored manifest version: MAJOR.MINOR.PATCH. */
547
- const MANIFEST_SEMVER = /^\d+\.\d+\.\d+$/;
548
- /** An `AgentRef` version: the `AgentRevision.sequence` integer. */
549
- const AGENT_REVISION_SEQUENCE = /^\d+$/;
550
-
551
- /**
552
- * Map an `AgentRef` version onto `WorkspaceManifestMetadataSchema.version`.
553
- *
554
- * These are two different vocabularies and the reconstruction sits exactly on
555
- * the seam. An `AgentRef` is pinned by `AgentRevision.sequence` — a monotonic
556
- * INTEGER, so real refs look like `document-md-default@2`. The manifest schema
557
- * was written for AUTHORED YAML and requires MAJOR.MINOR.PATCH. Passing the
558
- * sequence straight through made `compileManifest` reject every reconstructed
559
- * manifest with "version must follow semver MAJOR.MINOR.PATCH", which is a
560
- * schema complaint about a field no author ever wrote.
561
- *
562
- * Sequence N maps to `N.0.0`: total, injective, and order-preserving, so
563
- * `drift-detector`'s version comparison keeps working and two distinct
564
- * revisions can never collide on one manifest version.
565
- *
566
- * A semver-shaped version passes through untouched — the legacy
567
- * `workspace-manifests-api` refs are genuinely semver, and this module serves
568
- * both. Anything else is REFUSED rather than coerced: a ref we cannot map is a
569
- * contract we do not understand, and inventing a version for it would put a
570
- * fabricated identity into the compiled manifest.
571
- */
572
- function toManifestVersion(agentRef: string, version: string): string {
573
- if (MANIFEST_SEMVER.test(version)) {
574
- return version;
575
- }
576
- if (AGENT_REVISION_SEQUENCE.test(version)) {
577
- return `${version}.0.0`;
578
- }
579
- throw new AgentMountLayoutError(
580
- agentRef,
581
- `cannot map ref version "${version}" onto a manifest version — expected ` +
582
- 'either an AgentRevision sequence integer or semver MAJOR.MINOR.PATCH',
583
- );
584
- }
585
-
586
- /** Split a `slug@version` composition ref; fail fast on a bare slug. */
587
- function splitAgentRef(ref: string): [slug: string, version: string] {
588
- const at = ref.lastIndexOf('@');
589
- if (at <= 0 || at === ref.length - 1) {
590
- throw new AgentMountLayoutError(
591
- ref,
592
- 'expected a version-pinned `slug@version` composition ref — the ' +
593
- 'session resolver always pins the resolved version',
594
- );
595
- }
596
- return [ref.slice(0, at), ref.slice(at + 1)];
597
- }
598
-
599
- /**
600
- * Manifest inputs flow into seed-file templates via Handlebars
601
- * interpolation. Non-primitives produce `[object Object]` silently;
602
- * mixed-type arrays break string-array inputs. Reject both at the
603
- * boundary so the error surfaces near the cause.
604
- */
605
- function assertManifestInputValue(
606
- agentRef: string,
607
- key: string,
608
- value: unknown,
609
- ): void {
610
- if (
611
- value !== null &&
612
- typeof value !== 'string' &&
613
- typeof value !== 'number' &&
614
- typeof value !== 'boolean' &&
615
- !Array.isArray(value)
616
- ) {
617
- throw new AgentMountLayoutError(
618
- agentRef,
619
- `manifest input "${key}" must be a primitive ` +
620
- `(string, number, boolean, string[]) — got ${typeof value}`,
621
- );
622
- }
623
- if (
624
- Array.isArray(value) &&
625
- !value.every((entry) => typeof entry === 'string')
626
- ) {
627
- throw new AgentMountLayoutError(
628
- agentRef,
629
- `manifest input "${key}" is an array — every element must be a string`,
630
- );
631
- }
632
- }