@xemahq/agent-session-runtime 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.
- package/dist/lib/agent-resolution.d.ts.map +1 -1
- package/dist/lib/agent-resolution.js.map +1 -1
- package/dist/lib/composer.d.ts +25 -2
- package/dist/lib/composer.d.ts.map +1 -1
- package/dist/lib/composer.js +237 -81
- package/dist/lib/composer.js.map +1 -1
- package/dist/lib/launch-identity/canonical-contract.d.ts +1 -0
- package/dist/lib/launch-identity/canonical-contract.d.ts.map +1 -1
- package/dist/lib/launch-identity/canonical-contract.js +14 -2
- package/dist/lib/launch-identity/canonical-contract.js.map +1 -1
- package/dist/lib/launch-identity/compiled-session-launch-plan.d.ts.map +1 -1
- package/dist/lib/launch-identity/compiled-session-launch-plan.js +1 -5
- package/dist/lib/launch-identity/compiled-session-launch-plan.js.map +1 -1
- package/dist/lib/launch-identity/index.d.ts +2 -0
- package/dist/lib/launch-identity/index.d.ts.map +1 -1
- package/dist/lib/launch-identity/index.js +2 -0
- package/dist/lib/launch-identity/index.js.map +1 -1
- package/dist/lib/launch-identity/preparation-generation.d.ts +26 -0
- package/dist/lib/launch-identity/preparation-generation.d.ts.map +1 -0
- package/dist/lib/launch-identity/preparation-generation.js +88 -0
- package/dist/lib/launch-identity/preparation-generation.js.map +1 -0
- package/dist/lib/launch-identity/prepared-launch-variant.d.ts +46 -0
- package/dist/lib/launch-identity/prepared-launch-variant.d.ts.map +1 -0
- package/dist/lib/launch-identity/prepared-launch-variant.js +118 -0
- package/dist/lib/launch-identity/prepared-launch-variant.js.map +1 -0
- package/dist/lib/launch-identity/runtime-template-key.d.ts.map +1 -1
- package/dist/lib/launch-identity/runtime-template-key.js +1 -5
- package/dist/lib/launch-identity/runtime-template-key.js.map +1 -1
- package/package.json +21 -20
- package/src/lib/agent-resolution.ts +15 -4
- package/src/lib/composer.ts +396 -135
- package/src/lib/launch-identity/canonical-contract.ts +36 -2
- package/src/lib/launch-identity/compiled-session-launch-plan.ts +2 -10
- package/src/lib/launch-identity/index.ts +2 -0
- package/src/lib/launch-identity/preparation-generation.ts +171 -0
- package/src/lib/launch-identity/prepared-launch-variant.ts +263 -0
- package/src/lib/launch-identity/runtime-template-key.ts +2 -7
- package/LICENSE +0 -176
package/src/lib/composer.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
LogicalWorkspaceSlot,
|
|
5
|
+
LogicalMountPlanEntrySchema,
|
|
6
6
|
WorkspaceMountMode,
|
|
7
|
-
type
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
201
|
-
|
|
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
|
|
210
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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: '
|
|
326
|
+
pathWithinWorkspace: '.keep',
|
|
267
327
|
bytes: '',
|
|
268
328
|
},
|
|
269
329
|
}),
|
|
270
330
|
);
|
|
271
331
|
|
|
272
|
-
// ── Agent
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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:
|
|
289
|
-
relPath: `${
|
|
357
|
+
slot: LogicalWorkspaceSlot.AgentDefinitions,
|
|
358
|
+
relPath: `${sub}.md`,
|
|
290
359
|
mode: WorkspaceMountMode.ReadOnly,
|
|
291
|
-
mountKey: `agent-bundle:${
|
|
360
|
+
mountKey: `agent-bundle:${sub}`,
|
|
292
361
|
source: {
|
|
293
362
|
kind: 'agent-kernel',
|
|
294
363
|
orgId: req.orgId,
|
|
295
|
-
agentSlug:
|
|
364
|
+
agentSlug: sub,
|
|
296
365
|
stageKey: req.manifest.agent.stageKey,
|
|
297
|
-
agentMode: '
|
|
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
|
|
328
|
-
//
|
|
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:
|
|
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:
|
|
406
|
+
slot: LogicalWorkspaceSlot.SkillCommands,
|
|
356
407
|
relPath: commandRelPath,
|
|
357
408
|
mode: WorkspaceMountMode.ReadOnly,
|
|
358
409
|
mountKey: `skill-command:${skillKey}`,
|
|
359
|
-
source:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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:
|
|
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 =
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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 (
|
|
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.
|
|
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 && {
|
|
440
|
-
|
|
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 =
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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'
|
|
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.
|
|
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 =
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
804
|
+
slot: LogicalWorkspaceSlot;
|
|
637
805
|
relPath: string;
|
|
638
806
|
mode: WorkspaceMountMode;
|
|
639
807
|
mountKey: string;
|
|
640
808
|
source: MountSource;
|
|
641
|
-
}):
|
|
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(
|
|
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: {
|
|
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 (
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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 =
|
|
1251
|
+
const slotDef = MANIFEST_LOGICAL_SLOTS[slotKey];
|
|
998
1252
|
if (!slotDef) continue;
|
|
999
1253
|
out.push({
|
|
1000
|
-
path: `${slotDef.
|
|
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
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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<
|
|
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 =
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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.
|
|
1313
|
+
out.push({ mount: slotDef.slot, mode });
|
|
1053
1314
|
}
|
|
1054
1315
|
return out;
|
|
1055
1316
|
}
|