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
@@ -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));
@@ -1,6 +1,5 @@
1
1
  import path from "node:path";
2
2
  import { SpawnfileError } from "../shared/index.js";
3
- const INVALID_PATH_SEGMENT = "..";
4
3
  export const assertPortableRelativePath = (inputPath) => {
5
4
  if (inputPath.includes("\\")) {
6
5
  throw new SpawnfileError("validation_error", `Paths must use forward slashes: ${inputPath}`);
@@ -8,10 +7,6 @@ export const assertPortableRelativePath = (inputPath) => {
8
7
  if (path.isAbsolute(inputPath)) {
9
8
  throw new SpawnfileError("validation_error", `Absolute paths are not allowed: ${inputPath}`);
10
9
  }
11
- const segments = inputPath.split("/");
12
- if (segments.includes(INVALID_PATH_SEGMENT)) {
13
- throw new SpawnfileError("validation_error", `Path traversal is not allowed: ${inputPath}`);
14
- }
15
10
  };
16
11
  export const getCanonicalManifestPath = (filePath) => path.resolve(filePath);
17
12
  export const getManifestPath = (inputPath) => path.basename(inputPath) === "Spawnfile"
@@ -21,10 +16,6 @@ export const getProjectRoot = (manifestPath) => path.dirname(manifestPath);
21
16
  export const resolveProjectPath = (manifestPath, relativePath) => {
22
17
  assertPortableRelativePath(relativePath);
23
18
  const projectRoot = getProjectRoot(manifestPath);
24
- const resolved = path.resolve(projectRoot, relativePath);
25
- if (!resolved.startsWith(projectRoot + path.sep) && resolved !== projectRoot) {
26
- throw new SpawnfileError("validation_error", `Path escapes project root: ${relativePath}`);
27
- }
28
- return resolved;
19
+ return path.resolve(projectRoot, relativePath);
29
20
  };
30
21
  export const toPosixPath = (value) => value.split(path.sep).join("/");
@@ -0,0 +1,106 @@
1
+ import { z } from "zod";
2
+ export declare const modelEntryAuthSchema: z.ZodObject<{
3
+ key: z.ZodOptional<z.ZodString>;
4
+ method: z.ZodOptional<z.ZodEnum<{
5
+ api_key: "api_key";
6
+ "claude-code": "claude-code";
7
+ codex: "codex";
8
+ none: "none";
9
+ }>>;
10
+ }, z.core.$strict>;
11
+ export declare const modelEndpointSchema: z.ZodObject<{
12
+ base_url: z.ZodString;
13
+ compatibility: z.ZodEnum<{
14
+ anthropic: "anthropic";
15
+ openai: "openai";
16
+ }>;
17
+ }, z.core.$strict>;
18
+ export declare const modelTargetSchema: z.ZodObject<{
19
+ auth: z.ZodOptional<z.ZodObject<{
20
+ key: z.ZodOptional<z.ZodString>;
21
+ method: z.ZodOptional<z.ZodEnum<{
22
+ api_key: "api_key";
23
+ "claude-code": "claude-code";
24
+ codex: "codex";
25
+ none: "none";
26
+ }>>;
27
+ }, z.core.$strict>>;
28
+ endpoint: z.ZodOptional<z.ZodObject<{
29
+ base_url: z.ZodString;
30
+ compatibility: z.ZodEnum<{
31
+ anthropic: "anthropic";
32
+ openai: "openai";
33
+ }>;
34
+ }, z.core.$strict>>;
35
+ name: z.ZodString;
36
+ provider: z.ZodString;
37
+ }, z.core.$strict>;
38
+ export declare const executionSchema: z.ZodObject<{
39
+ model: z.ZodOptional<z.ZodObject<{
40
+ auth: z.ZodOptional<z.ZodObject<{
41
+ method: z.ZodOptional<z.ZodEnum<{
42
+ api_key: "api_key";
43
+ "claude-code": "claude-code";
44
+ codex: "codex";
45
+ none: "none";
46
+ }>>;
47
+ methods: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
48
+ api_key: "api_key";
49
+ "claude-code": "claude-code";
50
+ codex: "codex";
51
+ none: "none";
52
+ }>>>;
53
+ }, z.core.$strict>>;
54
+ fallback: z.ZodOptional<z.ZodArray<z.ZodObject<{
55
+ auth: z.ZodOptional<z.ZodObject<{
56
+ key: z.ZodOptional<z.ZodString>;
57
+ method: z.ZodOptional<z.ZodEnum<{
58
+ api_key: "api_key";
59
+ "claude-code": "claude-code";
60
+ codex: "codex";
61
+ none: "none";
62
+ }>>;
63
+ }, z.core.$strict>>;
64
+ endpoint: z.ZodOptional<z.ZodObject<{
65
+ base_url: z.ZodString;
66
+ compatibility: z.ZodEnum<{
67
+ anthropic: "anthropic";
68
+ openai: "openai";
69
+ }>;
70
+ }, z.core.$strict>>;
71
+ name: z.ZodString;
72
+ provider: z.ZodString;
73
+ }, z.core.$strict>>>;
74
+ primary: z.ZodObject<{
75
+ auth: z.ZodOptional<z.ZodObject<{
76
+ key: z.ZodOptional<z.ZodString>;
77
+ method: z.ZodOptional<z.ZodEnum<{
78
+ api_key: "api_key";
79
+ "claude-code": "claude-code";
80
+ codex: "codex";
81
+ none: "none";
82
+ }>>;
83
+ }, z.core.$strict>>;
84
+ endpoint: z.ZodOptional<z.ZodObject<{
85
+ base_url: z.ZodString;
86
+ compatibility: z.ZodEnum<{
87
+ anthropic: "anthropic";
88
+ openai: "openai";
89
+ }>;
90
+ }, z.core.$strict>>;
91
+ name: z.ZodString;
92
+ provider: z.ZodString;
93
+ }, z.core.$strict>;
94
+ }, z.core.$strict>>;
95
+ sandbox: z.ZodOptional<z.ZodObject<{
96
+ mode: z.ZodEnum<{
97
+ sandboxed: "sandboxed";
98
+ unrestricted: "unrestricted";
99
+ workspace: "workspace";
100
+ }>;
101
+ }, z.core.$strict>>;
102
+ }, z.core.$strict>;
103
+ export type ExecutionBlock = z.infer<typeof executionSchema>;
104
+ export type ModelEndpoint = z.infer<typeof modelEndpointSchema>;
105
+ export type ModelEntryAuth = z.infer<typeof modelEntryAuthSchema>;
106
+ export type ModelTarget = z.infer<typeof modelTargetSchema>;