@xemahq/agent-session-runtime 0.8.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 (30) hide show
  1. package/dist/lib/composer.d.ts +25 -2
  2. package/dist/lib/composer.d.ts.map +1 -1
  3. package/dist/lib/composer.js +237 -81
  4. package/dist/lib/composer.js.map +1 -1
  5. package/dist/lib/launch-identity/index.d.ts +2 -0
  6. package/dist/lib/launch-identity/index.d.ts.map +1 -1
  7. package/dist/lib/launch-identity/index.js +2 -0
  8. package/dist/lib/launch-identity/index.js.map +1 -1
  9. package/dist/lib/launch-identity/preparation-generation.d.ts +26 -0
  10. package/dist/lib/launch-identity/preparation-generation.d.ts.map +1 -0
  11. package/dist/lib/launch-identity/preparation-generation.js +88 -0
  12. package/dist/lib/launch-identity/preparation-generation.js.map +1 -0
  13. package/dist/lib/launch-identity/prepared-launch-variant.d.ts +46 -0
  14. package/dist/lib/launch-identity/prepared-launch-variant.d.ts.map +1 -0
  15. package/dist/lib/launch-identity/prepared-launch-variant.js +118 -0
  16. package/dist/lib/launch-identity/prepared-launch-variant.js.map +1 -0
  17. package/package.json +20 -20
  18. package/src/lib/composer.ts +396 -135
  19. package/src/lib/launch-identity/index.ts +2 -0
  20. package/src/lib/launch-identity/preparation-generation.ts +171 -0
  21. package/src/lib/launch-identity/prepared-launch-variant.ts +263 -0
  22. package/LICENSE +0 -176
  23. package/dist/lib/composition-resolution.d.ts +0 -43
  24. package/dist/lib/composition-resolution.d.ts.map +0 -1
  25. package/dist/lib/composition-resolution.js +0 -236
  26. package/dist/lib/composition-resolution.js.map +0 -1
  27. package/dist/lib/composition-workspace-manifest.d.ts +0 -51
  28. package/dist/lib/composition-workspace-manifest.d.ts.map +0 -1
  29. package/dist/lib/composition-workspace-manifest.js +0 -178
  30. package/dist/lib/composition-workspace-manifest.js.map +0 -1
@@ -1,13 +1,19 @@
1
1
  import { createHash } from 'node:crypto';
2
2
 
3
3
  import {
4
- AWP_V1_SPEC,
5
- WorkspaceMount,
4
+ LogicalWorkspaceSlot,
5
+ LogicalMountPlanEntrySchema,
6
6
  WorkspaceMountMode,
7
- type SlotDefinition,
7
+ type LogicalMountPlanEntry,
8
8
  type WorkspaceMountPlan,
9
- type WorkspaceMountPlanEntry,
10
9
  } from '@xemahq/kernel-contracts/agent-workspace';
10
+ import {
11
+ RuntimePlacementAssessment,
12
+ UnsupportedRuntimePlacementError,
13
+ WorkerRuntimeKind,
14
+ type RuntimeLayoutAdapter,
15
+ type UnsupportedPlacement,
16
+ } from '@xemahq/kernel-contracts/worker-runtime';
11
17
  import {
12
18
  BriefcaseReferenceKind,
13
19
  type AgentRunRole,
@@ -54,6 +60,8 @@ interface AgentContextShape {
54
60
  readonly delegatedSubAgents?: readonly string[];
55
61
  readonly skills?: readonly string[];
56
62
  readonly instructions?: readonly string[];
63
+ readonly runtimeConfig?: Readonly<Record<string, unknown>>;
64
+ readonly mcpConfig?: Readonly<Record<string, unknown>>;
57
65
  readonly inputsJsonBase64?: string;
58
66
  readonly kbSpaceIds?: readonly string[];
59
67
  /**
@@ -113,8 +121,29 @@ export interface InputArtifactRef {
113
121
  readonly contentType?: string;
114
122
  }
115
123
 
124
+ export interface ExplicitlyUnsupportedOptionalEntry {
125
+ readonly mountKey: string;
126
+ readonly slot: LogicalWorkspaceSlot;
127
+ readonly placement: UnsupportedPlacement;
128
+ }
129
+
130
+ export interface RuntimeAssessedWorkspaceMountPlan extends WorkspaceMountPlan {
131
+ readonly runtimeKind: WorkerRuntimeKind;
132
+ readonly runtimeLayoutRevision: string;
133
+ readonly runtimeLayoutProfileRef: string;
134
+ readonly staticDependencyClosureHash: string;
135
+ readonly unsupportedOptionalEntries: readonly ExplicitlyUnsupportedOptionalEntry[];
136
+ }
137
+
116
138
  export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
117
- async compose(req: ComposeWorkspaceImageRequest): Promise<WorkspaceMountPlan> {
139
+ public constructor(
140
+ private readonly runtimeLayout: RuntimeLayoutAdapter,
141
+ private readonly optionalMountKeys: ReadonlySet<string> = new Set(),
142
+ ) {}
143
+
144
+ async compose(
145
+ req: ComposeWorkspaceImageRequest,
146
+ ): Promise<RuntimeAssessedWorkspaceMountPlan> {
118
147
  // Exactly-one-of invariant: the doc on `ComposeWorkspaceImageRequest`
119
148
  // promises `workflowRun` xor `interactive`. Enforce it here so
120
149
  // `deriveApplyId` can never collapse two unrelated requests onto a
@@ -172,7 +201,8 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
172
201
  req.manifest.agent.deliverableSpecRef ??
173
202
  ctx.deliverableSpecRef;
174
203
 
175
- const entries: WorkspaceMountPlanEntry[] = [];
204
+ assertNoSecretOrBindingMaterial(req);
205
+ const entries: LogicalMountPlanEntry[] = [];
176
206
 
177
207
  // ── Platform-rendered single-file slots ───────────────────────────
178
208
  // The three rendered-* sources (agents-md, context-json, system-overlay)
@@ -197,8 +227,12 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
197
227
  req.manifest.slug,
198
228
  req.manifest.version,
199
229
  );
200
- const renderedInputArtifacts = buildRenderedInputArtifacts(ctx.inputArtifacts);
201
- const renderedWorkingFiles = buildRenderedWorkingFiles(req.manifest.workingFiles);
230
+ const renderedInputArtifacts = buildRenderedInputArtifacts(
231
+ ctx.inputArtifacts,
232
+ );
233
+ const renderedWorkingFiles = buildRenderedWorkingFiles(
234
+ req.manifest.workingFiles,
235
+ );
202
236
  const renderedSourceParams: RenderedAgentRunContextParams = {
203
237
  orgId: req.orgId,
204
238
  projectId: req.projectId,
@@ -206,8 +240,12 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
206
240
  stageKey: req.manifest.agent.stageKey,
207
241
  role,
208
242
  ...(deliverableSpecRef === undefined ? {} : { deliverableSpecRef }),
209
- ...(req.workflowRun?.runId === undefined ? {} : { runId: req.workflowRun.runId }),
210
- ...(req.workflowRun?.jobRunId === undefined ? {} : { jobRunId: req.workflowRun.jobRunId }),
243
+ ...(req.workflowRun?.runId === undefined
244
+ ? {}
245
+ : { runId: req.workflowRun.runId }),
246
+ ...(req.workflowRun?.jobRunId === undefined
247
+ ? {}
248
+ : { jobRunId: req.workflowRun.jobRunId }),
211
249
  ...(req.interactive?.sessionId === undefined
212
250
  ? {}
213
251
  : { sessionId: req.interactive.sessionId }),
@@ -223,7 +261,7 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
223
261
 
224
262
  entries.push(
225
263
  buildEntry({
226
- slot: WorkspaceMount.AgentsMd,
264
+ slot: LogicalWorkspaceSlot.AgentContext,
227
265
  relPath: '',
228
266
  mode: WorkspaceMountMode.ReadOnly,
229
267
  mountKey: 'agents-md',
@@ -231,9 +269,31 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
231
269
  }),
232
270
  );
233
271
 
272
+ if (ctx.runtimeConfig !== undefined) {
273
+ assertRuntimeConfigExcludesImageOwnedContent(ctx.runtimeConfig);
274
+ entries.push(
275
+ buildCanonicalConfigEntry(
276
+ LogicalWorkspaceSlot.RuntimeConfig,
277
+ 'runtime-config',
278
+ 'runtime-config.json',
279
+ ctx.runtimeConfig,
280
+ ),
281
+ );
282
+ }
283
+ if (ctx.mcpConfig !== undefined) {
284
+ entries.push(
285
+ buildCanonicalConfigEntry(
286
+ LogicalWorkspaceSlot.McpConfig,
287
+ 'mcp-config',
288
+ 'mcp-config.json',
289
+ ctx.mcpConfig,
290
+ ),
291
+ );
292
+ }
293
+
234
294
  entries.push(
235
295
  buildEntry({
236
- slot: WorkspaceMount.ContextJson,
296
+ slot: LogicalWorkspaceSlot.AgentContext,
237
297
  relPath: '',
238
298
  mode: WorkspaceMountMode.ReadOnly,
239
299
  mountKey: 'context-json',
@@ -243,7 +303,7 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
243
303
 
244
304
  entries.push(
245
305
  buildEntry({
246
- slot: WorkspaceMount.SystemOverlay,
306
+ slot: LogicalWorkspaceSlot.SystemOverlay,
247
307
  relPath: '',
248
308
  mode: WorkspaceMountMode.ReadOnly,
249
309
  mountKey: 'system-overlay',
@@ -257,65 +317,56 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
257
317
  // and snapshots carry them across retries automatically.
258
318
  entries.push(
259
319
  buildEntry({
260
- slot: WorkspaceMount.Tmp,
320
+ slot: LogicalWorkspaceSlot.Temporary,
261
321
  relPath: '.keep',
262
322
  mode: WorkspaceMountMode.ReadWrite,
263
323
  mountKey: 'tmp:seed',
264
324
  source: {
265
325
  kind: 'static-literal',
266
- pathWithinWorkspace: 'tmp/.keep',
326
+ pathWithinWorkspace: '.keep',
267
327
  bytes: '',
268
328
  },
269
329
  }),
270
330
  );
271
331
 
272
- // ── Agent bundles (primary + delegated subagents) ─────────────────
273
- // WORKFLOW SURFACE ONLY. On the interactive session surface the session
274
- // control-bundle (workspace-proxy `materializeSessionBundle`) is the sole
275
- // authoritative writer of `/workspace/.opencode/agents/*.md`: it renders
276
- // the primary with its composition overrides (node `instructions` +
277
- // `promptMode: replace` + `permission`) server-side and patches the
278
- // delegate allowlist. If the composer also emitted these here, the
279
- // bootstrap mount-apply — which runs AFTER apply-control-bundle on launch
280
- // and does a whole-slot `rmSync`+rename on the AgentBundles slot — would
281
- // WIPE the control-bundle's correct render and replace it with the BARE
282
- // base kernel (e.g. a composition extending `planner` collapses to raw
283
- // `planner`, losing its persona prompt + delegate set). The workflow
284
- // surface has no control-bundle, so it MUST still emit them here.
285
- if (isWorkflowSurface) {
332
+ // ── Agent definitions (primary + delegated subagents) ─────────────
333
+ // The logical plan is the sole static closure authority on both
334
+ // workflow and interactive surfaces. Runtime-specific writers may render
335
+ // these differently, but no second control bundle may reconstruct them.
336
+ entries.push(
337
+ buildEntry({
338
+ slot: LogicalWorkspaceSlot.AgentDefinitions,
339
+ relPath: `${req.manifest.agent.slug}.md`,
340
+ mode: WorkspaceMountMode.ReadOnly,
341
+ mountKey: `agent-bundle:${req.manifest.agent.slug}`,
342
+ source: {
343
+ kind: 'agent-kernel',
344
+ orgId: req.orgId,
345
+ agentSlug: req.manifest.agent.slug,
346
+ stageKey: req.manifest.agent.stageKey,
347
+ agentMode: 'primary',
348
+ },
349
+ }),
350
+ );
351
+ for (const sub of dedupe(ctx.delegatedSubAgents ?? [])) {
352
+ if (sub === req.manifest.agent.slug) {
353
+ continue;
354
+ }
286
355
  entries.push(
287
356
  buildEntry({
288
- slot: WorkspaceMount.AgentBundles,
289
- relPath: `${req.manifest.agent.slug}.md`,
357
+ slot: LogicalWorkspaceSlot.AgentDefinitions,
358
+ relPath: `${sub}.md`,
290
359
  mode: WorkspaceMountMode.ReadOnly,
291
- mountKey: `agent-bundle:${req.manifest.agent.slug}`,
360
+ mountKey: `agent-bundle:${sub}`,
292
361
  source: {
293
362
  kind: 'agent-kernel',
294
363
  orgId: req.orgId,
295
- agentSlug: req.manifest.agent.slug,
364
+ agentSlug: sub,
296
365
  stageKey: req.manifest.agent.stageKey,
297
- agentMode: 'primary',
366
+ agentMode: 'subagent',
298
367
  },
299
368
  }),
300
369
  );
301
- for (const sub of ctx.delegatedSubAgents ?? []) {
302
- if (sub === req.manifest.agent.slug) continue; // primary already emitted
303
- entries.push(
304
- buildEntry({
305
- slot: WorkspaceMount.AgentBundles,
306
- relPath: `${sub}.md`,
307
- mode: WorkspaceMountMode.ReadOnly,
308
- mountKey: `agent-bundle:${sub}`,
309
- source: {
310
- kind: 'agent-kernel',
311
- orgId: req.orgId,
312
- agentSlug: sub,
313
- stageKey: req.manifest.agent.stageKey,
314
- agentMode: 'subagent',
315
- },
316
- }),
317
- );
318
- }
319
370
  }
320
371
 
321
372
  // ── Skill bundles + /skill launch commands ────────────────────────
@@ -324,8 +375,8 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
324
375
  // resolver yields one file per bundle member, so the writer composes
325
376
  // `<skill-bundles slot>/<skillKey>/<resolver relPath>`.
326
377
  //
327
- // Alongside each bundle we emit one OpenCode command file so typing
328
- // `/<skillKey>` in a session loads and applies that skill natively.
378
+ // Alongside each bundle we emit one logical invocation intent. Qwen's
379
+ // adapter consumes it natively; file-oriented adapters render a command.
329
380
  //
330
381
  // Kernel-shipped System skills are PREPENDED to whatever skills the
331
382
  // caller already named in `agentContext.skills` (per the skill
@@ -339,7 +390,7 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
339
390
  ])) {
340
391
  entries.push(
341
392
  buildEntry({
342
- slot: WorkspaceMount.SkillBundles,
393
+ slot: LogicalWorkspaceSlot.SkillBundles,
343
394
  relPath: skillKey,
344
395
  mode: WorkspaceMountMode.ReadOnly,
345
396
  mountKey: `skill-bundle:${skillKey}`,
@@ -352,17 +403,21 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
352
403
  const commandRelPath = `${skillKey}.md`;
353
404
  entries.push(
354
405
  buildEntry({
355
- slot: WorkspaceMount.Commands,
406
+ slot: LogicalWorkspaceSlot.SkillCommands,
356
407
  relPath: commandRelPath,
357
408
  mode: WorkspaceMountMode.ReadOnly,
358
409
  mountKey: `skill-command:${skillKey}`,
359
- source: {
360
- kind: 'static-literal',
361
- pathWithinWorkspace: `.opencode/command/${commandRelPath}`,
362
- bytes: Buffer.from(renderSkillCommand(skillKey), 'utf8').toString(
363
- 'base64',
364
- ),
365
- },
410
+ source:
411
+ this.runtimeLayout.runtimeKind === WorkerRuntimeKind.Qwen
412
+ ? { kind: 'skill-bundle', skillKey }
413
+ : {
414
+ kind: 'static-literal',
415
+ pathWithinWorkspace: commandRelPath,
416
+ bytes: Buffer.from(
417
+ renderSkillCommand(skillKey),
418
+ 'utf8',
419
+ ).toString('base64'),
420
+ },
366
421
  }),
367
422
  );
368
423
  }
@@ -371,7 +426,7 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
371
426
  for (const sectionKey of dedupe(ctx.instructions ?? [])) {
372
427
  entries.push(
373
428
  buildEntry({
374
- slot: WorkspaceMount.Instructions,
429
+ slot: LogicalWorkspaceSlot.Instructions,
375
430
  relPath: `${sectionKey}.md`,
376
431
  mode: WorkspaceMountMode.ReadOnly,
377
432
  mountKey: `instruction:${sectionKey}`,
@@ -410,24 +465,25 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
410
465
  `Enable the slot in the agent manifest or change the inputArtifact's 'slot' to one of: [${[...enabledSlots].join(', ') || '(none)'}].`,
411
466
  );
412
467
  }
413
- const slotDef = AWP_V1_SPEC.slots.find((s) => s.key === slotKey);
414
- if (!slotDef) {
415
- throw new UnknownSlotError(
416
- slotKey,
417
- 'inputArtifacts',
418
- req.manifest.slug,
419
- req.manifest.version,
420
- );
421
- }
468
+ const slotDef = requireLogicalSlot(
469
+ slotKey,
470
+ 'inputArtifacts',
471
+ req.manifest.slug,
472
+ req.manifest.version,
473
+ );
422
474
  const fileName = ia.fileName.trim();
423
- if (fileName.length === 0 || fileName.includes('..') || fileName.startsWith('/')) {
475
+ if (
476
+ fileName.length === 0 ||
477
+ fileName.includes('..') ||
478
+ fileName.startsWith('/')
479
+ ) {
424
480
  throw new Error(
425
481
  `WorkspaceImageComposer: inputArtifact fileName '${ia.fileName}' is invalid — must be a non-empty relative path without '..' segments.`,
426
482
  );
427
483
  }
428
484
  entries.push(
429
485
  buildEntry({
430
- slot: slotDef.path as WorkspaceMount,
486
+ slot: slotDef.slot,
431
487
  relPath: fileName,
432
488
  mode: WorkspaceMountMode.ReadOnly,
433
489
  mountKey: `input-artifact:${ia.artifactId}:v${ia.version}:${fileName}`,
@@ -436,8 +492,12 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
436
492
  artifactId: ia.artifactId,
437
493
  version: ia.version,
438
494
  fileName,
439
- ...(ia.payloadField !== undefined && { payloadField: ia.payloadField }),
440
- ...(ia.contentType !== undefined && { contentType: ia.contentType }),
495
+ ...(ia.payloadField !== undefined && {
496
+ payloadField: ia.payloadField,
497
+ }),
498
+ ...(ia.contentType !== undefined && {
499
+ contentType: ia.contentType,
500
+ }),
441
501
  },
442
502
  }),
443
503
  );
@@ -446,18 +506,17 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
446
506
  // ── User-data mounts (manifest-declared) ──────────────────────────
447
507
  for (const [slotKey, decl] of Object.entries(req.manifest.mounts)) {
448
508
  if (!decl.enabled) continue;
449
- const slotDef = AWP_V1_SPEC.slots.find((s) => s.key === slotKey);
450
- if (!slotDef) {
451
- throw new UnknownSlotError(
452
- slotKey,
453
- 'manifest.mounts',
454
- req.manifest.slug,
455
- req.manifest.version,
456
- );
457
- }
509
+ const slotDef = requireLogicalSlot(
510
+ slotKey,
511
+ 'manifest.mounts',
512
+ req.manifest.slug,
513
+ req.manifest.version,
514
+ );
458
515
  const mode = decl.mode ?? slotDef.defaultMode;
459
516
  const mountMode =
460
- mode === 'read-write' ? WorkspaceMountMode.ReadWrite : WorkspaceMountMode.ReadOnly;
517
+ mode === 'read-write'
518
+ ? WorkspaceMountMode.ReadWrite
519
+ : WorkspaceMountMode.ReadOnly;
461
520
  const sources = extractMountSourcesForSlot(
462
521
  slotKey,
463
522
  decl,
@@ -469,7 +528,7 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
469
528
  for (const { source, relPath, mountKey } of sources) {
470
529
  entries.push(
471
530
  buildEntry({
472
- slot: slotDef.path as WorkspaceMount,
531
+ slot: slotDef.slot,
473
532
  relPath,
474
533
  mode: mountMode,
475
534
  mountKey,
@@ -490,15 +549,12 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
490
549
  );
491
550
  }
492
551
  for (const sf of req.manifest.seedFiles) {
493
- const slotDef = AWP_V1_SPEC.slots.find((s) => s.key === sf.slot);
494
- if (!slotDef) {
495
- throw new UnknownSlotError(
496
- sf.slot,
497
- 'manifest.seedFiles',
498
- req.manifest.slug,
499
- req.manifest.version,
500
- );
501
- }
552
+ const slotDef = requireLogicalSlot(
553
+ sf.slot,
554
+ 'manifest.seedFiles',
555
+ req.manifest.slug,
556
+ req.manifest.version,
557
+ );
502
558
  let bytesBase64: string;
503
559
  if (sf.source.kind === 'inline') {
504
560
  bytesBase64 = sf.source.bytesBase64;
@@ -518,7 +574,7 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
518
574
  // which is exactly what we want.
519
575
  entries.push(
520
576
  buildEntry({
521
- slot: slotDef.path as WorkspaceMount,
577
+ slot: slotDef.slot,
522
578
  relPath: sf.relPath,
523
579
  mode: WorkspaceMountMode.ReadOnly,
524
580
  mountKey: `seed:${sf.slot}:${sf.relPath}`,
@@ -531,10 +587,12 @@ export class DefaultWorkspaceImageComposer implements WorkspaceImageComposer {
531
587
  );
532
588
  }
533
589
 
534
- return {
535
- applyId: deriveApplyId(req),
590
+ return finalizeLogicalPlan(
591
+ req,
536
592
  entries,
537
- };
593
+ this.runtimeLayout,
594
+ this.optionalMountKeys,
595
+ );
538
596
  }
539
597
  }
540
598
 
@@ -576,6 +634,116 @@ function deriveApplyId(req: ComposeWorkspaceImageRequest): string {
576
634
  return createHash('sha256').update(components.join(':')).digest('hex');
577
635
  }
578
636
 
637
+ function finalizeLogicalPlan(
638
+ req: ComposeWorkspaceImageRequest,
639
+ entries: readonly LogicalMountPlanEntry[],
640
+ runtimeLayout: RuntimeLayoutAdapter,
641
+ optionalMountKeys: ReadonlySet<string>,
642
+ ): RuntimeAssessedWorkspaceMountPlan {
643
+ return compileLogicalWorkspacePlan({
644
+ applySeed: deriveApplyId(req),
645
+ entries,
646
+ runtimeLayout,
647
+ optionalMountKeys,
648
+ });
649
+ }
650
+
651
+ export interface CompileLogicalWorkspacePlanInput {
652
+ readonly applySeed: string;
653
+ readonly entries: readonly LogicalMountPlanEntry[];
654
+ readonly runtimeLayout: RuntimeLayoutAdapter;
655
+ readonly optionalMountKeys?: ReadonlySet<string>;
656
+ }
657
+
658
+ export function compileLogicalWorkspacePlan(
659
+ input: CompileLogicalWorkspacePlanInput,
660
+ ): RuntimeAssessedWorkspaceMountPlan {
661
+ const { applySeed, entries, runtimeLayout } = input;
662
+ const optionalMountKeys = input.optionalMountKeys ?? new Set<string>();
663
+ const ordered = [...entries].sort(compareLogicalEntries);
664
+ const seenMountKeys = new Set<string>();
665
+ const supported: LogicalMountPlanEntry[] = [];
666
+ const unsupportedOptionalEntries: ExplicitlyUnsupportedOptionalEntry[] = [];
667
+
668
+ for (const entry of ordered) {
669
+ assertNoSecretOrBindingMaterial(entry.source);
670
+ LogicalMountPlanEntrySchema.parse(entry);
671
+ if (seenMountKeys.has(entry.mountKey)) {
672
+ throw new Error(`DUPLICATE_LOGICAL_MOUNT_KEY:${entry.mountKey}`);
673
+ }
674
+ seenMountKeys.add(entry.mountKey);
675
+ const placement = runtimeLayout.assess(entry);
676
+ if (placement.assessment === RuntimePlacementAssessment.Unsupported) {
677
+ if (!optionalMountKeys.has(entry.mountKey)) {
678
+ throw new UnsupportedRuntimePlacementError(placement);
679
+ }
680
+ unsupportedOptionalEntries.push({
681
+ mountKey: entry.mountKey,
682
+ slot: entry.slot,
683
+ placement,
684
+ });
685
+ continue;
686
+ }
687
+ supported.push(entry);
688
+ }
689
+
690
+ const closure = {
691
+ schemaVersion: 1,
692
+ runtimeKind: runtimeLayout.runtimeKind,
693
+ runtimeLayoutRevision: runtimeLayout.revision,
694
+ runtimeLayoutProfileRef: runtimeLayout.profileRef,
695
+ entries: supported,
696
+ unsupportedOptionalEntries,
697
+ } as const;
698
+ const staticDependencyClosureHash = createHash('sha256')
699
+ .update(JSON.stringify(stableSort(closure)))
700
+ .digest('hex');
701
+ const applyId = createHash('sha256')
702
+ .update(`${applySeed}:${staticDependencyClosureHash}`)
703
+ .digest('hex');
704
+ return {
705
+ applyId,
706
+ entries: supported,
707
+ runtimeKind: runtimeLayout.runtimeKind,
708
+ runtimeLayoutRevision: runtimeLayout.revision,
709
+ runtimeLayoutProfileRef: runtimeLayout.profileRef,
710
+ staticDependencyClosureHash,
711
+ unsupportedOptionalEntries,
712
+ };
713
+ }
714
+
715
+ function compareLogicalEntries(
716
+ left: LogicalMountPlanEntry,
717
+ right: LogicalMountPlanEntry,
718
+ ): number {
719
+ const leftKey = `${left.slot}\u0000${left.relPath}\u0000${left.mountKey}`;
720
+ const rightKey = `${right.slot}\u0000${right.relPath}\u0000${right.mountKey}`;
721
+ return leftKey === rightKey ? 0 : leftKey < rightKey ? -1 : 1;
722
+ }
723
+
724
+ const FORBIDDEN_MATERIAL_KEY =
725
+ /(?:secret|password|accessToken|refreshToken|credentialLease|runtimeBinding|sessionBinding)/i;
726
+
727
+ function assertNoSecretOrBindingMaterial(value: unknown, path = 'input'): void {
728
+ if (value === null || typeof value !== 'object') {
729
+ return;
730
+ }
731
+ if (Array.isArray(value)) {
732
+ value.forEach((member, index) =>
733
+ assertNoSecretOrBindingMaterial(member, `${path}[${index}]`),
734
+ );
735
+ return;
736
+ }
737
+ for (const [key, member] of Object.entries(
738
+ value as Readonly<Record<string, unknown>>,
739
+ )) {
740
+ if (FORBIDDEN_MATERIAL_KEY.test(key) && member !== undefined) {
741
+ throw new Error(`FORBIDDEN_RUNTIME_MATERIAL:${path}.${key}`);
742
+ }
743
+ assertNoSecretOrBindingMaterial(member, `${path}.${key}`);
744
+ }
745
+ }
746
+
579
747
  /**
580
748
  * Hash the manifest fields that drive mount composition. A change to
581
749
  * any of these fields produces a different `applyId`, forcing
@@ -633,12 +801,12 @@ function renderSkillCommand(skillKey: string): string {
633
801
  }
634
802
 
635
803
  function buildEntry(args: {
636
- slot: WorkspaceMount;
804
+ slot: LogicalWorkspaceSlot;
637
805
  relPath: string;
638
806
  mode: WorkspaceMountMode;
639
807
  mountKey: string;
640
808
  source: MountSource;
641
- }): WorkspaceMountPlanEntry {
809
+ }): LogicalMountPlanEntry {
642
810
  return {
643
811
  mountKey: args.mountKey,
644
812
  slot: args.slot,
@@ -648,6 +816,38 @@ function buildEntry(args: {
648
816
  };
649
817
  }
650
818
 
819
+ function buildCanonicalConfigEntry(
820
+ slot: LogicalWorkspaceSlot.RuntimeConfig | LogicalWorkspaceSlot.McpConfig,
821
+ mountKey: string,
822
+ sourceName: string,
823
+ value: Readonly<Record<string, unknown>>,
824
+ ): LogicalMountPlanEntry {
825
+ assertNoSecretOrBindingMaterial(value, mountKey);
826
+ return buildEntry({
827
+ slot,
828
+ relPath: '',
829
+ mode: WorkspaceMountMode.ReadOnly,
830
+ mountKey,
831
+ source: {
832
+ kind: 'static-literal',
833
+ pathWithinWorkspace: sourceName,
834
+ bytes: Buffer.from(JSON.stringify(stableSort(value)), 'utf8').toString(
835
+ 'base64',
836
+ ),
837
+ },
838
+ });
839
+ }
840
+
841
+ function assertRuntimeConfigExcludesImageOwnedContent(
842
+ config: Readonly<Record<string, unknown>>,
843
+ ): void {
844
+ for (const key of ['plugins', 'tools']) {
845
+ if (config[key] !== undefined) {
846
+ throw new Error(`RUNTIME_IMAGE_OWNED_CONTENT:${key}`);
847
+ }
848
+ }
849
+ }
850
+
651
851
  interface ExtractedMountSource {
652
852
  readonly source: MountSource;
653
853
  readonly relPath: string;
@@ -720,7 +920,9 @@ function extractMountSourcesForSlot(
720
920
  mountKey: `references:project:${projectId}`,
721
921
  });
722
922
  }
723
- for (const entry of extractBriefcaseReferenceMounts(briefcase?.references)) {
923
+ for (const entry of extractBriefcaseReferenceMounts(
924
+ briefcase?.references,
925
+ )) {
724
926
  out.push(entry);
725
927
  }
726
928
  return out;
@@ -749,7 +951,10 @@ function extractMountSourcesForSlot(
749
951
  if (!deliverableSpecRef) return [];
750
952
  return [
751
953
  {
752
- source: { kind: 'deliverable-specs', contractKey: deliverableSpecRef },
954
+ source: {
955
+ kind: 'deliverable-specs',
956
+ contractKey: deliverableSpecRef,
957
+ },
753
958
  relPath: deliverableSpecRef,
754
959
  mountKey: `deliverable-specs:${deliverableSpecRef}`,
755
960
  },
@@ -796,7 +1001,11 @@ function extractBriefcaseUploadMounts(
796
1001
  if (!uploads || uploads.length === 0) return [];
797
1002
  return uploads.map((u) => {
798
1003
  const fileName = u.filename.trim();
799
- if (fileName.length === 0 || fileName.includes('..') || fileName.startsWith('/')) {
1004
+ if (
1005
+ fileName.length === 0 ||
1006
+ fileName.includes('..') ||
1007
+ fileName.startsWith('/')
1008
+ ) {
800
1009
  throw new Error(
801
1010
  `WorkspaceImageComposer: briefcase upload filename '${u.filename}' is invalid — must be a non-empty relative path without '..' segments.`,
802
1011
  );
@@ -839,7 +1048,9 @@ function extractBriefcaseReferenceMounts(
839
1048
  return out;
840
1049
  }
841
1050
 
842
- function briefcaseReferenceToMount(ref: BriefcaseReference): ExtractedMountSource | null {
1051
+ function briefcaseReferenceToMount(
1052
+ ref: BriefcaseReference,
1053
+ ): ExtractedMountSource | null {
843
1054
  switch (ref.kind) {
844
1055
  case BriefcaseReferenceKind.KB_SPACE:
845
1056
  return {
@@ -881,7 +1092,8 @@ function briefcaseReferenceToMount(ref: BriefcaseReference): ExtractedMountSourc
881
1092
  );
882
1093
  }
883
1094
  const trimmedTitle = ref.title?.trim() ?? '';
884
- const fileName = trimmedTitle.length > 0 ? trimmedTitle : `${artifactId}-v${version}`;
1095
+ const fileName =
1096
+ trimmedTitle.length > 0 ? trimmedTitle : `${artifactId}-v${version}`;
885
1097
  return {
886
1098
  source: {
887
1099
  kind: 'artifact-version',
@@ -916,13 +1128,53 @@ function briefcaseReferenceToMount(ref: BriefcaseReference): ExtractedMountSourc
916
1128
  }
917
1129
  }
918
1130
 
919
- // Static-only assertion that AWP slot `path`s align with the
920
- // WorkspaceMount enum we depend on. Kept here so a kernel slot rename
921
- // fails fast at build time.
922
- function _assertSlotsCover(): SlotDefinition[] {
923
- return [...AWP_V1_SPEC.slots];
1131
+ interface ManifestLogicalSlot {
1132
+ readonly slot: LogicalWorkspaceSlot;
1133
+ readonly defaultMode: 'read-only' | 'read-write';
1134
+ }
1135
+
1136
+ const MANIFEST_LOGICAL_SLOTS: Readonly<Record<string, ManifestLogicalSlot>> =
1137
+ Object.freeze({
1138
+ inputs: { slot: LogicalWorkspaceSlot.Inputs, defaultMode: 'read-only' },
1139
+ tmp: { slot: LogicalWorkspaceSlot.Temporary, defaultMode: 'read-write' },
1140
+ deliverables: {
1141
+ slot: LogicalWorkspaceSlot.Deliverables,
1142
+ defaultMode: 'read-write',
1143
+ },
1144
+ 'deliverable-specs': {
1145
+ slot: LogicalWorkspaceSlot.DeliverableSpecs,
1146
+ defaultMode: 'read-only',
1147
+ },
1148
+ references: {
1149
+ slot: LogicalWorkspaceSlot.References,
1150
+ defaultMode: 'read-only',
1151
+ },
1152
+ repos: {
1153
+ slot: LogicalWorkspaceSlot.Repositories,
1154
+ defaultMode: 'read-write',
1155
+ },
1156
+ uploads: {
1157
+ slot: LogicalWorkspaceSlot.Uploads,
1158
+ defaultMode: 'read-only',
1159
+ },
1160
+ });
1161
+
1162
+ function requireLogicalSlot(
1163
+ key: string,
1164
+ source:
1165
+ | 'manifest.mounts'
1166
+ | 'manifest.seedFiles'
1167
+ | 'rendered.mounts'
1168
+ | 'inputArtifacts',
1169
+ manifestSlug: string,
1170
+ manifestVersion: string,
1171
+ ): ManifestLogicalSlot {
1172
+ const slot = MANIFEST_LOGICAL_SLOTS[key];
1173
+ if (!slot) {
1174
+ throw new UnknownSlotError(key, source, manifestSlug, manifestVersion);
1175
+ }
1176
+ return slot;
924
1177
  }
925
- void _assertSlotsCover;
926
1178
 
927
1179
  /**
928
1180
  * Drop the keys the composer itself consumes (subagents, skills, mount
@@ -938,6 +1190,8 @@ function pickWorkflowInputs(
938
1190
  'delegatedSubAgents',
939
1191
  'skills',
940
1192
  'instructions',
1193
+ 'runtimeConfig',
1194
+ 'mcpConfig',
941
1195
  'inputsJsonBase64',
942
1196
  'kbSpaceIds',
943
1197
  'kbPageMounts',
@@ -994,10 +1248,10 @@ function buildRenderedInputArtifacts(
994
1248
  }> = [];
995
1249
  for (const ia of inputArtifacts) {
996
1250
  const slotKey = ia.slot ?? 'inputs';
997
- const slotDef = AWP_V1_SPEC.slots.find((s) => s.key === slotKey);
1251
+ const slotDef = MANIFEST_LOGICAL_SLOTS[slotKey];
998
1252
  if (!slotDef) continue;
999
1253
  out.push({
1000
- path: `${slotDef.path}/${ia.fileName}`,
1254
+ path: `${slotDef.slot}/${ia.fileName}`,
1001
1255
  ...(ia.contentType !== undefined && { contentType: ia.contentType }),
1002
1256
  sourceArtifactId: ia.artifactId,
1003
1257
  sourceVersion: ia.version,
@@ -1021,35 +1275,42 @@ function buildRenderedWorkingFiles(
1021
1275
  readonly sourceRef: Readonly<Record<string, string>>;
1022
1276
  }> {
1023
1277
  if (!workingFiles || workingFiles.length === 0) return [];
1024
- return workingFiles.map((wf) => ({
1025
- slug: wf.slug,
1026
- path: wf.path,
1027
- format: wf.format,
1028
- syncDirection: wf.syncDirection,
1029
- sourceKind: wf.sourceKind,
1030
- sourceRef: wf.sourceRef,
1031
- }));
1278
+ return workingFiles.map((wf) => {
1279
+ if (
1280
+ wf.path.startsWith('/') ||
1281
+ /(^|\/)\.(?:opencode|qwen|claude|hermes)(\/|$)/.test(wf.path)
1282
+ ) {
1283
+ throw new Error(`NON_LOGICAL_WORKING_FILE_PATH:${wf.slug}:${wf.path}`);
1284
+ }
1285
+ return {
1286
+ slug: wf.slug,
1287
+ path: wf.path,
1288
+ format: wf.format,
1289
+ syncDirection: wf.syncDirection,
1290
+ sourceKind: wf.sourceKind,
1291
+ sourceRef: wf.sourceRef,
1292
+ };
1293
+ });
1032
1294
  }
1033
1295
 
1034
1296
  function buildRenderedMounts(
1035
- mounts: Readonly<Record<string, { enabled: boolean; mode?: 'read-only' | 'read-write' }>>,
1297
+ mounts: Readonly<
1298
+ Record<string, { enabled: boolean; mode?: 'read-only' | 'read-write' }>
1299
+ >,
1036
1300
  manifestSlug: string,
1037
1301
  manifestVersion: string,
1038
1302
  ): ReadonlyArray<{ mount: string; mode: 'read-only' | 'read-write' }> {
1039
1303
  const out: { mount: string; mode: 'read-only' | 'read-write' }[] = [];
1040
1304
  for (const [slotKey, decl] of Object.entries(mounts)) {
1041
1305
  if (!decl.enabled) continue;
1042
- const slotDef = AWP_V1_SPEC.slots.find((s) => s.key === slotKey);
1043
- if (!slotDef) {
1044
- throw new UnknownSlotError(
1045
- slotKey,
1046
- 'rendered.mounts',
1047
- manifestSlug,
1048
- manifestVersion,
1049
- );
1050
- }
1306
+ const slotDef = requireLogicalSlot(
1307
+ slotKey,
1308
+ 'rendered.mounts',
1309
+ manifestSlug,
1310
+ manifestVersion,
1311
+ );
1051
1312
  const mode = decl.mode ?? slotDef.defaultMode;
1052
- out.push({ mount: slotDef.path, mode });
1313
+ out.push({ mount: slotDef.slot, mode });
1053
1314
  }
1054
1315
  return out;
1055
1316
  }