spawnfile 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -396
- package/dist/cli/modelCommands.d.ts +3 -0
- package/dist/cli/modelCommands.js +68 -0
- package/dist/cli/runCli.d.ts +6 -1
- package/dist/cli/runCli.js +12 -67
- package/dist/cli/runtimeCommands.d.ts +3 -0
- package/dist/cli/runtimeCommands.js +20 -0
- package/dist/cli/surfaceCommands.d.ts +3 -0
- package/dist/cli/surfaceCommands.js +98 -0
- package/dist/compiler/agentSurfaces.js +51 -5
- package/dist/compiler/buildCompilePlan.js +36 -40
- package/dist/compiler/buildCompilePlanRuntime.d.ts +14 -0
- package/dist/compiler/buildCompilePlanRuntime.js +39 -0
- package/dist/compiler/buildCompilePlanTeams.d.ts +5 -0
- package/dist/compiler/buildCompilePlanTeams.js +38 -0
- package/dist/compiler/compilePlanHelpers.js +4 -1
- package/dist/compiler/compileProject.js +62 -13
- package/dist/compiler/compileProjectSupport.d.ts +17 -0
- package/dist/compiler/compileProjectSupport.js +136 -0
- package/dist/compiler/containerArtifacts.d.ts +6 -1
- package/dist/compiler/containerArtifacts.js +26 -4
- package/dist/compiler/containerArtifactsPlans.js +16 -1
- package/dist/compiler/containerArtifactsRender.d.ts +4 -2
- package/dist/compiler/containerArtifactsRender.js +21 -126
- package/dist/compiler/containerArtifactsTypes.d.ts +7 -0
- package/dist/compiler/containerEntrypointRender.d.ts +12 -0
- package/dist/compiler/containerEntrypointRender.js +186 -0
- package/dist/compiler/index.d.ts +2 -0
- package/dist/compiler/index.js +2 -0
- package/dist/compiler/interactiveSurfaceScopes.d.ts +2 -0
- package/dist/compiler/interactiveSurfaceScopes.js +21 -0
- package/dist/compiler/moltnetArtifacts.d.ts +27 -0
- package/dist/compiler/moltnetArtifacts.js +204 -0
- package/dist/compiler/moltnetBinaries.d.ts +4 -0
- package/dist/compiler/moltnetBinaries.js +103 -0
- package/dist/compiler/moltnetClientConfig.d.ts +11 -0
- package/dist/compiler/moltnetClientConfig.js +89 -0
- package/dist/compiler/moltnetRepresentativeResolution.d.ts +16 -0
- package/dist/compiler/moltnetRepresentativeResolution.js +86 -0
- package/dist/compiler/moltnetResolution.d.ts +3 -0
- package/dist/compiler/moltnetResolution.js +201 -0
- package/dist/compiler/runProject.js +1 -1
- package/dist/compiler/surfaceDefinitions.d.ts +55 -0
- package/dist/compiler/surfaceDefinitions.js +204 -0
- package/dist/compiler/teamContextHelpers.d.ts +18 -0
- package/dist/compiler/teamContextHelpers.js +112 -0
- package/dist/compiler/teamContextSupport.d.ts +4 -0
- package/dist/compiler/teamContextSupport.js +264 -0
- package/dist/compiler/teamContextSupport.testHelpers.d.ts +16 -0
- package/dist/compiler/teamContextSupport.testHelpers.js +68 -0
- package/dist/compiler/teamContextTypes.d.ts +28 -0
- package/dist/compiler/teamContextTypes.js +1 -0
- package/dist/compiler/teamRoster.d.ts +12 -0
- package/dist/compiler/teamRoster.js +48 -0
- package/dist/compiler/teamRosterEntries.d.ts +13 -0
- package/dist/compiler/teamRosterEntries.js +230 -0
- package/dist/compiler/teamRosterTypes.d.ts +45 -0
- package/dist/compiler/teamRosterTypes.js +1 -0
- package/dist/compiler/types.d.ts +72 -6
- package/dist/compiler/updateProjectRuntime.d.ts +9 -0
- package/dist/compiler/updateProjectRuntime.js +67 -0
- package/dist/compiler/updateProjectSurfaces.d.ts +8 -0
- package/dist/compiler/updateProjectSurfaces.js +106 -0
- package/dist/manifest/loadManifest.js +4 -4
- package/dist/manifest/renderSpawnfile.js +74 -8
- package/dist/manifest/scaffold.js +1 -3
- package/dist/manifest/schemas.d.ts +227 -17
- package/dist/manifest/schemas.js +62 -20
- package/dist/manifest/surfaceSchemas.d.ts +154 -0
- package/dist/manifest/surfaceSchemas.js +77 -5
- package/dist/runtime/common.js +3 -0
- package/dist/runtime/openclaw/adapter.js +38 -5
- package/dist/runtime/openclaw/moltnet.d.ts +12 -0
- package/dist/runtime/openclaw/moltnet.js +124 -0
- package/dist/runtime/openclaw/surfaces.js +3 -0
- package/dist/runtime/picoclaw/adapter.js +27 -8
- package/dist/runtime/picoclaw/pico.d.ts +2 -0
- package/dist/runtime/picoclaw/pico.js +2 -0
- package/dist/runtime/picoclaw/surfaces.js +11 -0
- package/dist/runtime/tinyclaw/adapter.js +22 -8
- package/dist/runtime/tinyclaw/runAuth.js +28 -1
- package/dist/runtime/tinyclaw/surfaces.js +8 -0
- package/dist/runtime/types.d.ts +11 -0
- package/package.json +4 -2
- package/runtimes.yaml +4 -4
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CompilePlan, ResolvedMoltnetAttachment, ResolvedTeamNetwork, ResolvedTeamNode } from "./types.js";
|
|
2
|
+
export interface MoltnetTeamContext {
|
|
3
|
+
memberId: string;
|
|
4
|
+
teamName: string;
|
|
5
|
+
teamSource: string;
|
|
6
|
+
networks: ResolvedTeamNetwork[];
|
|
7
|
+
}
|
|
8
|
+
export interface TeamRepresentativeResolution {
|
|
9
|
+
agentSource: string;
|
|
10
|
+
memberId: string;
|
|
11
|
+
path: string[];
|
|
12
|
+
sourceTeamName: string;
|
|
13
|
+
sourceTeamSource: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const resolveTeamRepresentatives: (plan: CompilePlan, teamNode: ResolvedTeamNode, seen?: string[]) => TeamRepresentativeResolution[];
|
|
16
|
+
export declare const resolveMoltnetAttachments: (attachments: ResolvedMoltnetAttachment[] | undefined, context: MoltnetTeamContext | undefined, nodeName: string) => ResolvedMoltnetAttachment[] | undefined;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
2
|
+
const cloneAttachment = (attachment, context) => ({
|
|
3
|
+
...(attachment.dms ? { dms: { ...attachment.dms } } : {}),
|
|
4
|
+
memberId: context.memberId,
|
|
5
|
+
network: attachment.network,
|
|
6
|
+
...(attachment.rooms
|
|
7
|
+
? {
|
|
8
|
+
rooms: Object.fromEntries(Object.entries(attachment.rooms).map(([roomId, policy]) => [
|
|
9
|
+
roomId,
|
|
10
|
+
{ ...policy }
|
|
11
|
+
]))
|
|
12
|
+
}
|
|
13
|
+
: {}),
|
|
14
|
+
teamSource: context.teamSource
|
|
15
|
+
});
|
|
16
|
+
const findTeamBySource = (plan, source) => {
|
|
17
|
+
const node = plan.nodes.find((entry) => entry.kind === "team" && entry.value.source === source);
|
|
18
|
+
if (!node || node.value.kind !== "team") {
|
|
19
|
+
throw new SpawnfileError("compile_error", `Unable to find team node at ${source}`);
|
|
20
|
+
}
|
|
21
|
+
return node.value;
|
|
22
|
+
};
|
|
23
|
+
export const resolveTeamRepresentatives = (plan, teamNode, seen = []) => {
|
|
24
|
+
if (seen.includes(teamNode.source)) {
|
|
25
|
+
throw new SpawnfileError("compile_error", `Cycle detected while resolving representatives for ${teamNode.name}`);
|
|
26
|
+
}
|
|
27
|
+
const selectedMemberIds = teamNode.externalExplicit
|
|
28
|
+
? teamNode.external
|
|
29
|
+
: teamNode.mode === "hierarchical" && teamNode.lead
|
|
30
|
+
? [teamNode.lead]
|
|
31
|
+
: teamNode.members.map((member) => member.id);
|
|
32
|
+
const nextSeen = [...seen, teamNode.source];
|
|
33
|
+
const representatives = [];
|
|
34
|
+
for (const memberId of selectedMemberIds) {
|
|
35
|
+
const member = teamNode.members.find((entry) => entry.id === memberId);
|
|
36
|
+
if (!member) {
|
|
37
|
+
throw new SpawnfileError("validation_error", `Team ${teamNode.name} representative interface references unknown member ${memberId}`);
|
|
38
|
+
}
|
|
39
|
+
if (member.kind === "agent") {
|
|
40
|
+
representatives.push({
|
|
41
|
+
agentSource: member.nodeSource,
|
|
42
|
+
memberId: member.id,
|
|
43
|
+
path: [member.id],
|
|
44
|
+
sourceTeamName: teamNode.name,
|
|
45
|
+
sourceTeamSource: teamNode.source
|
|
46
|
+
});
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const childTeam = findTeamBySource(plan, member.nodeSource);
|
|
50
|
+
const childRepresentatives = resolveTeamRepresentatives(plan, childTeam, nextSeen);
|
|
51
|
+
if (childRepresentatives.length === 0) {
|
|
52
|
+
throw new SpawnfileError("validation_error", `Team ${childTeam.name} does not resolve to a concrete representative for ${teamNode.name}`);
|
|
53
|
+
}
|
|
54
|
+
for (const representative of childRepresentatives) {
|
|
55
|
+
representatives.push({
|
|
56
|
+
...representative,
|
|
57
|
+
path: [member.id, ...representative.path]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return representatives;
|
|
62
|
+
};
|
|
63
|
+
export const resolveMoltnetAttachments = (attachments, context, nodeName) => {
|
|
64
|
+
if (!attachments || attachments.length === 0) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
if (!context) {
|
|
68
|
+
throw new SpawnfileError("validation_error", `Agent ${nodeName} declares Moltnet surfaces but is not attached to a team network`);
|
|
69
|
+
}
|
|
70
|
+
return attachments.map((attachment) => {
|
|
71
|
+
const network = context.networks.find((entry) => entry.id === attachment.network);
|
|
72
|
+
if (!network) {
|
|
73
|
+
throw new SpawnfileError("validation_error", `Agent ${nodeName} references unknown Moltnet network ${attachment.network} on team ${context.teamName}`);
|
|
74
|
+
}
|
|
75
|
+
for (const roomId of Object.keys(attachment.rooms ?? {})) {
|
|
76
|
+
const room = network.rooms.find((entry) => entry.id === roomId);
|
|
77
|
+
if (!room) {
|
|
78
|
+
throw new SpawnfileError("validation_error", `Agent ${nodeName} references unknown Moltnet room ${roomId} on network ${network.id}`);
|
|
79
|
+
}
|
|
80
|
+
if (!room.members.includes(context.memberId)) {
|
|
81
|
+
throw new SpawnfileError("validation_error", `Agent ${nodeName} cannot attach to Moltnet room ${roomId} because member ${context.memberId} is not in that room`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return cloneAttachment(attachment, context);
|
|
85
|
+
});
|
|
86
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CompilePlan } from "./types.js";
|
|
2
|
+
export { resolveMoltnetAttachments, resolveTeamRepresentatives, type MoltnetTeamContext, type TeamRepresentativeResolution } from "./moltnetRepresentativeResolution.js";
|
|
3
|
+
export declare const resolvePlanMoltnetAttachments: (plan: CompilePlan) => void;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
2
|
+
import { resolveMoltnetAttachments, resolveTeamRepresentatives } from "./moltnetRepresentativeResolution.js";
|
|
3
|
+
export { resolveMoltnetAttachments, resolveTeamRepresentatives } from "./moltnetRepresentativeResolution.js";
|
|
4
|
+
const findTeamBySource = (plan, source) => {
|
|
5
|
+
const node = plan.nodes.find((entry) => entry.kind === "team" && entry.value.source === source);
|
|
6
|
+
if (!node || node.value.kind !== "team") {
|
|
7
|
+
throw new SpawnfileError("compile_error", `Unable to find team node at ${source}`);
|
|
8
|
+
}
|
|
9
|
+
return node.value;
|
|
10
|
+
};
|
|
11
|
+
const hasMoltnetIntent = (plan) => plan.nodes.some((node) => {
|
|
12
|
+
if (node.value.kind === "team") {
|
|
13
|
+
return (node.value.networks?.length ?? 0) > 0;
|
|
14
|
+
}
|
|
15
|
+
return (node.value.surfaces?.moltnet?.length ?? 0) > 0;
|
|
16
|
+
});
|
|
17
|
+
const validateGlobalMemberIds = (plan) => {
|
|
18
|
+
if (!hasMoltnetIntent(plan)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const seen = new Map();
|
|
22
|
+
const uniqueContexts = new Map((plan.memberships ?? []).map((context) => [
|
|
23
|
+
`${context.teamSource}::${context.memberId}::${context.agentSource}`,
|
|
24
|
+
context
|
|
25
|
+
]));
|
|
26
|
+
for (const context of uniqueContexts.values()) {
|
|
27
|
+
const previous = seen.get(context.memberId);
|
|
28
|
+
const label = `${context.teamName} (${context.teamSource}) member ${context.memberId}`;
|
|
29
|
+
if (previous && previous !== label) {
|
|
30
|
+
throw new SpawnfileError("validation_error", `Moltnet member_id ${context.memberId} is declared by multiple direct agent member slots: ${previous}; ${label}`);
|
|
31
|
+
}
|
|
32
|
+
seen.set(context.memberId, label);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const expandTeamNetworkRooms = (plan) => {
|
|
36
|
+
const synthesizedAttachments = [];
|
|
37
|
+
for (const node of plan.nodes) {
|
|
38
|
+
if (node.value.kind !== "team") {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const teamNode = node.value;
|
|
42
|
+
for (const network of teamNode.networks ?? []) {
|
|
43
|
+
for (const room of network.rooms) {
|
|
44
|
+
const expandedMembers = [];
|
|
45
|
+
for (const roomMemberId of room.members) {
|
|
46
|
+
const member = teamNode.members.find((entry) => entry.id === roomMemberId);
|
|
47
|
+
if (!member) {
|
|
48
|
+
throw new SpawnfileError("validation_error", `Team ${teamNode.name} Moltnet room ${room.id} references unknown member ${roomMemberId}`);
|
|
49
|
+
}
|
|
50
|
+
if (member.kind === "agent") {
|
|
51
|
+
expandedMembers.push(member.id);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const childTeam = findTeamBySource(plan, member.nodeSource);
|
|
55
|
+
const representatives = resolveTeamRepresentatives(plan, childTeam);
|
|
56
|
+
if (representatives.length === 0) {
|
|
57
|
+
throw new SpawnfileError("validation_error", `Team ${childTeam.name} has no concrete representative for Moltnet room ${room.id} on ${teamNode.name}`);
|
|
58
|
+
}
|
|
59
|
+
for (const representative of representatives) {
|
|
60
|
+
expandedMembers.push(representative.memberId);
|
|
61
|
+
synthesizedAttachments.push({
|
|
62
|
+
contextRooms: {
|
|
63
|
+
[teamNode.source]: [room.id]
|
|
64
|
+
},
|
|
65
|
+
memberId: representative.memberId,
|
|
66
|
+
network: network.id,
|
|
67
|
+
rooms: {
|
|
68
|
+
[room.id]: {}
|
|
69
|
+
},
|
|
70
|
+
teamSource: teamNode.source
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
room.members = [...new Set(expandedMembers)];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return synthesizedAttachments;
|
|
79
|
+
};
|
|
80
|
+
const roomPolicyKey = (policy) => JSON.stringify(policy ?? {});
|
|
81
|
+
const mergeAttachment = (target, next, nodeName) => {
|
|
82
|
+
if (target.dms &&
|
|
83
|
+
next.dms &&
|
|
84
|
+
roomPolicyKey(target.dms) !== roomPolicyKey(next.dms)) {
|
|
85
|
+
throw new SpawnfileError("validation_error", `Agent ${nodeName} declares incompatible Moltnet dms for ${next.network}/${next.memberId ?? "unknown"}`);
|
|
86
|
+
}
|
|
87
|
+
target.dms ??= next.dms ? { ...next.dms } : undefined;
|
|
88
|
+
target.teamSource ??= next.teamSource;
|
|
89
|
+
target.rooms ??= {};
|
|
90
|
+
for (const [roomId, policy] of Object.entries(next.rooms ?? {})) {
|
|
91
|
+
const existingPolicy = target.rooms[roomId];
|
|
92
|
+
if (existingPolicy &&
|
|
93
|
+
roomPolicyKey(existingPolicy) !== roomPolicyKey(policy)) {
|
|
94
|
+
throw new SpawnfileError("validation_error", `Agent ${nodeName} declares incompatible Moltnet room policy for ${next.network}/${next.memberId ?? "unknown"} room ${roomId}`);
|
|
95
|
+
}
|
|
96
|
+
target.rooms[roomId] = { ...policy };
|
|
97
|
+
}
|
|
98
|
+
if (next.contextRooms) {
|
|
99
|
+
target.contextRooms ??= {};
|
|
100
|
+
for (const [teamSource, roomIds] of Object.entries(next.contextRooms)) {
|
|
101
|
+
target.contextRooms[teamSource] = [
|
|
102
|
+
...new Set([...(target.contextRooms[teamSource] ?? []), ...roomIds])
|
|
103
|
+
].sort();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (next.teamSource) {
|
|
107
|
+
target.contextRooms ??= {};
|
|
108
|
+
target.contextRooms[next.teamSource] = [
|
|
109
|
+
...new Set([
|
|
110
|
+
...(target.contextRooms[next.teamSource] ?? []),
|
|
111
|
+
...Object.keys(next.rooms ?? {})
|
|
112
|
+
])
|
|
113
|
+
].sort();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const mergeAgentAttachments = (agentNode, attachments) => {
|
|
117
|
+
const merged = new Map();
|
|
118
|
+
for (const attachment of attachments) {
|
|
119
|
+
const key = `${attachment.network}::${attachment.memberId ?? ""}`;
|
|
120
|
+
const existing = merged.get(key);
|
|
121
|
+
if (existing) {
|
|
122
|
+
mergeAttachment(existing, attachment, agentNode.name);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
merged.set(key, {
|
|
126
|
+
contextRooms: attachment.contextRooms
|
|
127
|
+
? Object.fromEntries(Object.entries(attachment.contextRooms).map(([teamSource, roomIds]) => [
|
|
128
|
+
teamSource,
|
|
129
|
+
[...roomIds].sort()
|
|
130
|
+
]))
|
|
131
|
+
: attachment.teamSource
|
|
132
|
+
? { [attachment.teamSource]: Object.keys(attachment.rooms ?? {}).sort() }
|
|
133
|
+
: undefined,
|
|
134
|
+
...(attachment.dms ? { dms: { ...attachment.dms } } : {}),
|
|
135
|
+
memberId: attachment.memberId,
|
|
136
|
+
network: attachment.network,
|
|
137
|
+
...(attachment.rooms
|
|
138
|
+
? {
|
|
139
|
+
rooms: Object.fromEntries(Object.entries(attachment.rooms).map(([roomId, policy]) => [
|
|
140
|
+
roomId,
|
|
141
|
+
{ ...policy }
|
|
142
|
+
]))
|
|
143
|
+
}
|
|
144
|
+
: {}),
|
|
145
|
+
teamSource: attachment.teamSource
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return [...merged.values()].sort((left, right) => `${left.network}:${left.memberId ?? ""}`.localeCompare(`${right.network}:${right.memberId ?? ""}`));
|
|
149
|
+
};
|
|
150
|
+
export const resolvePlanMoltnetAttachments = (plan) => {
|
|
151
|
+
validateGlobalMemberIds(plan);
|
|
152
|
+
const synthesizedAttachments = expandTeamNetworkRooms(plan);
|
|
153
|
+
const synthesizedByAgent = new Map();
|
|
154
|
+
for (const attachment of synthesizedAttachments) {
|
|
155
|
+
const representativeContext = (plan.memberships ?? []).find((context) => context.memberId === attachment.memberId);
|
|
156
|
+
if (!representativeContext) {
|
|
157
|
+
throw new SpawnfileError("validation_error", `Unable to find direct member context for synthesized Moltnet member ${attachment.memberId ?? "unknown"}`);
|
|
158
|
+
}
|
|
159
|
+
const group = synthesizedByAgent.get(representativeContext.agentSource) ?? [];
|
|
160
|
+
group.push(attachment);
|
|
161
|
+
synthesizedByAgent.set(representativeContext.agentSource, group);
|
|
162
|
+
}
|
|
163
|
+
for (const node of plan.nodes) {
|
|
164
|
+
if (node.value.kind !== "agent") {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const agentNode = node.value;
|
|
168
|
+
const declaredAttachments = agentNode.surfaces?.moltnet;
|
|
169
|
+
const directContexts = (plan.memberships ?? []).filter((context) => context.agentSource === agentNode.source);
|
|
170
|
+
const resolvedAttachments = [];
|
|
171
|
+
if (declaredAttachments && directContexts.length === 0) {
|
|
172
|
+
throw new SpawnfileError("validation_error", `Agent ${agentNode.name} declares Moltnet surfaces but is not attached to a team network`);
|
|
173
|
+
}
|
|
174
|
+
for (const context of directContexts) {
|
|
175
|
+
if (!declaredAttachments) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const teamNode = findTeamBySource(plan, context.teamSource);
|
|
179
|
+
const resolved = resolveMoltnetAttachments(declaredAttachments, {
|
|
180
|
+
memberId: context.memberId,
|
|
181
|
+
networks: teamNode.networks ?? [],
|
|
182
|
+
teamName: context.teamName,
|
|
183
|
+
teamSource: context.teamSource
|
|
184
|
+
}, agentNode.name);
|
|
185
|
+
resolvedAttachments.push(...(resolved ?? []));
|
|
186
|
+
}
|
|
187
|
+
resolvedAttachments.push(...(synthesizedByAgent.get(agentNode.source) ?? []));
|
|
188
|
+
if (resolvedAttachments.length > 0) {
|
|
189
|
+
agentNode.surfaces = {
|
|
190
|
+
...agentNode.surfaces,
|
|
191
|
+
moltnet: mergeAgentAttachments(agentNode, resolvedAttachments)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
else if (agentNode.surfaces?.moltnet) {
|
|
195
|
+
agentNode.surfaces = {
|
|
196
|
+
...agentNode.surfaces,
|
|
197
|
+
moltnet: undefined
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
@@ -22,7 +22,7 @@ const createDefaultContainerName = (imageTag) => {
|
|
|
22
22
|
};
|
|
23
23
|
const getImportMountTargetName = (kind) => kind === "claude-code" ? ".claude" : ".codex";
|
|
24
24
|
const createGeneratedRuntimeSecret = (secretName) => {
|
|
25
|
-
if (secretName === "OPENCLAW_GATEWAY_TOKEN") {
|
|
25
|
+
if (secretName === "OPENCLAW_GATEWAY_TOKEN" || secretName === "OPENCLAW_HOOKS_TOKEN") {
|
|
26
26
|
return randomBytes(24).toString("hex");
|
|
27
27
|
}
|
|
28
28
|
return null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AgentManifest, SurfacesBlock, TeamManifest } from "../manifest/index.js";
|
|
2
|
+
type ProjectManifest = AgentManifest | TeamManifest;
|
|
3
|
+
export type SurfaceAccessMode = "allowlist" | "open" | "pairing";
|
|
4
|
+
export declare const PORTABLE_SURFACE_NAMES: readonly ["discord", "slack", "telegram", "whatsapp"];
|
|
5
|
+
export type SurfaceName = (typeof PORTABLE_SURFACE_NAMES)[number];
|
|
6
|
+
export interface ProjectSurfaceSecretOptions {
|
|
7
|
+
appTokenSecret?: string;
|
|
8
|
+
botTokenSecret?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface AddProjectSurfaceOptions extends ProjectSurfaceSecretOptions {
|
|
11
|
+
path?: string;
|
|
12
|
+
recursive?: boolean;
|
|
13
|
+
surface: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ProjectSurfaceAccessOptions {
|
|
16
|
+
channels?: string[];
|
|
17
|
+
chats?: string[];
|
|
18
|
+
groups?: string[];
|
|
19
|
+
guilds?: string[];
|
|
20
|
+
mode: SurfaceAccessMode;
|
|
21
|
+
path?: string;
|
|
22
|
+
recursive?: boolean;
|
|
23
|
+
surface: string;
|
|
24
|
+
users?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface RemoveProjectSurfaceOptions {
|
|
27
|
+
path?: string;
|
|
28
|
+
recursive?: boolean;
|
|
29
|
+
surface: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ShowProjectSurfacesOptions {
|
|
32
|
+
path?: string;
|
|
33
|
+
recursive?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface ProjectSurfaceSummary {
|
|
36
|
+
kind: "agent" | "team";
|
|
37
|
+
manifestPath: string;
|
|
38
|
+
name: string;
|
|
39
|
+
surfaces?: SurfacesBlock;
|
|
40
|
+
}
|
|
41
|
+
export interface ProjectSurfaceSummariesResult {
|
|
42
|
+
entries: ProjectSurfaceSummary[];
|
|
43
|
+
}
|
|
44
|
+
export interface UpdateProjectSurfacesResult {
|
|
45
|
+
updatedFiles: string[];
|
|
46
|
+
}
|
|
47
|
+
export declare const TEAM_SURFACE_COMMAND_ERROR = "spawnfile surface commands only write agent manifests; use --recursive to update descendant agents of a team project";
|
|
48
|
+
export declare const getRuntimeName: (runtime: AgentManifest["runtime"]) => string | undefined;
|
|
49
|
+
export declare const resolvePortableSurfaceName: (surface: string) => SurfaceName;
|
|
50
|
+
export declare const assertSurfaceMutationAllowed: (manifest: ProjectManifest, recursive: boolean) => manifest is AgentManifest;
|
|
51
|
+
export declare const upsertSurface: (surfaces: SurfacesBlock | undefined, options: AddProjectSurfaceOptions) => SurfacesBlock;
|
|
52
|
+
export declare const updateSurfaceAccess: (surfaces: SurfacesBlock | undefined, options: ProjectSurfaceAccessOptions, manifestPath: string, recursive: boolean) => SurfacesBlock | null;
|
|
53
|
+
export declare const removeSurface: (surfaces: SurfacesBlock | undefined, surface: string) => SurfacesBlock | undefined;
|
|
54
|
+
export declare const validateAgentSurfaceSupport: (manifest: AgentManifest) => void;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
2
|
+
import { resolveAgentSurfaces } from "./agentSurfaces.js";
|
|
3
|
+
import { assertRuntimeSupportsAgentSurfaces } from "./surfaceSupport.js";
|
|
4
|
+
export const PORTABLE_SURFACE_NAMES = ["discord", "slack", "telegram", "whatsapp"];
|
|
5
|
+
export const TEAM_SURFACE_COMMAND_ERROR = "spawnfile surface commands only write agent manifests; use --recursive to update descendant agents of a team project";
|
|
6
|
+
export const getRuntimeName = (runtime) => typeof runtime === "string" ? runtime : runtime?.name;
|
|
7
|
+
export const resolvePortableSurfaceName = (surface) => {
|
|
8
|
+
if (!PORTABLE_SURFACE_NAMES.includes(surface)) {
|
|
9
|
+
throw new SpawnfileError("validation_error", `Unsupported portable surface ${surface}; expected one of: ${PORTABLE_SURFACE_NAMES.join(", ")}`);
|
|
10
|
+
}
|
|
11
|
+
return surface;
|
|
12
|
+
};
|
|
13
|
+
export const assertSurfaceMutationAllowed = (manifest, recursive) => {
|
|
14
|
+
if (manifest.kind === "agent") {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (recursive) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
throw new SpawnfileError("validation_error", TEAM_SURFACE_COMMAND_ERROR);
|
|
21
|
+
};
|
|
22
|
+
const normalizeEntries = (values) => {
|
|
23
|
+
const normalized = [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))].sort();
|
|
24
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
25
|
+
};
|
|
26
|
+
const validateSecrets = (surface, options) => {
|
|
27
|
+
if (options.appTokenSecret && surface !== "slack") {
|
|
28
|
+
throw new SpawnfileError("validation_error", "--app-token-secret is only valid for slack surfaces");
|
|
29
|
+
}
|
|
30
|
+
if (options.botTokenSecret && !["discord", "slack", "telegram"].includes(surface)) {
|
|
31
|
+
throw new SpawnfileError("validation_error", `--bot-token-secret is not valid for ${surface} surfaces`);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const buildDiscordAccess = (options) => {
|
|
35
|
+
const users = normalizeEntries(options.users);
|
|
36
|
+
const guilds = normalizeEntries(options.guilds);
|
|
37
|
+
const channels = normalizeEntries(options.channels);
|
|
38
|
+
const hasAllowlistEntries = Boolean(users || guilds || channels);
|
|
39
|
+
if (options.mode === "allowlist" && !hasAllowlistEntries) {
|
|
40
|
+
throw new SpawnfileError("validation_error", "discord allowlist access requires at least one --user, --guild, or --channel");
|
|
41
|
+
}
|
|
42
|
+
if (options.mode !== "allowlist" && hasAllowlistEntries) {
|
|
43
|
+
throw new SpawnfileError("validation_error", "discord allowlist entries are only valid with --mode allowlist");
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
...(channels ? { channels } : {}),
|
|
47
|
+
...(guilds ? { guilds } : {}),
|
|
48
|
+
mode: options.mode,
|
|
49
|
+
...(users ? { users } : {})
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
const buildTelegramAccess = (options) => {
|
|
53
|
+
const users = normalizeEntries(options.users);
|
|
54
|
+
const chats = normalizeEntries(options.chats);
|
|
55
|
+
const hasAllowlistEntries = Boolean(users || chats);
|
|
56
|
+
if (options.mode === "allowlist" && !hasAllowlistEntries) {
|
|
57
|
+
throw new SpawnfileError("validation_error", "telegram allowlist access requires at least one --user or --chat");
|
|
58
|
+
}
|
|
59
|
+
if (options.mode !== "allowlist" && hasAllowlistEntries) {
|
|
60
|
+
throw new SpawnfileError("validation_error", "telegram allowlist entries are only valid with --mode allowlist");
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
...(chats ? { chats } : {}),
|
|
64
|
+
mode: options.mode,
|
|
65
|
+
...(users ? { users } : {})
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
const buildWhatsAppAccess = (options) => {
|
|
69
|
+
const users = normalizeEntries(options.users);
|
|
70
|
+
const groups = normalizeEntries(options.groups);
|
|
71
|
+
const hasAllowlistEntries = Boolean(users || groups);
|
|
72
|
+
if (options.mode === "allowlist" && !hasAllowlistEntries) {
|
|
73
|
+
throw new SpawnfileError("validation_error", "whatsapp allowlist access requires at least one --user or --group");
|
|
74
|
+
}
|
|
75
|
+
if (options.mode !== "allowlist" && hasAllowlistEntries) {
|
|
76
|
+
throw new SpawnfileError("validation_error", "whatsapp allowlist entries are only valid with --mode allowlist");
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...(groups ? { groups } : {}),
|
|
80
|
+
mode: options.mode,
|
|
81
|
+
...(users ? { users } : {})
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
const buildSlackAccess = (options) => {
|
|
85
|
+
const users = normalizeEntries(options.users);
|
|
86
|
+
const channels = normalizeEntries(options.channels);
|
|
87
|
+
const hasAllowlistEntries = Boolean(users || channels);
|
|
88
|
+
if (options.mode === "allowlist" && !hasAllowlistEntries) {
|
|
89
|
+
throw new SpawnfileError("validation_error", "slack allowlist access requires at least one --user or --channel");
|
|
90
|
+
}
|
|
91
|
+
if (options.mode !== "allowlist" && hasAllowlistEntries) {
|
|
92
|
+
throw new SpawnfileError("validation_error", "slack allowlist entries are only valid with --mode allowlist");
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
...(channels ? { channels } : {}),
|
|
96
|
+
mode: options.mode,
|
|
97
|
+
...(users ? { users } : {})
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
const buildSurfaceAccess = (options) => {
|
|
101
|
+
const surface = resolvePortableSurfaceName(options.surface);
|
|
102
|
+
switch (surface) {
|
|
103
|
+
case "discord":
|
|
104
|
+
return buildDiscordAccess(options);
|
|
105
|
+
case "telegram":
|
|
106
|
+
return buildTelegramAccess(options);
|
|
107
|
+
case "whatsapp":
|
|
108
|
+
return buildWhatsAppAccess(options);
|
|
109
|
+
case "slack":
|
|
110
|
+
return buildSlackAccess(options);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
export const upsertSurface = (surfaces, options) => {
|
|
114
|
+
const surface = resolvePortableSurfaceName(options.surface);
|
|
115
|
+
validateSecrets(surface, options);
|
|
116
|
+
const nextSurfaces = { ...(surfaces ?? {}) };
|
|
117
|
+
switch (surface) {
|
|
118
|
+
case "discord":
|
|
119
|
+
nextSurfaces.discord = {
|
|
120
|
+
...(surfaces?.discord ?? {}),
|
|
121
|
+
...(options.botTokenSecret ? { bot_token_secret: options.botTokenSecret } : {})
|
|
122
|
+
};
|
|
123
|
+
break;
|
|
124
|
+
case "telegram":
|
|
125
|
+
nextSurfaces.telegram = {
|
|
126
|
+
...(surfaces?.telegram ?? {}),
|
|
127
|
+
...(options.botTokenSecret ? { bot_token_secret: options.botTokenSecret } : {})
|
|
128
|
+
};
|
|
129
|
+
break;
|
|
130
|
+
case "whatsapp":
|
|
131
|
+
nextSurfaces.whatsapp = {
|
|
132
|
+
...(surfaces?.whatsapp ?? {})
|
|
133
|
+
};
|
|
134
|
+
break;
|
|
135
|
+
case "slack":
|
|
136
|
+
nextSurfaces.slack = {
|
|
137
|
+
...(surfaces?.slack ?? {}),
|
|
138
|
+
...(options.appTokenSecret ? { app_token_secret: options.appTokenSecret } : {}),
|
|
139
|
+
...(options.botTokenSecret ? { bot_token_secret: options.botTokenSecret } : {})
|
|
140
|
+
};
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
return nextSurfaces;
|
|
144
|
+
};
|
|
145
|
+
export const updateSurfaceAccess = (surfaces, options, manifestPath, recursive) => {
|
|
146
|
+
const surface = resolvePortableSurfaceName(options.surface);
|
|
147
|
+
const nextSurfaces = { ...(surfaces ?? {}) };
|
|
148
|
+
const access = buildSurfaceAccess({ ...options, surface });
|
|
149
|
+
switch (surface) {
|
|
150
|
+
case "discord":
|
|
151
|
+
if (!nextSurfaces.discord) {
|
|
152
|
+
if (recursive) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
throw new SpawnfileError("validation_error", `Surface discord is not declared in ${manifestPath}; use spawnfile surface add discord first`);
|
|
156
|
+
}
|
|
157
|
+
nextSurfaces.discord = { ...nextSurfaces.discord, access: access };
|
|
158
|
+
break;
|
|
159
|
+
case "telegram":
|
|
160
|
+
if (!nextSurfaces.telegram) {
|
|
161
|
+
if (recursive) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
throw new SpawnfileError("validation_error", `Surface telegram is not declared in ${manifestPath}; use spawnfile surface add telegram first`);
|
|
165
|
+
}
|
|
166
|
+
nextSurfaces.telegram = { ...nextSurfaces.telegram, access: access };
|
|
167
|
+
break;
|
|
168
|
+
case "whatsapp":
|
|
169
|
+
if (!nextSurfaces.whatsapp) {
|
|
170
|
+
if (recursive) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
throw new SpawnfileError("validation_error", `Surface whatsapp is not declared in ${manifestPath}; use spawnfile surface add whatsapp first`);
|
|
174
|
+
}
|
|
175
|
+
nextSurfaces.whatsapp = { ...nextSurfaces.whatsapp, access: access };
|
|
176
|
+
break;
|
|
177
|
+
case "slack":
|
|
178
|
+
if (!nextSurfaces.slack) {
|
|
179
|
+
if (recursive) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
throw new SpawnfileError("validation_error", `Surface slack is not declared in ${manifestPath}; use spawnfile surface add slack first`);
|
|
183
|
+
}
|
|
184
|
+
nextSurfaces.slack = { ...nextSurfaces.slack, access: access };
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
return nextSurfaces;
|
|
188
|
+
};
|
|
189
|
+
export const removeSurface = (surfaces, surface) => {
|
|
190
|
+
const surfaceName = resolvePortableSurfaceName(surface);
|
|
191
|
+
if (!surfaces?.[surfaceName]) {
|
|
192
|
+
return surfaces;
|
|
193
|
+
}
|
|
194
|
+
const nextSurfaces = { ...surfaces };
|
|
195
|
+
delete nextSurfaces[surfaceName];
|
|
196
|
+
return Object.keys(nextSurfaces).length > 0 ? nextSurfaces : undefined;
|
|
197
|
+
};
|
|
198
|
+
export const validateAgentSurfaceSupport = (manifest) => {
|
|
199
|
+
const runtimeName = getRuntimeName(manifest.runtime);
|
|
200
|
+
if (!runtimeName) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
assertRuntimeSupportsAgentSurfaces(runtimeName, resolveAgentSurfaces(manifest.surfaces), manifest.name);
|
|
204
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EmittedFile } from "../runtime/index.js";
|
|
2
|
+
import type { CompilePlan, ResolvedAgentNode, ResolvedMemberRef, ResolvedTeamNode } from "./types.js";
|
|
3
|
+
import type { AgentContext, TeamContextIndex } from "./teamContextTypes.js";
|
|
4
|
+
export declare const pathSafe: (value: string) => string;
|
|
5
|
+
export declare const shortHash: (value: string) => string;
|
|
6
|
+
export declare const reportKeySegment: (value: string) => string;
|
|
7
|
+
export declare const findTeamBySource: (plan: CompilePlan, source: string) => ResolvedTeamNode | null;
|
|
8
|
+
export declare const findAgentBySource: (plan: CompilePlan, source: string) => ResolvedAgentNode | null;
|
|
9
|
+
export declare const getSystemTeamDoc: (teamNode: ResolvedTeamNode) => string;
|
|
10
|
+
export declare const createContextBaseKey: (context: AgentContext) => string;
|
|
11
|
+
export declare const collectMoltnetBindings: (agentNode: ResolvedAgentNode, teamSource: string, memberId: string) => Array<{
|
|
12
|
+
network: string;
|
|
13
|
+
rooms: string[];
|
|
14
|
+
}>;
|
|
15
|
+
export declare const createTeamCard: (contextKey: string, member: ResolvedMemberRef, childTeam: ResolvedTeamNode, representatives: string[]) => EmittedFile;
|
|
16
|
+
export declare const buildContextOrientation: (index: TeamContextIndex) => string;
|
|
17
|
+
export declare const createVisibleTeamCards: (plan: CompilePlan, contextKey: string, teamNode: ResolvedTeamNode, visibleMembers: ResolvedMemberRef[]) => EmittedFile[];
|
|
18
|
+
export declare const generateContextRoster: (teamNode: ResolvedTeamNode, plan: CompilePlan, options: import("./teamRosterTypes.js").GenerateTeamRosterOptions) => import("./teamRosterTypes.js").GeneratedTeamRoster;
|