spawnfile 0.1.3 → 0.1.5

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 (78) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/lifecycleCommands.d.ts +3 -0
  3. package/dist/cli/lifecycleCommands.js +80 -0
  4. package/dist/cli/runCli.d.ts +2 -1
  5. package/dist/cli/runCli.js +5 -47
  6. package/dist/compiler/buildCompilePlan.js +12 -202
  7. package/dist/compiler/buildCompilePlanRuntime.d.ts +6 -1
  8. package/dist/compiler/buildCompilePlanRuntime.js +9 -0
  9. package/dist/compiler/buildCompilePlanTeams.js +16 -10
  10. package/dist/compiler/buildCompilePlanTraversal.d.ts +16 -0
  11. package/dist/compiler/buildCompilePlanTraversal.js +214 -0
  12. package/dist/compiler/buildCompilePlanTraversalHelpers.d.ts +18 -0
  13. package/dist/compiler/buildCompilePlanTraversalHelpers.js +22 -0
  14. package/dist/compiler/compilePlanHelpers.d.ts +3 -1
  15. package/dist/compiler/compilePlanHelpers.js +37 -1
  16. package/dist/compiler/compileProject.js +14 -0
  17. package/dist/compiler/containerArtifacts.js +18 -3
  18. package/dist/compiler/containerArtifactsPlans.js +32 -0
  19. package/dist/compiler/containerArtifactsRender.js +86 -3
  20. package/dist/compiler/containerArtifactsTypes.d.ts +7 -3
  21. package/dist/compiler/containerEntrypointRender.d.ts +1 -1
  22. package/dist/compiler/containerEntrypointRender.js +37 -27
  23. package/dist/compiler/containerTargetResources.d.ts +4 -0
  24. package/dist/compiler/containerTargetResources.js +54 -0
  25. package/dist/compiler/containerWorkspaceResourceRender.d.ts +3 -0
  26. package/dist/compiler/containerWorkspaceResourceRender.js +128 -0
  27. package/dist/compiler/executionDefaults.js +0 -3
  28. package/dist/compiler/helpers.d.ts +1 -1
  29. package/dist/compiler/index.d.ts +1 -0
  30. package/dist/compiler/index.js +1 -0
  31. package/dist/compiler/moltnetArtifacts.d.ts +11 -5
  32. package/dist/compiler/moltnetArtifacts.js +133 -117
  33. package/dist/compiler/moltnetClientConfig.js +8 -2
  34. package/dist/compiler/moltnetConfigLowering.d.ts +36 -0
  35. package/dist/compiler/moltnetConfigLowering.js +125 -0
  36. package/dist/compiler/moltnetRuntimeConfig.d.ts +2 -0
  37. package/dist/compiler/moltnetRuntimeConfig.js +69 -0
  38. package/dist/compiler/runProject.d.ts +2 -0
  39. package/dist/compiler/runProject.js +20 -6
  40. package/dist/compiler/surfaces.d.ts +3 -13
  41. package/dist/compiler/surfaces.js +1 -6
  42. package/dist/compiler/syncProjectAuth.js +67 -19
  43. package/dist/compiler/types.d.ts +16 -1
  44. package/dist/compiler/upProject.d.ts +19 -0
  45. package/dist/compiler/upProject.js +37 -0
  46. package/dist/compiler/view/buildOrganizationView.js +22 -2
  47. package/dist/compiler/view/renderNetworks.js +14 -3
  48. package/dist/compiler/view/renderTree.js +8 -2
  49. package/dist/compiler/view/types.d.ts +18 -3
  50. package/dist/compiler/workspaceResources.d.ts +34 -0
  51. package/dist/compiler/workspaceResources.js +120 -0
  52. package/dist/manifest/executionSchemas.d.ts +106 -0
  53. package/dist/manifest/executionSchemas.js +140 -0
  54. package/dist/manifest/loadManifest.js +15 -27
  55. package/dist/manifest/renderSpawnfile.js +44 -52
  56. package/dist/manifest/renderSpawnfileNetworks.d.ts +2 -0
  57. package/dist/manifest/renderSpawnfileNetworks.js +63 -0
  58. package/dist/manifest/renderSpawnfileWorkspace.d.ts +2 -0
  59. package/dist/manifest/renderSpawnfileWorkspace.js +47 -0
  60. package/dist/manifest/scaffold.js +12 -6
  61. package/dist/manifest/scheduleSchemas.d.ts +15 -0
  62. package/dist/manifest/scheduleSchemas.js +26 -0
  63. package/dist/manifest/schemas.d.ts +626 -368
  64. package/dist/manifest/schemas.js +51 -191
  65. package/dist/manifest/teamNetworkSchemas.d.ts +228 -0
  66. package/dist/manifest/teamNetworkSchemas.js +295 -0
  67. package/dist/manifest/workspaceSchemas.d.ts +96 -0
  68. package/dist/manifest/workspaceSchemas.js +166 -0
  69. package/dist/report/types.d.ts +10 -0
  70. package/dist/runtime/common.d.ts +2 -1
  71. package/dist/runtime/common.js +3 -3
  72. package/dist/runtime/picoclaw/adapter.js +7 -0
  73. package/dist/runtime/picoclaw/runAuth.js +5 -41
  74. package/dist/runtime/tinyclaw/adapter.js +9 -2
  75. package/dist/runtime/tinyclaw/schedules.d.ts +9 -0
  76. package/dist/runtime/tinyclaw/schedules.js +62 -0
  77. package/dist/runtime/types.d.ts +1 -0
  78. package/package.json +2 -1
@@ -1,21 +1,11 @@
1
- import { DocsBlock, McpServer, Secret, SharedSurface, SkillReference } from "../manifest/index.js";
1
+ import { DocsBlock, McpServer, Secret, SkillReference } from "../manifest/index.js";
2
2
  import { StringMap } from "../shared/index.js";
3
- import { ResolvedDocument, ResolvedSkill } from "./types.js";
3
+ import { ResolvedDocument, ResolvedPackage, ResolvedSkill } from "./types.js";
4
4
  export declare const mergeSkills: (sharedSkills?: SkillReference[], localSkills?: SkillReference[]) => SkillReference[];
5
5
  export declare const mergeResolvedSkills: (sharedSkills?: ResolvedSkill[], localSkills?: ResolvedSkill[]) => ResolvedSkill[];
6
+ export declare const mergePackages: (sharedPackages?: ResolvedPackage[], localPackages?: ResolvedPackage[]) => ResolvedPackage[];
6
7
  export declare const mergeMcpServers: (sharedServers?: McpServer[], localServers?: McpServer[]) => McpServer[];
7
8
  export declare const mergeSecrets: (sharedSecrets?: Secret[], localSecrets?: Secret[]) => Secret[];
8
9
  export declare const mergeEnv: (sharedEnv?: StringMap, localEnv?: StringMap) => StringMap;
9
10
  export declare const loadResolvedDocuments: (manifestPath: string, docs: DocsBlock | undefined) => Promise<ResolvedDocument[]>;
10
11
  export declare const loadResolvedSkills: (manifestPath: string, skills?: SkillReference[]) => Promise<ResolvedSkill[]>;
11
- export declare const mergeSharedSurface: (shared: SharedSurface | undefined, local: {
12
- env: StringMap | undefined;
13
- mcpServers: McpServer[] | undefined;
14
- secrets: Secret[] | undefined;
15
- skills: SkillReference[] | undefined;
16
- }) => {
17
- env: StringMap;
18
- mcpServers: McpServer[];
19
- secrets: Secret[];
20
- skills: SkillReference[];
21
- };
@@ -12,6 +12,7 @@ const mergeByKey = (sharedValues, localValues, getKey) => {
12
12
  };
13
13
  export const mergeSkills = (sharedSkills = [], localSkills = []) => mergeByKey(sharedSkills, localSkills, (skill) => skill.ref);
14
14
  export const mergeResolvedSkills = (sharedSkills = [], localSkills = []) => mergeByKey(sharedSkills, localSkills, (skill) => skill.ref);
15
+ export const mergePackages = (sharedPackages = [], localPackages = []) => mergeByKey(sharedPackages, localPackages, (pkg) => `${pkg.manager}::${pkg.name}`);
15
16
  export const mergeMcpServers = (sharedServers = [], localServers = []) => mergeByKey(sharedServers, localServers, (server) => server.name);
16
17
  export const mergeSecrets = (sharedSecrets = [], localSecrets = []) => mergeByKey(sharedSecrets, localSecrets, (secret) => secret.name);
17
18
  export const mergeEnv = (sharedEnv = {}, localEnv = {}) => ({
@@ -51,9 +52,3 @@ export const loadResolvedSkills = async (manifestPath, skills = []) => Promise.a
51
52
  sourcePath
52
53
  };
53
54
  }));
54
- export const mergeSharedSurface = (shared, local) => ({
55
- env: mergeEnv(shared?.env, local.env),
56
- mcpServers: mergeMcpServers(shared?.mcp_servers, local.mcpServers),
57
- secrets: mergeSecrets(shared?.secrets, local.secrets),
58
- skills: mergeSkills(shared?.skills, local.skills)
59
- });
@@ -3,43 +3,82 @@ import { readUtf8File } from "../filesystem/index.js";
3
3
  import { SpawnfileError } from "../shared/index.js";
4
4
  import { listAgentSurfaceSecretNames } from "./agentSurfaces.js";
5
5
  import { buildCompilePlan } from "./buildCompilePlan.js";
6
+ import { listMoltnetNetworkSecretNames } from "./compilePlanHelpers.js";
6
7
  import { listExecutionModelSecretNames, resolveExecutionModelAuthMethods } from "./modelEnv.js";
7
8
  const resolveAuthRequirements = async (inputPath) => {
8
9
  const plan = await buildCompilePlan(inputPath);
9
10
  const methods = new Set();
10
- const envNames = new Set();
11
+ const optionalEnvNames = new Set();
12
+ const requiredEnvNames = new Set();
13
+ const addProjectSecret = (secret) => {
14
+ if (secret.required) {
15
+ requiredEnvNames.add(secret.name);
16
+ optionalEnvNames.delete(secret.name);
17
+ }
18
+ else if (!requiredEnvNames.has(secret.name)) {
19
+ optionalEnvNames.add(secret.name);
20
+ }
21
+ };
11
22
  for (const node of plan.nodes) {
12
23
  if (node.value.kind !== "agent") {
24
+ for (const secret of node.value.shared.secrets) {
25
+ addProjectSecret(secret);
26
+ }
27
+ for (const server of node.value.shared.mcpServers) {
28
+ if (server.auth?.secret) {
29
+ addProjectSecret({ name: server.auth.secret, required: true });
30
+ }
31
+ }
32
+ for (const secretName of listMoltnetNetworkSecretNames([node])) {
33
+ addProjectSecret({ name: secretName, required: true });
34
+ }
13
35
  continue;
14
36
  }
37
+ for (const secret of node.value.secrets) {
38
+ addProjectSecret(secret);
39
+ }
40
+ for (const server of node.value.mcpServers) {
41
+ if (server.auth?.secret) {
42
+ addProjectSecret({ name: server.auth.secret, required: true });
43
+ }
44
+ }
15
45
  for (const method of Object.values(resolveExecutionModelAuthMethods(node.value.execution))) {
16
46
  methods.add(method);
17
47
  }
18
48
  for (const envName of listExecutionModelSecretNames(node.value.execution)) {
19
- envNames.add(envName);
49
+ requiredEnvNames.add(envName);
50
+ optionalEnvNames.delete(envName);
20
51
  }
21
52
  for (const envName of listAgentSurfaceSecretNames(node.value.surfaces)) {
22
- envNames.add(envName);
53
+ requiredEnvNames.add(envName);
54
+ optionalEnvNames.delete(envName);
23
55
  }
24
56
  }
25
- return { envNames, methods };
57
+ return { methods, optionalEnvNames, requiredEnvNames };
58
+ };
59
+ const readEnvFile = async (envFilePath) => envFilePath ? parseEnvFile(await readUtf8File(envFilePath)) : {};
60
+ const resolveEnvValue = (envName, fileEnv) => {
61
+ const processValue = process.env[envName];
62
+ if (typeof processValue === "string" && processValue.length > 0) {
63
+ return processValue;
64
+ }
65
+ const fileValue = fileEnv[envName];
66
+ if (typeof fileValue === "string" && fileValue.length > 0) {
67
+ return fileValue;
68
+ }
69
+ return null;
26
70
  };
27
- const resolveRequiredEnv = async (envNames, envFilePath) => {
28
- if (envNames.size === 0) {
71
+ const resolveRequiredEnv = async (requiredEnvNames, optionalEnvNames, envFilePath) => {
72
+ if (requiredEnvNames.size === 0 && optionalEnvNames.size === 0) {
29
73
  return {};
30
74
  }
31
- const fileEnv = envFilePath ? parseEnvFile(await readUtf8File(envFilePath)) : {};
75
+ const fileEnv = await readEnvFile(envFilePath);
32
76
  const resolvedEnv = {};
33
77
  const missingEnv = [];
34
- for (const envName of [...envNames].sort()) {
35
- const processValue = process.env[envName];
36
- if (typeof processValue === "string" && processValue.length > 0) {
37
- resolvedEnv[envName] = processValue;
38
- continue;
39
- }
40
- const fileValue = fileEnv[envName];
41
- if (typeof fileValue === "string" && fileValue.length > 0) {
42
- resolvedEnv[envName] = fileValue;
78
+ for (const envName of [...requiredEnvNames].sort()) {
79
+ const value = resolveEnvValue(envName, fileEnv);
80
+ if (value !== null) {
81
+ resolvedEnv[envName] = value;
43
82
  continue;
44
83
  }
45
84
  missingEnv.push(envName);
@@ -47,10 +86,19 @@ const resolveRequiredEnv = async (envNames, envFilePath) => {
47
86
  if (missingEnv.length > 0) {
48
87
  throw new SpawnfileError("validation_error", `Missing required auth env: ${missingEnv.join(", ")}`);
49
88
  }
89
+ for (const envName of [...optionalEnvNames].sort()) {
90
+ if (requiredEnvNames.has(envName)) {
91
+ continue;
92
+ }
93
+ const value = resolveEnvValue(envName, fileEnv);
94
+ if (value !== null) {
95
+ resolvedEnv[envName] = value;
96
+ }
97
+ }
50
98
  return resolvedEnv;
51
99
  };
52
100
  export const syncProjectAuth = async (inputPath, options) => {
53
- const { envNames, methods } = await resolveAuthRequirements(inputPath);
101
+ const { methods, optionalEnvNames, requiredEnvNames } = await resolveAuthRequirements(inputPath);
54
102
  await ensureAuthProfile(options.profileName);
55
103
  if (methods.has("codex")) {
56
104
  await importCodexAuth(options.profileName, options.codexDirectory);
@@ -58,8 +106,8 @@ export const syncProjectAuth = async (inputPath, options) => {
58
106
  if (methods.has("claude-code")) {
59
107
  await importClaudeCodeAuth(options.profileName, options.claudeCodeDirectory);
60
108
  }
61
- if (envNames.size > 0) {
62
- await setAuthProfileEnv(options.profileName, await resolveRequiredEnv(envNames, options.envFilePath));
109
+ if (requiredEnvNames.size > 0 || optionalEnvNames.size > 0) {
110
+ await setAuthProfileEnv(options.profileName, await resolveRequiredEnv(requiredEnvNames, optionalEnvNames, options.envFilePath));
63
111
  }
64
112
  return requireAuthProfile(options.profileName);
65
113
  };
@@ -1,5 +1,6 @@
1
- import { ExecutionBlock, McpServer, ModelEndpoint, Secret } from "../manifest/index.js";
1
+ import { AgentSchedule, ExecutionBlock, McpServer, ModelEndpoint, Secret, TeamNetworkServer } from "../manifest/index.js";
2
2
  import { ModelAuthMethod, StringMap } from "../shared/index.js";
3
+ import type { ResolvedWorkspaceResource } from "./workspaceResources.js";
3
4
  export interface ResolvedDocument {
4
5
  content: string;
5
6
  role: string;
@@ -12,6 +13,13 @@ export interface ResolvedSkill {
12
13
  requiresMcp: string[];
13
14
  sourcePath: string;
14
15
  }
16
+ export interface ResolvedPackage {
17
+ id: string;
18
+ manager: string;
19
+ name: string;
20
+ scope?: string;
21
+ version?: string;
22
+ }
15
23
  export interface ResolvedRuntime {
16
24
  name: string;
17
25
  options: Record<string, unknown>;
@@ -104,6 +112,7 @@ export interface ResolvedAgentSurfaces {
104
112
  export interface ResolvedTeamNetworkRoom {
105
113
  id: string;
106
114
  members: string[];
115
+ name?: string;
107
116
  }
108
117
  export interface ResolvedTeamNetwork {
109
118
  expose?: boolean;
@@ -111,6 +120,7 @@ export interface ResolvedTeamNetwork {
111
120
  name: string;
112
121
  provider: "moltnet";
113
122
  rooms: ResolvedTeamNetworkRoom[];
123
+ server?: TeamNetworkServer;
114
124
  }
115
125
  export interface EffectiveModelTarget {
116
126
  auth: {
@@ -140,14 +150,17 @@ export interface ResolvedAgentNode {
140
150
  kind: "agent";
141
151
  mcpServers: McpServer[];
142
152
  name: string;
153
+ packages?: ResolvedPackage[];
143
154
  policyMode: string | null;
144
155
  policyOnDegrade: string | null;
145
156
  runtime: ResolvedRuntime;
157
+ schedule?: AgentSchedule;
146
158
  secrets: Secret[];
147
159
  skills: ResolvedSkill[];
148
160
  source: string;
149
161
  surfaces?: ResolvedAgentSurfaces;
150
162
  subagents: ResolvedSubagentRef[];
163
+ workspaceResources?: ResolvedWorkspaceResource[];
151
164
  }
152
165
  export interface ResolvedTeamNode {
153
166
  description: string;
@@ -162,11 +175,13 @@ export interface ResolvedTeamNode {
162
175
  networks?: ResolvedTeamNetwork[];
163
176
  policyMode: string | null;
164
177
  policyOnDegrade: string | null;
178
+ workspaceResources?: ResolvedWorkspaceResource[];
165
179
  shared: {
166
180
  env: StringMap;
167
181
  mcpServers: McpServer[];
168
182
  secrets: Secret[];
169
183
  skills: ResolvedSkill[];
184
+ packages?: ResolvedPackage[];
170
185
  };
171
186
  source: string;
172
187
  }
@@ -0,0 +1,19 @@
1
+ import { type BuildProjectResult, type DockerBuildRunner } from "./buildProject.js";
2
+ import { type DockerRunRunner } from "./runProject.js";
3
+ import { CompileProjectOptions } from "./compileProject.js";
4
+ export interface UpProjectOptions extends CompileProjectOptions {
5
+ authProfile?: string;
6
+ buildRunner?: DockerBuildRunner;
7
+ containerName?: string;
8
+ detach?: boolean;
9
+ dockerCommand?: string;
10
+ envFilePath?: string;
11
+ imageTag?: string;
12
+ runRunner?: DockerRunRunner;
13
+ }
14
+ export interface UpProjectResult extends BuildProjectResult {
15
+ authProfileName: string | null;
16
+ containerName: string | null;
17
+ supportDirectory: string | null;
18
+ }
19
+ export declare const upProject: (inputPath: string, options?: UpProjectOptions) => Promise<UpProjectResult>;
@@ -0,0 +1,37 @@
1
+ import { removeDirectory } from "../filesystem/index.js";
2
+ import { buildProject } from "./buildProject.js";
3
+ import { createDockerRunInvocation, runDockerContainer } from "./runProject.js";
4
+ import { requireAuthProfile } from "../auth/index.js";
5
+ export const upProject = async (inputPath, options = {}) => {
6
+ const buildResult = await buildProject(inputPath, {
7
+ buildRunner: options.buildRunner,
8
+ clean: options.clean,
9
+ dockerCommand: options.dockerCommand,
10
+ imageTag: options.imageTag,
11
+ outputDirectory: options.outputDirectory
12
+ });
13
+ const authProfile = options.authProfile
14
+ ? await requireAuthProfile(options.authProfile)
15
+ : null;
16
+ const invocation = await createDockerRunInvocation(buildResult, buildResult.imageTag, {
17
+ authProfile,
18
+ containerName: options.containerName,
19
+ detach: options.detach,
20
+ dockerCommand: options.dockerCommand,
21
+ envFilePath: options.envFilePath
22
+ });
23
+ try {
24
+ await (options.runRunner ?? runDockerContainer)(invocation);
25
+ }
26
+ finally {
27
+ if (!invocation.detach) {
28
+ await removeDirectory(invocation.supportDirectory);
29
+ }
30
+ }
31
+ return {
32
+ ...buildResult,
33
+ authProfileName: authProfile?.name ?? null,
34
+ containerName: invocation.containerName,
35
+ supportDirectory: invocation.supportDirectory
36
+ };
37
+ };
@@ -30,10 +30,15 @@ const buildTreeNetworkSummaries = (node) => {
30
30
  return [];
31
31
  }
32
32
  return (node.value.networks ?? []).map((network) => ({
33
- expose: network.expose ?? false,
33
+ authMode: network.server?.auth.mode,
34
+ directMessages: network.server?.mode === "managed" ? network.server.direct_messages : undefined,
35
+ expose: network.server?.mode === "managed" ? network.server.human_ingress : undefined,
36
+ httpEnabled: network.server?.mode === "managed" ? network.server.human_ingress === true : false,
34
37
  id: network.id,
35
38
  name: network.name,
36
39
  provider: network.provider,
40
+ serverMode: network.server?.mode,
41
+ url: network.server?.url,
37
42
  rooms: network.rooms.map((room) => ({
38
43
  declaredMembers: [...room.members],
39
44
  id: room.id
@@ -101,10 +106,15 @@ const toNetworkMemberView = (membership) => ({
101
106
  });
102
107
  const createNetworkKey = (provider, networkId) => `${provider}::${networkId}`;
103
108
  const buildNetworkDeclaration = (teamNode, network, roomMemberships) => ({
109
+ authMode: network.server?.auth.mode,
104
110
  declaringTeamName: teamNode.name,
105
111
  declaringTeamSource: teamNode.source,
106
- expose: network.expose ?? false,
112
+ directMessages: network.server?.mode === "managed" ? network.server.direct_messages : undefined,
113
+ expose: network.server?.mode === "managed" ? network.server.human_ingress : undefined,
114
+ httpEnabled: network.server?.mode === "managed" ? network.server.human_ingress === true : false,
107
115
  name: network.name,
116
+ serverMode: network.server?.mode,
117
+ url: network.server?.url,
108
118
  rooms: network.rooms.map((room) => {
109
119
  const members = roomMemberships
110
120
  .filter((membership) => membership.declaringTeamSource === teamNode.source
@@ -139,10 +149,15 @@ const buildNetworks = (plan) => {
139
149
  declaringTeamName: declaration.declaringTeamName,
140
150
  declaringTeamSource: declaration.declaringTeamSource,
141
151
  declarations: [declaration],
152
+ authMode: declaration.authMode,
153
+ directMessages: declaration.directMessages,
142
154
  expose: declaration.expose,
155
+ httpEnabled: declaration.httpEnabled,
143
156
  id: network.id,
144
157
  name: declaration.name,
145
158
  provider: network.provider,
159
+ serverMode: declaration.serverMode,
160
+ url: declaration.url,
146
161
  rooms: declaration.rooms
147
162
  });
148
163
  }
@@ -153,8 +168,13 @@ const buildNetworks = (plan) => {
153
168
  if (firstDeclaration) {
154
169
  network.declaringTeamName = firstDeclaration.declaringTeamName;
155
170
  network.declaringTeamSource = firstDeclaration.declaringTeamSource;
171
+ network.authMode = firstDeclaration.authMode;
172
+ network.directMessages = firstDeclaration.directMessages;
156
173
  network.expose = firstDeclaration.expose;
174
+ network.httpEnabled = firstDeclaration.httpEnabled;
157
175
  network.name = firstDeclaration.name;
176
+ network.serverMode = firstDeclaration.serverMode;
177
+ network.url = firstDeclaration.url;
158
178
  network.rooms = firstDeclaration.rooms;
159
179
  }
160
180
  }
@@ -40,19 +40,30 @@ const formatNetwork = (network, options) => {
40
40
  };
41
41
  const getDeclarations = (network) => network.declarations ?? [
42
42
  {
43
+ authMode: network.authMode,
43
44
  declaringTeamName: network.declaringTeamName,
44
45
  declaringTeamSource: network.declaringTeamSource,
46
+ directMessages: network.directMessages,
45
47
  expose: network.expose,
48
+ httpEnabled: network.httpEnabled,
46
49
  name: network.name,
47
- rooms: network.rooms
50
+ rooms: network.rooms,
51
+ serverMode: network.serverMode,
52
+ url: network.url
48
53
  }
49
54
  ];
50
55
  const formatDeclaration = (network, declaration, options, projectRoot) => {
51
- const exposed = declaration.expose ? " exposed" : "";
56
+ const metadata = [
57
+ declaration.serverMode ? `server=${declaration.serverMode}` : undefined,
58
+ declaration.url ? `url=${declaration.url}` : undefined,
59
+ declaration.authMode ? `auth=${declaration.authMode}` : undefined,
60
+ declaration.directMessages === false ? "dms=disabled" : undefined,
61
+ declaration.expose === true || declaration.httpEnabled ? "human_ingress" : undefined
62
+ ].filter((entry) => entry !== undefined);
52
63
  const source = options.paths
53
64
  ? formatSourceMeta("declared_source", declaration.declaringTeamSource, projectRoot)
54
65
  : "";
55
- return `${network.id} "${declaration.name}" on ${declaration.declaringTeamName}${exposed}${source}`;
66
+ return `${network.id} "${declaration.name}" on ${declaration.declaringTeamName}${metadata.length > 0 ? ` ${metadata.join(" ")}` : ""}${source}`;
56
67
  };
57
68
  export const renderOrganizationNetworks = (view, options = {}) => {
58
69
  if (view.networks.length === 0) {
@@ -24,11 +24,17 @@ const formatEdgeLabel = (edge) => edge.relation === "subagent"
24
24
  : edge.label;
25
25
  const formatNetworkSummary = (network, options) => {
26
26
  const id = color(network.id, "36", options);
27
- const exposed = network.expose ? " exposed" : "";
27
+ const metadata = [
28
+ network.serverMode ? `server=${network.serverMode}` : undefined,
29
+ network.url ? `url=${network.url}` : undefined,
30
+ network.authMode ? `auth=${network.authMode}` : undefined,
31
+ network.directMessages === false ? "dms=disabled" : undefined,
32
+ network.expose === true || network.httpEnabled ? "human_ingress" : undefined
33
+ ].filter((entry) => entry !== undefined);
28
34
  const rooms = network.rooms
29
35
  .map((room) => `${room.id} [${room.declaredMembers.join(", ")}]`)
30
36
  .join("; ");
31
- return `network ${id} "${network.name}"${exposed}: ${rooms}`;
37
+ return `network ${id} "${network.name}"${metadata.length > 0 ? ` ${metadata.join(" ")}` : ""}: ${rooms}`;
32
38
  };
33
39
  const renderChildren = (node, options, projectRoot, prefix = "") => {
34
40
  const glyphs = options.ascii
@@ -17,10 +17,15 @@ export interface OrganizationTreeNetworkRoomSummary {
17
17
  id: string;
18
18
  }
19
19
  export interface OrganizationTreeNetworkSummary {
20
- expose: boolean;
20
+ authMode?: "bearer" | "none" | "open";
21
+ directMessages?: boolean;
22
+ expose?: boolean;
23
+ httpEnabled?: boolean;
21
24
  id: string;
22
25
  name: string;
23
26
  provider: "moltnet";
27
+ serverMode?: "external" | "managed";
28
+ url?: string;
24
29
  rooms: OrganizationTreeNetworkRoomSummary[];
25
30
  }
26
31
  export interface OrganizationViewTreeEdge {
@@ -42,11 +47,16 @@ export interface OrganizationNetworkMemberView {
42
47
  representativePath?: string[];
43
48
  }
44
49
  export interface OrganizationNetworkDeclarationView {
50
+ authMode?: "bearer" | "none" | "open";
45
51
  declaringTeamName: string;
46
52
  declaringTeamSource: string;
47
- expose: boolean;
53
+ directMessages?: boolean;
54
+ expose?: boolean;
55
+ httpEnabled?: boolean;
48
56
  name: string;
49
57
  rooms: OrganizationNetworkRoomView[];
58
+ serverMode?: "external" | "managed";
59
+ url?: string;
50
60
  }
51
61
  export interface OrganizationNetworkRoomView {
52
62
  declaredMembers: string[];
@@ -54,13 +64,18 @@ export interface OrganizationNetworkRoomView {
54
64
  members: OrganizationNetworkMemberView[];
55
65
  }
56
66
  export interface OrganizationNetworkView {
67
+ authMode?: "bearer" | "none" | "open";
57
68
  declaringTeamName: string;
58
69
  declaringTeamSource: string;
59
- expose: boolean;
70
+ directMessages?: boolean;
71
+ expose?: boolean;
72
+ httpEnabled?: boolean;
60
73
  id: string;
61
74
  name: string;
62
75
  provider: "moltnet";
63
76
  rooms: OrganizationNetworkRoomView[];
77
+ serverMode?: "external" | "managed";
78
+ url?: string;
64
79
  declarations?: OrganizationNetworkDeclarationView[];
65
80
  }
66
81
  export interface OrganizationView {
@@ -0,0 +1,34 @@
1
+ import type { TeamWorkspaceResource } from "../manifest/index.js";
2
+ export type WorkspaceResourceSharing = "per_agent" | "team";
3
+ export interface WorkspaceResourceScope {
4
+ kind: "agent" | "team";
5
+ key: string;
6
+ name: string;
7
+ }
8
+ export type ResolvedWorkspaceResource = TeamWorkspaceResource & {
9
+ scope: WorkspaceResourceScope;
10
+ sharing: WorkspaceResourceSharing;
11
+ };
12
+ export interface WorkspaceResourcePlan {
13
+ backingPath: string;
14
+ branch?: string;
15
+ id: string;
16
+ kind: "git" | "volume";
17
+ linkPath: string;
18
+ mode: "mutable" | "readonly";
19
+ mount: string;
20
+ name?: string;
21
+ ref?: string;
22
+ sharing: WorkspaceResourceSharing;
23
+ tag?: string;
24
+ url?: string;
25
+ }
26
+ export declare const mergeWorkspaceResources: (inherited: ResolvedWorkspaceResource[] | undefined, local: TeamWorkspaceResource[] | undefined, ownerName: string, ownerScope: WorkspaceResourceScope) => ResolvedWorkspaceResource[];
27
+ export declare const toWorkspaceResourcePlan: (resource: ResolvedWorkspaceResource, context: {
28
+ targetId: string;
29
+ workspacePath: string;
30
+ }) => WorkspaceResourcePlan;
31
+ export declare const mergeWorkspaceResourcePlans: (resources: ResolvedWorkspaceResource[], ownerName: string, context: {
32
+ targetId: string;
33
+ workspacePath: string;
34
+ }) => WorkspaceResourcePlan[];
@@ -0,0 +1,120 @@
1
+ import path from "node:path";
2
+ import { SpawnfileError } from "../shared/index.js";
3
+ import { createShortHash, slugify } from "./helpers.js";
4
+ const normalizeMount = (value) => {
5
+ const trimmed = value.trim();
6
+ const workspaceRelative = trimmed.startsWith("${workspace}/")
7
+ ? `./${trimmed.slice("${workspace}/".length)}`
8
+ : trimmed;
9
+ const collapsed = workspaceRelative.replace(/\/+/g, "/");
10
+ if (collapsed.startsWith("./")) {
11
+ const relativePath = collapsed.slice(2).replace(/\/+$/u, "");
12
+ return `./${relativePath}`;
13
+ }
14
+ return collapsed.length > 1 ? collapsed.replace(/\/+$/u, "") : "/";
15
+ };
16
+ const normalizeResourceIdentity = (resource) => {
17
+ if (resource.kind === "git") {
18
+ return JSON.stringify({
19
+ branch: resource.branch?.trim() ?? "",
20
+ kind: "git",
21
+ mode: resource.mode,
22
+ mount: normalizeMount(resource.mount),
23
+ ref: resource.ref?.trim() ?? "",
24
+ sharing: resource.sharing,
25
+ tag: resource.tag?.trim() ?? "",
26
+ url: resource.url.trim()
27
+ });
28
+ }
29
+ return JSON.stringify({
30
+ kind: "volume",
31
+ mode: resource.mode,
32
+ mount: normalizeMount(resource.mount),
33
+ name: resource.name?.trim() ?? "",
34
+ scope: resource.sharing === "team" ? resource.scope.key : "",
35
+ sharing: resource.sharing
36
+ });
37
+ };
38
+ const mountsOverlap = (left, right) => left === right ||
39
+ left.startsWith(`${right}/`) ||
40
+ right.startsWith(`${left}/`);
41
+ const resolveSharing = (resource) => resource.sharing ?? "per_agent";
42
+ const toResolvedResource = (resource, scope) => ({
43
+ ...resource,
44
+ mount: normalizeMount(resource.mount),
45
+ scope,
46
+ sharing: resolveSharing(resource)
47
+ });
48
+ const createPathSegment = (value) => {
49
+ const slug = slugify(value);
50
+ const hash = createShortHash(value);
51
+ return slug ? `${slug}-${hash}` : hash;
52
+ };
53
+ const createScopeSegment = (scope) => `${scope.kind}-${createPathSegment(`${scope.name}:${scope.key}`)}`;
54
+ const createResourceSegment = (resource) => createPathSegment(resource.kind === "volume" && resource.name ? resource.name : resource.id);
55
+ const resolveLinkPath = (mount, workspacePath) => mount.startsWith("./")
56
+ ? path.posix.join(workspacePath, mount.slice(2))
57
+ : mount;
58
+ const resolveBackingPath = (resource, targetId) => {
59
+ const resourceSegment = createResourceSegment(resource);
60
+ if (resource.sharing === "team") {
61
+ return path.posix.join("/var/lib/spawnfile/resources/teams", createScopeSegment(resource.scope), resourceSegment);
62
+ }
63
+ return path.posix.join("/var/lib/spawnfile/resources/instances", createPathSegment(targetId), resourceSegment);
64
+ };
65
+ export const mergeWorkspaceResources = (inherited = [], local = [], ownerName, ownerScope) => {
66
+ const merged = [];
67
+ const identityById = new Map();
68
+ const resources = [
69
+ ...inherited,
70
+ ...local.map((resource) => toResolvedResource(resource, ownerScope))
71
+ ];
72
+ for (const resource of resources) {
73
+ const mount = normalizeMount(resource.mount);
74
+ const identity = normalizeResourceIdentity(resource);
75
+ const existingIdentity = identityById.get(resource.id);
76
+ if (existingIdentity) {
77
+ if (existingIdentity !== identity) {
78
+ throw new SpawnfileError("validation_error", `Workspace resource ${resource.id} resolves differently for ${ownerName}`);
79
+ }
80
+ continue;
81
+ }
82
+ const overlapping = merged.find((candidate) => candidate.id !== resource.id &&
83
+ mountsOverlap(normalizeMount(candidate.mount), mount));
84
+ if (overlapping) {
85
+ throw new SpawnfileError("validation_error", `Workspace resources ${overlapping.id} and ${resource.id} use overlapping mounts for ${ownerName}`);
86
+ }
87
+ identityById.set(resource.id, identity);
88
+ merged.push({ ...resource, mount });
89
+ }
90
+ return merged.sort((left, right) => left.id.localeCompare(right.id));
91
+ };
92
+ export const toWorkspaceResourcePlan = (resource, context) => resource.kind === "git"
93
+ ? {
94
+ backingPath: resolveBackingPath(resource, context.targetId),
95
+ ...(resource.branch ? { branch: resource.branch } : {}),
96
+ id: resource.id,
97
+ kind: "git",
98
+ linkPath: resolveLinkPath(normalizeMount(resource.mount), context.workspacePath),
99
+ mode: resource.mode,
100
+ mount: normalizeMount(resource.mount),
101
+ ...(resource.ref ? { ref: resource.ref } : {}),
102
+ sharing: resource.sharing,
103
+ ...(resource.tag ? { tag: resource.tag } : {}),
104
+ url: resource.url
105
+ }
106
+ : {
107
+ backingPath: resolveBackingPath(resource, context.targetId),
108
+ id: resource.id,
109
+ kind: "volume",
110
+ linkPath: resolveLinkPath(normalizeMount(resource.mount), context.workspacePath),
111
+ mode: resource.mode,
112
+ mount: normalizeMount(resource.mount),
113
+ ...(resource.name ? { name: resource.name } : {}),
114
+ sharing: resource.sharing
115
+ };
116
+ export const mergeWorkspaceResourcePlans = (resources, ownerName, context) => mergeWorkspaceResources(resources, [], ownerName, {
117
+ kind: "agent",
118
+ key: context.targetId,
119
+ name: context.targetId
120
+ }).map((resource) => toWorkspaceResourcePlan(resource, context));