spawnfile 0.1.4 → 0.1.6

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 (74) hide show
  1. package/dist/cli/lifecycleCommands.d.ts +3 -0
  2. package/dist/cli/lifecycleCommands.js +80 -0
  3. package/dist/cli/runCli.d.ts +2 -1
  4. package/dist/cli/runCli.js +4 -48
  5. package/dist/compiler/buildCompilePlan.js +12 -202
  6. package/dist/compiler/buildCompilePlanRuntime.d.ts +6 -1
  7. package/dist/compiler/buildCompilePlanRuntime.js +9 -0
  8. package/dist/compiler/buildCompilePlanTeams.js +16 -10
  9. package/dist/compiler/buildCompilePlanTraversal.d.ts +16 -0
  10. package/dist/compiler/buildCompilePlanTraversal.js +214 -0
  11. package/dist/compiler/buildCompilePlanTraversalHelpers.d.ts +18 -0
  12. package/dist/compiler/buildCompilePlanTraversalHelpers.js +22 -0
  13. package/dist/compiler/compilePlanHelpers.d.ts +3 -1
  14. package/dist/compiler/compilePlanHelpers.js +37 -1
  15. package/dist/compiler/compileProject.js +14 -0
  16. package/dist/compiler/containerArtifacts.js +18 -3
  17. package/dist/compiler/containerArtifactsPlans.js +32 -0
  18. package/dist/compiler/containerArtifactsRender.js +86 -3
  19. package/dist/compiler/containerArtifactsTypes.d.ts +7 -3
  20. package/dist/compiler/containerEntrypointRender.d.ts +1 -1
  21. package/dist/compiler/containerEntrypointRender.js +34 -24
  22. package/dist/compiler/containerTargetResources.d.ts +4 -0
  23. package/dist/compiler/containerTargetResources.js +54 -0
  24. package/dist/compiler/containerWorkspaceResourceRender.d.ts +3 -0
  25. package/dist/compiler/containerWorkspaceResourceRender.js +128 -0
  26. package/dist/compiler/executionDefaults.js +0 -3
  27. package/dist/compiler/helpers.d.ts +1 -1
  28. package/dist/compiler/index.d.ts +1 -0
  29. package/dist/compiler/index.js +1 -0
  30. package/dist/compiler/moltnetArtifacts.d.ts +11 -5
  31. package/dist/compiler/moltnetArtifacts.js +133 -117
  32. package/dist/compiler/moltnetClientConfig.js +8 -2
  33. package/dist/compiler/moltnetConfigLowering.d.ts +36 -0
  34. package/dist/compiler/moltnetConfigLowering.js +125 -0
  35. package/dist/compiler/moltnetRuntimeConfig.d.ts +2 -0
  36. package/dist/compiler/moltnetRuntimeConfig.js +69 -0
  37. package/dist/compiler/surfaces.d.ts +3 -13
  38. package/dist/compiler/surfaces.js +1 -6
  39. package/dist/compiler/syncProjectAuth.js +14 -0
  40. package/dist/compiler/types.d.ts +16 -1
  41. package/dist/compiler/upProject.d.ts +19 -0
  42. package/dist/compiler/upProject.js +37 -0
  43. package/dist/compiler/view/buildOrganizationView.js +22 -2
  44. package/dist/compiler/view/renderNetworks.js +14 -3
  45. package/dist/compiler/view/renderTree.js +8 -2
  46. package/dist/compiler/view/types.d.ts +18 -3
  47. package/dist/compiler/workspaceResources.d.ts +34 -0
  48. package/dist/compiler/workspaceResources.js +120 -0
  49. package/dist/filesystem/paths.js +1 -10
  50. package/dist/manifest/executionSchemas.d.ts +106 -0
  51. package/dist/manifest/executionSchemas.js +140 -0
  52. package/dist/manifest/loadManifest.js +15 -27
  53. package/dist/manifest/renderSpawnfile.js +44 -52
  54. package/dist/manifest/renderSpawnfileNetworks.d.ts +2 -0
  55. package/dist/manifest/renderSpawnfileNetworks.js +63 -0
  56. package/dist/manifest/renderSpawnfileWorkspace.d.ts +2 -0
  57. package/dist/manifest/renderSpawnfileWorkspace.js +47 -0
  58. package/dist/manifest/scaffold.js +12 -6
  59. package/dist/manifest/scheduleSchemas.d.ts +15 -0
  60. package/dist/manifest/scheduleSchemas.js +26 -0
  61. package/dist/manifest/schemas.d.ts +626 -368
  62. package/dist/manifest/schemas.js +51 -191
  63. package/dist/manifest/teamNetworkSchemas.d.ts +228 -0
  64. package/dist/manifest/teamNetworkSchemas.js +295 -0
  65. package/dist/manifest/workspaceSchemas.d.ts +96 -0
  66. package/dist/manifest/workspaceSchemas.js +166 -0
  67. package/dist/report/types.d.ts +10 -0
  68. package/dist/runtime/common.d.ts +2 -1
  69. package/dist/runtime/common.js +3 -3
  70. package/dist/runtime/tinyclaw/adapter.js +9 -2
  71. package/dist/runtime/tinyclaw/schedules.d.ts +9 -0
  72. package/dist/runtime/tinyclaw/schedules.js +62 -0
  73. package/dist/runtime/types.d.ts +1 -0
  74. package/package.json +2 -1
@@ -0,0 +1,214 @@
1
+ import { getCanonicalManifestPath, getManifestPath, resolveProjectPath } from "../filesystem/index.js";
2
+ import { isAgentManifest, isTeamManifest, mergeExecution } from "../manifest/index.js";
3
+ import { SpawnfileError } from "../shared/index.js";
4
+ import { getAgentFingerprint, getMcpNames, getTeamFingerprint, validateEffectiveSkillRequirements } from "./compilePlanHelpers.js";
5
+ import { resolveAgentSurfaces } from "./agentSurfaces.js";
6
+ import { mergeResolvedSkills, loadResolvedSkills } from "./surfaces.js";
7
+ import { assertRuntimeSupportsExecutionModelAuth } from "./modelAuth.js";
8
+ import { assertRuntimeSupportsAgentSurfaces } from "./surfaceSupport.js";
9
+ import { applyExecutionDefaults } from "./executionDefaults.js";
10
+ import { normalizeDescription, resolveDescription, resolveRuntime } from "./buildCompilePlanRuntime.js";
11
+ import { resolveTeamExternalIds, resolveTeamNetworks, validateTeamNetworkRooms } from "./buildCompilePlanTeams.js";
12
+ import { mergeWorkspaceResources } from "./workspaceResources.js";
13
+ import { DEFAULT_POLICY_MODE, DEFAULT_POLICY_ON_DEGRADE, mergeResolvedDocuments, resolveEffectiveEnvironment } from "./buildCompilePlanTraversalHelpers.js";
14
+ export const createCompilePlanTraversal = ({ getLoadedManifest, nodeCache, fingerprintCache, edges, memberships }) => {
15
+ const visitStack = [];
16
+ const visitAgent = async (manifestPath, context) => {
17
+ const canonicalPath = getCanonicalManifestPath(manifestPath);
18
+ if (visitStack.includes(canonicalPath)) {
19
+ throw new SpawnfileError("compile_error", `Cycle detected while visiting ${canonicalPath}`);
20
+ }
21
+ const loadedManifest = await getLoadedManifest(canonicalPath);
22
+ if (!isAgentManifest(loadedManifest.manifest)) {
23
+ throw new SpawnfileError("compile_error", `Expected agent manifest, got ${loadedManifest.manifest.kind} at ${canonicalPath}`);
24
+ }
25
+ const runtime = await resolveRuntime(loadedManifest.manifest, context);
26
+ const execution = applyExecutionDefaults(context.isSubagent
27
+ ? mergeExecution(context.inheritedExecution, loadedManifest.manifest.execution)
28
+ : loadedManifest.manifest.execution);
29
+ assertRuntimeSupportsExecutionModelAuth(runtime.name, execution, loadedManifest.manifest.name);
30
+ const environment = resolveEffectiveEnvironment(context.inheritedShared?.surface?.environment, loadedManifest.manifest.environment);
31
+ const inheritedSkills = context.inheritedShared
32
+ ? await loadResolvedSkills(context.inheritedShared.manifestPath, context.inheritedShared.surface?.workspace?.skills)
33
+ : [];
34
+ const localSkills = await loadResolvedSkills(canonicalPath, loadedManifest.manifest.workspace?.skills);
35
+ const skills = mergeResolvedSkills(inheritedSkills, localSkills);
36
+ validateEffectiveSkillRequirements(loadedManifest.manifest.name, getMcpNames(environment.mcpServers), skills);
37
+ const docs = await mergeResolvedDocuments(canonicalPath, loadedManifest.manifest.workspace?.docs, context.inheritedShared?.manifestPath, context.inheritedShared?.surface?.workspace?.docs);
38
+ const workspaceResources = mergeWorkspaceResources(context.inheritedResources, loadedManifest.manifest.workspace?.resources, loadedManifest.manifest.name, {
39
+ kind: "agent",
40
+ key: canonicalPath,
41
+ name: loadedManifest.manifest.name
42
+ });
43
+ const candidate = {
44
+ description: resolveDescription(loadedManifest.manifest.description, docs),
45
+ docs,
46
+ env: environment.env,
47
+ execution,
48
+ expose: loadedManifest.manifest.expose ?? false,
49
+ kind: "agent",
50
+ mcpServers: environment.mcpServers,
51
+ name: loadedManifest.manifest.name,
52
+ policyMode: loadedManifest.manifest.policy?.mode ?? DEFAULT_POLICY_MODE,
53
+ policyOnDegrade: loadedManifest.manifest.policy?.on_degrade ?? DEFAULT_POLICY_ON_DEGRADE,
54
+ runtime,
55
+ schedule: loadedManifest.manifest.schedule,
56
+ secrets: environment.secrets,
57
+ packages: environment.packages,
58
+ skills,
59
+ source: canonicalPath,
60
+ surfaces: resolveAgentSurfaces(loadedManifest.manifest.surfaces),
61
+ subagents: [],
62
+ workspaceResources
63
+ };
64
+ assertRuntimeSupportsAgentSurfaces(runtime.name, candidate.surfaces, loadedManifest.manifest.name);
65
+ const fingerprint = getAgentFingerprint(candidate);
66
+ const existingFingerprint = fingerprintCache.get(canonicalPath);
67
+ if (existingFingerprint && existingFingerprint !== fingerprint) {
68
+ throw new SpawnfileError("compile_error", `Manifest ${canonicalPath} resolves differently across compile contexts`);
69
+ }
70
+ const cachedNode = nodeCache.get(canonicalPath);
71
+ if (cachedNode) {
72
+ return cachedNode.value;
73
+ }
74
+ fingerprintCache.set(canonicalPath, fingerprint);
75
+ nodeCache.set(canonicalPath, {
76
+ runtimeName: runtime.name,
77
+ source: canonicalPath,
78
+ value: candidate
79
+ });
80
+ visitStack.push(canonicalPath);
81
+ for (const subagent of loadedManifest.manifest.subagents ?? []) {
82
+ const childManifestPath = getManifestPath(resolveProjectPath(canonicalPath, subagent.ref));
83
+ const resolvedSubagent = await visitAgent(childManifestPath, {
84
+ inheritedExecution: execution,
85
+ inheritedResources: candidate.workspaceResources,
86
+ inheritedRuntime: runtime,
87
+ isSubagent: true
88
+ });
89
+ candidate.subagents.push({
90
+ id: subagent.id,
91
+ nodeSource: resolvedSubagent.source
92
+ });
93
+ edges.push({
94
+ from: canonicalPath,
95
+ kind: "subagent",
96
+ label: subagent.id,
97
+ to: resolvedSubagent.source
98
+ });
99
+ }
100
+ visitStack.pop();
101
+ return candidate;
102
+ };
103
+ const visitTeam = async (manifestPath, inheritedResources = []) => {
104
+ const canonicalPath = getCanonicalManifestPath(manifestPath);
105
+ if (visitStack.includes(canonicalPath)) {
106
+ throw new SpawnfileError("compile_error", `Cycle detected while visiting ${canonicalPath}`);
107
+ }
108
+ const loadedManifest = await getLoadedManifest(canonicalPath);
109
+ if (!isTeamManifest(loadedManifest.manifest)) {
110
+ throw new SpawnfileError("compile_error", `Expected team manifest, got ${loadedManifest.manifest.kind} at ${canonicalPath}`);
111
+ }
112
+ const manifest = loadedManifest.manifest;
113
+ const sharedWorkspace = manifest.shared?.workspace;
114
+ const sharedEnvironment = manifest.shared?.environment;
115
+ const sharedSkills = await loadResolvedSkills(canonicalPath, sharedWorkspace?.skills);
116
+ validateEffectiveSkillRequirements(loadedManifest.manifest.name, getMcpNames(sharedEnvironment?.mcp_servers ?? []), sharedSkills);
117
+ const resolvedExternal = resolveTeamExternalIds(manifest);
118
+ const docs = await mergeResolvedDocuments(canonicalPath, sharedWorkspace?.docs, undefined, undefined);
119
+ const workspaceResources = mergeWorkspaceResources(inheritedResources, sharedWorkspace?.resources, manifest.name, {
120
+ kind: "team",
121
+ key: canonicalPath,
122
+ name: manifest.name
123
+ });
124
+ const candidate = {
125
+ description: manifest.description ? normalizeDescription(manifest.description) : "",
126
+ docs,
127
+ external: resolvedExternal,
128
+ externalExplicit: manifest.external !== undefined,
129
+ kind: "team",
130
+ lead: manifest.lead ?? null,
131
+ members: [],
132
+ mode: manifest.mode,
133
+ name: manifest.name,
134
+ networks: resolveTeamNetworks(manifest),
135
+ policyMode: manifest.policy?.mode ?? DEFAULT_POLICY_MODE,
136
+ policyOnDegrade: manifest.policy?.on_degrade ?? DEFAULT_POLICY_ON_DEGRADE,
137
+ workspaceResources,
138
+ shared: {
139
+ env: sharedEnvironment?.env ?? {},
140
+ mcpServers: sharedEnvironment?.mcp_servers ?? [],
141
+ packages: sharedEnvironment?.packages,
142
+ secrets: sharedEnvironment?.secrets ?? [],
143
+ skills: sharedSkills
144
+ },
145
+ source: canonicalPath,
146
+ };
147
+ const fingerprint = getTeamFingerprint(candidate);
148
+ const existingFingerprint = fingerprintCache.get(canonicalPath);
149
+ if (existingFingerprint && existingFingerprint !== fingerprint) {
150
+ throw new SpawnfileError("compile_error", `Team manifest ${canonicalPath} resolves differently across compile contexts`);
151
+ }
152
+ const cachedNode = nodeCache.get(canonicalPath);
153
+ if (cachedNode) {
154
+ return cachedNode.value;
155
+ }
156
+ fingerprintCache.set(canonicalPath, fingerprint);
157
+ nodeCache.set(canonicalPath, {
158
+ runtimeName: null,
159
+ source: canonicalPath,
160
+ value: candidate
161
+ });
162
+ visitStack.push(canonicalPath);
163
+ for (const member of loadedManifest.manifest.members) {
164
+ const childManifestPath = getManifestPath(resolveProjectPath(canonicalPath, member.ref));
165
+ const childManifest = await getLoadedManifest(childManifestPath);
166
+ let resolvedMember;
167
+ if (isAgentManifest(childManifest.manifest)) {
168
+ const resolvedAgent = await visitAgent(childManifestPath, {
169
+ inheritedShared: {
170
+ manifestPath: canonicalPath,
171
+ surface: loadedManifest.manifest.shared
172
+ },
173
+ inheritedResources: candidate.workspaceResources,
174
+ isSubagent: false
175
+ });
176
+ resolvedMember = {
177
+ id: member.id,
178
+ kind: "agent",
179
+ nodeSource: resolvedAgent.source,
180
+ runtimeName: resolvedAgent.runtime.name
181
+ };
182
+ memberships.set(`${canonicalPath}::${member.id}::${resolvedAgent.source}`, {
183
+ agentSource: resolvedAgent.source,
184
+ memberId: member.id,
185
+ teamName: candidate.name,
186
+ teamSource: canonicalPath
187
+ });
188
+ }
189
+ else {
190
+ const resolvedTeam = await visitTeam(childManifestPath, candidate.workspaceResources);
191
+ resolvedMember = {
192
+ id: member.id,
193
+ kind: "team",
194
+ nodeSource: resolvedTeam.source,
195
+ runtimeName: null
196
+ };
197
+ }
198
+ candidate.members.push(resolvedMember);
199
+ edges.push({
200
+ from: canonicalPath,
201
+ kind: "team_member",
202
+ label: member.id,
203
+ to: resolvedMember.nodeSource
204
+ });
205
+ }
206
+ validateTeamNetworkRooms(candidate);
207
+ visitStack.pop();
208
+ return candidate;
209
+ };
210
+ return {
211
+ visitAgent,
212
+ visitTeam
213
+ };
214
+ };
@@ -0,0 +1,18 @@
1
+ import { type Environment, type McpServer, type Secret, type TeamWorkspace } from "../manifest/index.js";
2
+ import { ResolvedDocument, ResolvedPackage, ResolvedAgentNode, ResolvedTeamNode } from "./types.js";
3
+ export declare const DEFAULT_POLICY_MODE = "warn";
4
+ export declare const DEFAULT_POLICY_ON_DEGRADE = "warn";
5
+ export type InternalNode = {
6
+ runtimeName: string | null;
7
+ source: string;
8
+ value: ResolvedAgentNode | ResolvedTeamNode;
9
+ };
10
+ type ResolvedPackageArray = ResolvedPackage[] | undefined;
11
+ export declare const mergeResolvedDocuments: (primaryPath: string, primaryDocs: TeamWorkspace["docs"] | undefined, fallbackPath: string | undefined, fallbackDocs: TeamWorkspace["docs"] | undefined) => Promise<ResolvedDocument[]>;
12
+ export declare const resolveEffectiveEnvironment: (sharedEnvironment: Environment | undefined, localEnvironment: Environment | undefined) => {
13
+ env: Record<string, string>;
14
+ mcpServers: McpServer[];
15
+ packages: ResolvedPackageArray;
16
+ secrets: Secret[];
17
+ };
18
+ export {};
@@ -0,0 +1,22 @@
1
+ import { loadResolvedDocuments, mergeEnv, mergeMcpServers, mergePackages, mergeSecrets } from "./surfaces.js";
2
+ export const DEFAULT_POLICY_MODE = "warn";
3
+ export const DEFAULT_POLICY_ON_DEGRADE = "warn";
4
+ export const mergeResolvedDocuments = async (primaryPath, primaryDocs, fallbackPath, fallbackDocs) => {
5
+ const fallbackResolved = fallbackPath && fallbackDocs
6
+ ? await loadResolvedDocuments(fallbackPath, fallbackDocs)
7
+ : [];
8
+ const primaryResolved = await loadResolvedDocuments(primaryPath, primaryDocs);
9
+ const merged = new Map(primaryResolved.map((doc) => [doc.role, doc]));
10
+ for (const doc of fallbackResolved) {
11
+ if (!merged.has(doc.role)) {
12
+ merged.set(doc.role, doc);
13
+ }
14
+ }
15
+ return [...merged.values()];
16
+ };
17
+ export const resolveEffectiveEnvironment = (sharedEnvironment, localEnvironment) => ({
18
+ env: mergeEnv(sharedEnvironment?.env, localEnvironment?.env),
19
+ mcpServers: mergeMcpServers(sharedEnvironment?.mcp_servers, localEnvironment?.mcp_servers),
20
+ packages: mergePackages(sharedEnvironment?.packages, localEnvironment?.packages),
21
+ secrets: mergeSecrets(sharedEnvironment?.secrets, localEnvironment?.secrets)
22
+ });
@@ -1,7 +1,9 @@
1
- import type { ResolvedAgentNode, ResolvedSkill, ResolvedTeamNode } from "./types.js";
1
+ export { mergePackages } from "./surfaces.js";
2
+ import type { CompilePlanNode, ResolvedAgentNode, ResolvedSkill, ResolvedTeamNode } from "./types.js";
2
3
  export declare const getMcpNames: (servers: Array<{
3
4
  name: string;
4
5
  }>) => Set<string>;
5
6
  export declare const validateEffectiveSkillRequirements: (nodeName: string, mcpNames: Set<string>, skills: ResolvedSkill[]) => void;
6
7
  export declare const getAgentFingerprint: (node: ResolvedAgentNode) => string;
7
8
  export declare const getTeamFingerprint: (node: ResolvedTeamNode) => string;
9
+ export declare const listMoltnetNetworkSecretNames: (nodes: CompilePlanNode[]) => string[];
@@ -1,5 +1,6 @@
1
1
  import { SpawnfileError } from "../shared/index.js";
2
2
  import { stableStringify } from "./helpers.js";
3
+ export { mergePackages } from "./surfaces.js";
3
4
  export const getMcpNames = (servers) => new Set(servers.map((server) => server.name));
4
5
  export const validateEffectiveSkillRequirements = (nodeName, mcpNames, skills) => {
5
6
  for (const skill of skills) {
@@ -15,13 +16,16 @@ export const getAgentFingerprint = (node) => stableStringify({
15
16
  execution: node.execution,
16
17
  mcpServers: node.mcpServers,
17
18
  runtime: node.runtime,
19
+ schedule: node.schedule,
20
+ packages: node.packages,
18
21
  secrets: node.secrets,
19
22
  skills: node.skills.map((skill) => ({
20
23
  name: skill.name,
21
24
  ref: skill.ref,
22
25
  requiresMcp: skill.requiresMcp
23
26
  })),
24
- surfaces: node.surfaces
27
+ surfaces: node.surfaces,
28
+ workspaceResources: node.workspaceResources
25
29
  });
26
30
  export const getTeamFingerprint = (node) => stableStringify({
27
31
  members: node.members,
@@ -29,9 +33,11 @@ export const getTeamFingerprint = (node) => stableStringify({
29
33
  lead: node.lead,
30
34
  external: node.external,
31
35
  networks: node.networks ?? [],
36
+ workspaceResources: node.workspaceResources,
32
37
  shared: {
33
38
  env: node.shared.env,
34
39
  mcpServers: node.shared.mcpServers,
40
+ packages: node.shared.packages,
35
41
  secrets: node.shared.secrets,
36
42
  skills: node.shared.skills.map((skill) => ({
37
43
  name: skill.name,
@@ -40,3 +46,33 @@ export const getTeamFingerprint = (node) => stableStringify({
40
46
  }))
41
47
  }
42
48
  });
49
+ const collectMoltnetSecretNames = (server, secretNames) => {
50
+ for (const token of server.auth.tokens ?? []) {
51
+ secretNames.add(token.secret);
52
+ }
53
+ if (server.mode === "managed") {
54
+ for (const pairing of server.pairings ?? []) {
55
+ secretNames.add(pairing.token_secret);
56
+ }
57
+ if (server.store.kind === "postgres") {
58
+ secretNames.add(server.store.dsn_secret);
59
+ }
60
+ }
61
+ if (server.auth.client?.token_env) {
62
+ secretNames.add(server.auth.client.token_env);
63
+ }
64
+ };
65
+ export const listMoltnetNetworkSecretNames = (nodes) => {
66
+ const secretNames = new Set();
67
+ for (const node of nodes) {
68
+ if (node.value.kind !== "team" || !node.value.networks) {
69
+ continue;
70
+ }
71
+ for (const network of node.value.networks) {
72
+ if (network.server) {
73
+ collectMoltnetSecretNames(network.server, secretNames);
74
+ }
75
+ }
76
+ }
77
+ return [...secretNames].sort();
78
+ };
@@ -136,11 +136,17 @@ const enforcePolicy = (nodeReport, policyMode, onDegrade) => {
136
136
  if (policyMode === "strict") {
137
137
  throw new Error(`Policy violation: ${capability.key} is unsupported for ${nodeReport.id} (strict mode)${capability.message ? `: ${capability.message}` : ""}`);
138
138
  }
139
+ if (policyMode === "warn") {
140
+ nodeReport.diagnostics.push(createDiagnostic("warn", `Policy warning: ${capability.key} is unsupported for ${nodeReport.id}${capability.message ? `: ${capability.message}` : ""}`));
141
+ }
139
142
  }
140
143
  if (capability.outcome === "degraded") {
141
144
  if (onDegrade === "error") {
142
145
  throw new Error(`Policy violation: ${capability.key} is degraded for ${nodeReport.id} (on_degrade: error)${capability.message ? `: ${capability.message}` : ""}`);
143
146
  }
147
+ if (onDegrade === "warn") {
148
+ nodeReport.diagnostics.push(createDiagnostic("warn", `Policy warning: ${capability.key} is degraded for ${nodeReport.id}${capability.message ? `: ${capability.message}` : ""}`));
149
+ }
144
150
  }
145
151
  }
146
152
  };
@@ -174,8 +180,16 @@ const createIdentityCapabilities = (node) => [
174
180
  }]
175
181
  : [])
176
182
  ];
183
+ const createWorkspaceResourceCapabilities = (node) => (node.workspaceResources?.length ?? 0) > 0
184
+ ? [{
185
+ key: "workspace.resources",
186
+ message: `${node.workspaceResources?.length ?? 0} workspace resource(s) will be prepared at startup`,
187
+ outcome: "supported"
188
+ }]
189
+ : [];
177
190
  const augmentNodeReports = (compiledNodes, support) => {
178
191
  for (const compiled of compiledNodes) {
192
+ compiled.report.capabilities.push(...createWorkspaceResourceCapabilities(compiled.value));
179
193
  if (compiled.value.kind === "team") {
180
194
  compiled.report.capabilities.push(...(support.capabilitiesByTeamSource.get(compiled.value.source) ?? []));
181
195
  compiled.report.diagnostics.push(...(support.diagnosticsByTeamSource.get(compiled.value.source) ?? []));
@@ -30,7 +30,7 @@ export const createContainerArtifacts = async (plan, compiledNodes, options = {}
30
30
  content: renderEntrypoint(runtimePlans, requiredSecrets.filter((secretName) => !modelSecretsRequired.includes(secretName)), {
31
31
  moltnet: options.moltnet
32
32
  ? {
33
- bridgePlans: options.moltnet.bridgePlans,
33
+ nodePlans: options.moltnet.nodePlans,
34
34
  serverPlans: options.moltnet.serverPlans
35
35
  }
36
36
  : undefined
@@ -59,13 +59,27 @@ export const createContainerArtifacts = async (plan, compiledNodes, options = {}
59
59
  }))
60
60
  .sort((left, right) => left.id.localeCompare(right.id));
61
61
  const runtimesInstalled = [...new Set(runtimePlans.map((plan) => plan.runtimeName))].sort();
62
+ const workspaceResources = [
63
+ ...new Map(runtimePlans.flatMap((plan) => (plan.resources ?? []).map((resource) => [
64
+ `${resource.kind}:${resource.id}:${resource.linkPath}`,
65
+ {
66
+ backing_path: resource.backingPath,
67
+ id: resource.id,
68
+ kind: resource.kind,
69
+ link_path: resource.linkPath,
70
+ mode: resource.mode,
71
+ mount: resource.mount,
72
+ sharing: resource.sharing
73
+ }
74
+ ]))).values()
75
+ ].sort((left, right) => left.link_path.localeCompare(right.link_path) || left.id.localeCompare(right.id));
62
76
  return {
63
77
  executablePaths: ["entrypoint.sh"],
64
78
  files,
65
79
  ...(options.moltnet
66
80
  ? {
67
81
  moltnet: {
68
- bridgePlans: options.moltnet.bridgePlans,
82
+ nodePlans: options.moltnet.nodePlans,
69
83
  serverPlans: options.moltnet.serverPlans
70
84
  }
71
85
  }
@@ -80,7 +94,8 @@ export const createContainerArtifacts = async (plan, compiledNodes, options = {}
80
94
  runtime_homes: runtimeHomes,
81
95
  runtime_secrets_required: runtimeSecretsRequired,
82
96
  runtimes_installed: runtimesInstalled,
83
- secrets_required: requiredSecrets
97
+ secrets_required: requiredSecrets,
98
+ ...(workspaceResources.length > 0 ? { workspace_resources: workspaceResources } : {})
84
99
  }
85
100
  };
86
101
  };
@@ -3,6 +3,7 @@ import { createRuntimeInstallRecipe, getRuntimeAdapter } from "../runtime/index.
3
3
  import { SpawnfileError } from "../shared/index.js";
4
4
  import { listAgentSurfaceSecretNames } from "./agentSurfaces.js";
5
5
  import { listExecutionModelSecretNames, resolveExecutionModelAuthMethods } from "./modelEnv.js";
6
+ import { resolveTargetResources } from "./containerTargetResources.js";
6
7
  const CONFIG_FILE_PLACEHOLDER = "<config-file>";
7
8
  const INSTANCE_ROOT_PLACEHOLDER = "<instance-root>";
8
9
  const createDefaultTargets = (inputs) => inputs.map((input) => ({
@@ -20,6 +21,34 @@ const assertTargetHasConfig = (runtimeName, targetId, meta, files) => {
20
21
  throw new SpawnfileError("runtime_error", `Container target ${targetId} for ${runtimeName} is missing ${meta.configFileName}`);
21
22
  }
22
23
  };
24
+ const createPackageIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}\u0000${pkg.version ?? ""}\u0000${pkg.scope ?? ""}`;
25
+ const createPackageConflictIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}`;
26
+ const createPackageLabel = (pkg) => `${pkg.manager} package ${pkg.name}`;
27
+ const resolveTargetPackages = (target, inputs) => {
28
+ const sourceIds = new Set(target.sourceIds ?? []);
29
+ if (sourceIds.size === 0) {
30
+ return [];
31
+ }
32
+ const byIdentity = new Map();
33
+ for (const input of inputs) {
34
+ if (!sourceIds.has(input.id) || input.value.kind !== "agent") {
35
+ continue;
36
+ }
37
+ const candidatePackages = input.value.packages ?? [];
38
+ for (const currentPackage of candidatePackages) {
39
+ const conflictIdentity = createPackageConflictIdentity(currentPackage);
40
+ const existingPackage = byIdentity.get(conflictIdentity);
41
+ if (!existingPackage) {
42
+ byIdentity.set(conflictIdentity, currentPackage);
43
+ continue;
44
+ }
45
+ if (createPackageIdentity(existingPackage) !== createPackageIdentity(currentPackage)) {
46
+ throw new SpawnfileError("validation_error", `Container target ${target.id} declares conflicting package definitions for ${createPackageLabel(currentPackage)}`);
47
+ }
48
+ }
49
+ }
50
+ return [...byIdentity.values()].sort((left, right) => createPackageIdentity(left).localeCompare(createPackageIdentity(right)));
51
+ };
23
52
  const replaceContainerPathTemplate = (template, instanceRoot, configFileName) => template
24
53
  .replaceAll(INSTANCE_ROOT_PLACEHOLDER, instanceRoot)
25
54
  .replaceAll(CONFIG_FILE_PLACEHOLDER, configFileName);
@@ -30,6 +59,7 @@ const resolveInstancePaths = (runtimeName, targetId, meta) => {
30
59
  homePath: meta.instancePaths.homePathTemplate
31
60
  ? replaceContainerPathTemplate(meta.instancePaths.homePathTemplate, instanceRoot, meta.configFileName)
32
61
  : undefined,
62
+ instanceRoot,
33
63
  workspacePath: replaceContainerPathTemplate(meta.instancePaths.workspacePathTemplate, instanceRoot, meta.configFileName)
34
64
  };
35
65
  };
@@ -149,6 +179,7 @@ export const createRuntimeTargetPlans = async (plan, compiledNodes) => {
149
179
  runtimePlans.push({
150
180
  configEnvBindings: resolveTargetConfigEnvBindings(adapter.container, target) ?? [],
151
181
  envFiles: resolveTargetEnvFiles(instancePaths.configPath, target),
182
+ packages: resolveTargetPackages(target, targetInputs),
152
183
  id: target.id,
153
184
  instancePaths,
154
185
  meta: adapter.container,
@@ -158,6 +189,7 @@ export const createRuntimeTargetPlans = async (plan, compiledNodes) => {
158
189
  publishedPort: resolveTargetExposure(target, targetInputs) && adapter.container.port
159
190
  ? adapter.container.port + (index * portStride)
160
191
  : undefined,
192
+ resources: resolveTargetResources(target, targetInputs, instancePaths, adapter.container),
161
193
  runtimeName,
162
194
  runtimeRoot: recipe.runtimeRoot,
163
195
  targetConfigEnvBindings: target.configEnvBindings,
@@ -11,6 +11,70 @@ const shellQuote = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
11
11
  const extractNodeMajorVersion = (image) => Number(image.match(/^node:(\d+)/)?.[1] ?? "0");
12
12
  const createPackageInstallCommand = (packages) => `RUN apt-get update && apt-get install -y --no-install-recommends ${packages.join(" ")} && rm -rf /var/lib/apt/lists/*`;
13
13
  const createNpmPackageInstallCommand = (packages) => `RUN npm install -g --omit=dev --no-fund --no-audit ${packages.join(" ")}`;
14
+ const createPipxPackageInstallCommand = (packages) => `RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ${packages.join(" ")}`;
15
+ const dedupePackages = (packages) => {
16
+ const seen = new Map();
17
+ for (const currentPackage of packages) {
18
+ seen.set(`${currentPackage.manager}\u0000${currentPackage.name}\u0000${currentPackage.version ?? ""}\u0000${currentPackage.scope ?? ""}`, currentPackage);
19
+ }
20
+ return [...seen.values()].sort((left, right) => `${left.manager}\u0000${left.name}\u0000${left.version ?? ""}\u0000${left.scope ?? ""}`.localeCompare(`${right.manager}\u0000${right.name}\u0000${right.version ?? ""}\u0000${right.scope ?? ""}`));
21
+ };
22
+ const createPackageIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}\u0000${pkg.version ?? ""}\u0000${pkg.scope ?? ""}`;
23
+ const createPackageConflictIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}`;
24
+ const assertImagePackageCompatibility = (packages) => {
25
+ const byName = new Map();
26
+ for (const currentPackage of packages) {
27
+ const conflictIdentity = createPackageConflictIdentity(currentPackage);
28
+ const existingPackage = byName.get(conflictIdentity);
29
+ if (!existingPackage) {
30
+ byName.set(conflictIdentity, currentPackage);
31
+ continue;
32
+ }
33
+ if (createPackageIdentity(existingPackage) !== createPackageIdentity(currentPackage)) {
34
+ throw new SpawnfileError("validation_error", `Generated container declares conflicting package definitions for ${currentPackage.manager} package ${currentPackage.name}`);
35
+ }
36
+ }
37
+ };
38
+ const createAptPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}=${pkg.version}` : pkg.name;
39
+ const createNpmPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name;
40
+ const createPipxPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}==${pkg.version}` : pkg.name;
41
+ const getNpmInstallItemName = (item) => {
42
+ if (!item.startsWith("@")) {
43
+ const versionMarker = item.indexOf("@");
44
+ return versionMarker === -1 ? item : item.slice(0, versionMarker);
45
+ }
46
+ const slashIndex = item.indexOf("/");
47
+ if (slashIndex === -1) {
48
+ return item;
49
+ }
50
+ const versionMarker = item.indexOf("@", slashIndex);
51
+ return versionMarker === -1 ? item : item.slice(0, versionMarker);
52
+ };
53
+ const collectPackagesByManager = (runtimePlans) => {
54
+ const packagesByManager = {
55
+ apt: [],
56
+ npm: [],
57
+ pipx: []
58
+ };
59
+ const imagePackages = runtimePlans.flatMap((plan) => plan.packages ?? []);
60
+ assertImagePackageCompatibility(imagePackages);
61
+ const resolvedPackages = dedupePackages(imagePackages);
62
+ for (const packageConfig of resolvedPackages) {
63
+ if (packageConfig.manager === "apt") {
64
+ packagesByManager.apt.push(packageConfig);
65
+ continue;
66
+ }
67
+ if (packageConfig.manager === "npm") {
68
+ packagesByManager.npm.push(packageConfig);
69
+ continue;
70
+ }
71
+ if (packageConfig.manager === "pipx") {
72
+ packagesByManager.pipx.push(packageConfig);
73
+ continue;
74
+ }
75
+ }
76
+ return packagesByManager;
77
+ };
14
78
  const selectBaseImage = (runtimePlans) => {
15
79
  const firstRuntimeMeta = runtimePlans[0]?.meta;
16
80
  if (runtimePlans.length <= 1) {
@@ -52,30 +116,49 @@ export const renderDockerfile = async (runtimePlans, options = {}) => {
52
116
  const runtimeRecipes = await Promise.all(runtimeNames.map((runtimeName) => createRuntimeInstallRecipe(runtimeName)));
53
117
  const baseImage = selectBaseImage(runtimePlans);
54
118
  const needsJsonEnvWriter = runtimePlans.some((plan) => (plan.configEnvBindings?.length ?? 0) > 0);
119
+ const needsGit = runtimePlans.some((plan) => (plan.resources ?? []).some((resource) => resource.kind === "git"));
55
120
  const systemDeps = [
56
121
  ...new Set([
57
122
  ...runtimePlans.flatMap((plan) => plan.meta.systemDeps),
123
+ ...(needsGit ? ["git"] : []),
58
124
  ...(options.hasMoltnet && !options.hasStagedMoltnetBinaries
59
125
  ? ["ca-certificates", "curl", "tar"]
60
126
  : []),
61
127
  ...(needsJsonEnvWriter ? ["python3"] : [])
62
128
  ])
63
129
  ].sort();
130
+ const { apt: aptPackages, npm: npmPackages, pipx: pipxPackages } = collectPackagesByManager(runtimePlans);
131
+ const aptDependencies = [
132
+ ...systemDeps,
133
+ ...aptPackages.map((pkg) => createAptPackageInstallItem(pkg)),
134
+ ...(pipxPackages.length > 0 ? ["pipx"] : [])
135
+ ];
136
+ const aptInstallPackages = [...new Set(aptDependencies)].sort();
137
+ const projectNpmPackages = npmPackages.map((pkg) => createNpmPackageInstallItem(pkg));
138
+ const projectNpmPackageNames = new Set(projectNpmPackages.map(getNpmInstallItemName));
139
+ const runtimeNpmPackages = runtimePlans
140
+ .flatMap((plan) => plan.meta.globalNpmPackages ?? [])
141
+ .filter((pkg) => !projectNpmPackageNames.has(getNpmInstallItemName(pkg)));
64
142
  const globalNpmPackages = [
65
- ...new Set(runtimePlans.flatMap((plan) => plan.meta.globalNpmPackages ?? []))
143
+ ...new Set([...runtimeNpmPackages, ...projectNpmPackages])
66
144
  ].sort();
145
+ const pipxInstallPackages = [...new Set(pipxPackages.map((pkg) => createPipxPackageInstallItem(pkg)))]
146
+ .sort();
67
147
  const runtimePorts = runtimePlans.flatMap((plan) => plan.publishedPort ? [plan.publishedPort] : []);
68
148
  const moltnetPorts = options.moltnetPublishedPorts ?? [];
69
149
  const exposedPorts = [...new Set([...runtimePorts, ...moltnetPorts])].sort((left, right) => left - right);
70
150
  const lines = [];
71
151
  lines.push(`FROM ${baseImage}`);
72
152
  lines.push("USER root", "", "WORKDIR /opt/spawnfile");
73
- if (systemDeps.length > 0) {
74
- lines.push(createPackageInstallCommand(systemDeps), "");
153
+ if (aptInstallPackages.length > 0) {
154
+ lines.push(createPackageInstallCommand(aptInstallPackages), "");
75
155
  }
76
156
  if (globalNpmPackages.length > 0) {
77
157
  lines.push(createNpmPackageInstallCommand(globalNpmPackages), "");
78
158
  }
159
+ if (pipxInstallPackages.length > 0) {
160
+ lines.push(createPipxPackageInstallCommand(pipxInstallPackages), "");
161
+ }
79
162
  if (options.hasMoltnet && options.hasStagedMoltnetBinaries) {
80
163
  lines.push(`COPY ${MOLTNET_BIN_DIRECTORY}/ /usr/local/bin/`, `RUN chmod +x ${MOLTNET_BINARY_NAMES.map((binaryName) => `/usr/local/bin/${binaryName}`).join(" ")}`, "");
81
164
  }
@@ -1,8 +1,9 @@
1
1
  import type { ContainerReport } from "../report/index.js";
2
2
  import type { EmittedFile, RuntimeContainerConfigEnvBinding, RuntimeContainerMeta } from "../runtime/index.js";
3
3
  import type { ModelAuthMethod } from "../shared/index.js";
4
- import type { ResolvedAgentNode, ResolvedTeamNode } from "./types.js";
5
- import type { MoltnetBridgePlan, MoltnetServerPlan } from "./moltnetArtifacts.js";
4
+ import type { ResolvedAgentNode, ResolvedPackage, ResolvedTeamNode } from "./types.js";
5
+ import type { MoltnetNodePlan, MoltnetServerPlan } from "./moltnetArtifacts.js";
6
+ import type { WorkspaceResourcePlan } from "./workspaceResources.js";
6
7
  export interface ContainerEnvVariable {
7
8
  categories: Array<"model" | "project" | "runtime" | "surface">;
8
9
  description: string;
@@ -11,6 +12,7 @@ export interface ContainerEnvVariable {
11
12
  }
12
13
  export interface RuntimeTargetPlan {
13
14
  configEnvBindings?: RuntimeContainerConfigEnvBinding[];
15
+ packages?: ResolvedPackage[];
14
16
  envFiles: Array<{
15
17
  envName: string;
16
18
  filePath: string;
@@ -19,6 +21,7 @@ export interface RuntimeTargetPlan {
19
21
  instancePaths: {
20
22
  configPath: string;
21
23
  homePath?: string;
24
+ instanceRoot?: string;
22
25
  workspacePath: string;
23
26
  };
24
27
  meta: RuntimeContainerMeta;
@@ -26,6 +29,7 @@ export interface RuntimeTargetPlan {
26
29
  modelSecretsRequired: string[];
27
30
  port?: number;
28
31
  publishedPort?: number;
32
+ resources?: WorkspaceResourcePlan[];
29
33
  runtimeName: string;
30
34
  runtimeRoot: string;
31
35
  targetConfigEnvBindings?: RuntimeContainerConfigEnvBinding[];
@@ -42,7 +46,7 @@ export interface GeneratedContainerArtifacts {
42
46
  executablePaths: string[];
43
47
  files: EmittedFile[];
44
48
  moltnet?: {
45
- bridgePlans: MoltnetBridgePlan[];
49
+ nodePlans: MoltnetNodePlan[];
46
50
  serverPlans: MoltnetServerPlan[];
47
51
  };
48
52
  report: ContainerReport;