totemlayer 0.1.6 → 0.1.12
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 +26 -44
- package/bin/agent-profile.mjs +18 -2
- package/bin/agent-runner.mjs +25 -17
- package/bin/agent-setup.mjs +141 -55
- package/bin/automatic-runtime.mjs +131 -0
- package/bin/cloud-bootstrap.mjs +64 -0
- package/bin/connected-agent-service.mjs +84 -0
- package/bin/device-login.mjs +168 -0
- package/bin/installation.mjs +160 -0
- package/bin/mcp-install.mjs +74 -18
- package/bin/mcp.mjs +55 -0
- package/bin/mission-agent-service.mjs +40 -2
- package/bin/mission-infrastructure.mjs +609 -0
- package/bin/mission-worker.mjs +59 -3
- package/bin/model-adapter.mjs +164 -0
- package/bin/network-runtime.mjs +58 -3
- package/bin/platform-client.mjs +3 -0
- package/bin/provider-detection.mjs +94 -0
- package/bin/runtime-installation.mjs +305 -0
- package/bin/sdk/index.js +5 -0
- package/bin/totem.mjs +13 -3
- package/bin/worker-core/adapters.mjs +279 -0
- package/bin/worker-core/errors.mjs +60 -0
- package/package.json +2 -1
- package/scripts/prepare.mjs +5 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { join, extname } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import {
|
|
8
|
+
assertMissionAgentSession,
|
|
9
|
+
ensurePrivateRuntimeDirectory,
|
|
10
|
+
readPrivateRuntimeJson,
|
|
11
|
+
} from "./mission-agent-session.mjs";
|
|
12
|
+
import { inspectLocalRepository } from "./repository.mjs";
|
|
13
|
+
|
|
14
|
+
const execute = promisify(execFile);
|
|
15
|
+
const sha = (value) => createHash("sha256").update(value).digest("hex");
|
|
16
|
+
const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
|
|
17
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
18
|
+
const KINDS = new Set(["infrastructure_deploy", "infrastructure_verify"]);
|
|
19
|
+
const LIMIT = 20 * 1024 * 1024;
|
|
20
|
+
const MIME = {
|
|
21
|
+
".html": "text/html",
|
|
22
|
+
".css": "text/css",
|
|
23
|
+
".js": "text/javascript",
|
|
24
|
+
".json": "application/json",
|
|
25
|
+
".svg": "image/svg+xml",
|
|
26
|
+
".png": "image/png",
|
|
27
|
+
".jpg": "image/jpeg",
|
|
28
|
+
".webp": "image/webp",
|
|
29
|
+
".ico": "image/x-icon",
|
|
30
|
+
".woff2": "font/woff2",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
async function save(path, value) {
|
|
34
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
35
|
+
try {
|
|
36
|
+
await writeFile(temporary, JSON.stringify(value), { mode: 0o600, flag: "wx" });
|
|
37
|
+
await rename(temporary, path);
|
|
38
|
+
} finally {
|
|
39
|
+
await rm(temporary, { force: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function load(path, max = 2 * 1024 * 1024) {
|
|
43
|
+
return readPrivateRuntimeJson(path, max).catch((error) => {
|
|
44
|
+
if (error.code === "ENOENT") return null;
|
|
45
|
+
throw error;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Read committed blobs directly: no checkout hooks, builds, dependency installs, or untracked secrets. */
|
|
50
|
+
export async function packageStaticApp(repositoryRoot, commit, sourceDirectory, signal) {
|
|
51
|
+
if (
|
|
52
|
+
!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(commit) ||
|
|
53
|
+
!/^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/.test(sourceDirectory)
|
|
54
|
+
)
|
|
55
|
+
throw new Error("Provide a pinned commit and a relative static app directory.");
|
|
56
|
+
const git = async (args, maxBuffer = LIMIT) =>
|
|
57
|
+
(
|
|
58
|
+
await execute("git", ["-C", repositoryRoot, ...args], {
|
|
59
|
+
encoding: "buffer",
|
|
60
|
+
maxBuffer,
|
|
61
|
+
timeout: 30_000,
|
|
62
|
+
signal,
|
|
63
|
+
env: { ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_TERMINAL_PROMPT: "0" },
|
|
64
|
+
})
|
|
65
|
+
).stdout;
|
|
66
|
+
const tree = await git(["ls-tree", "-rz", commit, "--", `${sourceDirectory}/`]);
|
|
67
|
+
const files = [];
|
|
68
|
+
let total = 0;
|
|
69
|
+
for (const entry of tree.toString("utf8").split("\0").filter(Boolean)) {
|
|
70
|
+
const match = /^(\d+) blob ([a-f0-9]+)\t(.+)$/.exec(entry);
|
|
71
|
+
if (!match || !["100644", "100755"].includes(match[1]))
|
|
72
|
+
throw new Error("Static apps cannot contain symbolic links or submodules.");
|
|
73
|
+
const path = match[3].slice(sourceDirectory.length + 1);
|
|
74
|
+
if (
|
|
75
|
+
!path ||
|
|
76
|
+
path
|
|
77
|
+
.split("/")
|
|
78
|
+
.some(
|
|
79
|
+
(part) =>
|
|
80
|
+
!part || part.startsWith(".") || /^(?:node_modules|credentials|secrets)$/i.test(part),
|
|
81
|
+
) ||
|
|
82
|
+
/(?:\.pem|\.key|\.p12|\.pfx|\.map)$/i.test(path) ||
|
|
83
|
+
!MIME[extname(path).toLowerCase()]
|
|
84
|
+
)
|
|
85
|
+
throw new Error(`Unsupported or private static app file: ${path}`);
|
|
86
|
+
const bytes = await git(["cat-file", "blob", match[2]]);
|
|
87
|
+
total += bytes.length;
|
|
88
|
+
if (total > LIMIT || files.length >= 1000)
|
|
89
|
+
throw new Error("The local static bundle limit is 20 MiB and 1000 files.");
|
|
90
|
+
files.push({ path, sha256: sha(bytes), content: bytes.toString("base64") });
|
|
91
|
+
}
|
|
92
|
+
files.sort((a, b) => a.path.localeCompare(b.path, "en"));
|
|
93
|
+
const index = files.find((file) => file.path === "index.html");
|
|
94
|
+
if (!index) throw new Error("The static app directory needs a committed index.html.");
|
|
95
|
+
return {
|
|
96
|
+
commit,
|
|
97
|
+
sourceDirectory,
|
|
98
|
+
files,
|
|
99
|
+
indexSha256: index.sha256,
|
|
100
|
+
bundleSha256: sha(JSON.stringify(files.map(({ path, sha256 }) => ({ path, sha256 })))),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function checkStaticRelease(url, intent, signal) {
|
|
105
|
+
const parsed = new URL(url);
|
|
106
|
+
if (
|
|
107
|
+
parsed.protocol !== "http:" ||
|
|
108
|
+
parsed.hostname !== "127.0.0.1" ||
|
|
109
|
+
!parsed.port ||
|
|
110
|
+
parsed.username ||
|
|
111
|
+
parsed.password ||
|
|
112
|
+
parsed.search ||
|
|
113
|
+
parsed.hash ||
|
|
114
|
+
parsed.pathname !== `/releases/${intent.bundleSha256}/`
|
|
115
|
+
)
|
|
116
|
+
throw new Error("Only the exact loopback release URL can be verified.");
|
|
117
|
+
const response = await fetch(url, {
|
|
118
|
+
redirect: "error",
|
|
119
|
+
signal: signal
|
|
120
|
+
? AbortSignal.any([signal, AbortSignal.timeout(5000)])
|
|
121
|
+
: AbortSignal.timeout(5000),
|
|
122
|
+
});
|
|
123
|
+
if (response.status !== 200)
|
|
124
|
+
throw new Error(`Local app health check returned ${response.status}.`);
|
|
125
|
+
let size = 0;
|
|
126
|
+
const hash = createHash("sha256");
|
|
127
|
+
for await (const chunk of response.body) {
|
|
128
|
+
size += chunk.length;
|
|
129
|
+
if (size > LIMIT) throw new Error("The local health response exceeds the bundle limit.");
|
|
130
|
+
hash.update(chunk);
|
|
131
|
+
}
|
|
132
|
+
const observedIndexSha256 = hash.digest("hex");
|
|
133
|
+
if (observedIndexSha256 !== intent.indexSha256)
|
|
134
|
+
throw new Error("The deployed response does not match the approved bundle.");
|
|
135
|
+
return { healthStatus: 200, observedIndexSha256, checkedAt: new Date().toISOString() };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The connected agent owns this isolated origin; releases survive restart, availability follows its lifetime. */
|
|
139
|
+
export async function createLocalStaticProvider({ directory, port = 0 }) {
|
|
140
|
+
await ensurePrivateRuntimeDirectory(directory);
|
|
141
|
+
const releases = join(directory, "releases");
|
|
142
|
+
const operations = join(directory, "operations");
|
|
143
|
+
const prepared = join(directory, "prepared");
|
|
144
|
+
await ensurePrivateRuntimeDirectory(prepared);
|
|
145
|
+
await ensurePrivateRuntimeDirectory(releases);
|
|
146
|
+
await ensurePrivateRuntimeDirectory(operations);
|
|
147
|
+
const saved = await load(join(directory, "server.json"));
|
|
148
|
+
const selectedPort = saved?.port ?? port;
|
|
149
|
+
if (!Number.isInteger(selectedPort) || selectedPort < 0 || selectedPort > 65535)
|
|
150
|
+
throw new Error("The saved preview port needs inspection.");
|
|
151
|
+
const server = createServer(async (request, response) => {
|
|
152
|
+
try {
|
|
153
|
+
if (!["GET", "HEAD"].includes(request.method)) {
|
|
154
|
+
response.writeHead(405).end();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const path = new URL(request.url, "http://127.0.0.1").pathname;
|
|
158
|
+
const match = /^\/releases\/([a-f0-9]{64})\/(.*)$/.exec(path);
|
|
159
|
+
if (!match) {
|
|
160
|
+
response.writeHead(404).end();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const bundle = await load(join(releases, `${match[1]}.json`), LIMIT * 2);
|
|
164
|
+
const filePath = decodeURIComponent(match[2] || "index.html");
|
|
165
|
+
const file = bundle?.files?.find((entry) => entry.path === filePath);
|
|
166
|
+
if (!file || !MIME[extname(filePath).toLowerCase()]) {
|
|
167
|
+
response.writeHead(404).end();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const bytes = Buffer.from(file.content, "base64");
|
|
171
|
+
response.writeHead(200, {
|
|
172
|
+
"content-type": MIME[extname(filePath).toLowerCase()],
|
|
173
|
+
"content-length": bytes.length,
|
|
174
|
+
"cache-control": "no-store",
|
|
175
|
+
"x-content-type-options": "nosniff",
|
|
176
|
+
"content-security-policy":
|
|
177
|
+
"sandbox allow-scripts; default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'none'; form-action 'none'; frame-ancestors 'none'",
|
|
178
|
+
});
|
|
179
|
+
response.end(request.method === "HEAD" ? undefined : bytes);
|
|
180
|
+
} catch {
|
|
181
|
+
response.writeHead(500).end();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
await new Promise((resolve, reject) => {
|
|
185
|
+
server.once("error", reject);
|
|
186
|
+
server.listen(selectedPort, "127.0.0.1", resolve);
|
|
187
|
+
});
|
|
188
|
+
const actualPort = server.address().port;
|
|
189
|
+
try {
|
|
190
|
+
await save(join(directory, "server.json"), { port: actualPort });
|
|
191
|
+
} catch (error) {
|
|
192
|
+
await new Promise((resolve) => server.close(resolve));
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
async prepare(bundle) {
|
|
197
|
+
await save(join(prepared, `${bundle.bundleSha256}.json`), bundle);
|
|
198
|
+
},
|
|
199
|
+
async preparedBundle(digest) {
|
|
200
|
+
if (!SHA.test(digest)) throw new Error("Invalid prepared bundle identity.");
|
|
201
|
+
const bundle = await load(join(prepared, `${digest}.json`), LIMIT * 2);
|
|
202
|
+
if (!bundle)
|
|
203
|
+
throw new Error("The authorized source bundle is missing; inspect the saved task.");
|
|
204
|
+
return bundle;
|
|
205
|
+
},
|
|
206
|
+
async deploy(operationId, bundle, signal) {
|
|
207
|
+
if (!UUID.test(operationId) || !SHA.test(bundle.bundleSha256))
|
|
208
|
+
throw new Error("Invalid local operation identity.");
|
|
209
|
+
const prior = await load(join(operations, `${operationId}.json`));
|
|
210
|
+
if (prior) {
|
|
211
|
+
if (prior.commit !== bundle.commit || prior.bundleSha256 !== bundle.bundleSha256)
|
|
212
|
+
throw new Error("This deployment operation already identifies another bundle.");
|
|
213
|
+
return prior;
|
|
214
|
+
}
|
|
215
|
+
await save(join(releases, `${bundle.bundleSha256}.json`), bundle);
|
|
216
|
+
const url = `http://127.0.0.1:${actualPort}/releases/${bundle.bundleSha256}/`;
|
|
217
|
+
const receipt = {
|
|
218
|
+
provider: "local-static",
|
|
219
|
+
operationId,
|
|
220
|
+
commit: bundle.commit,
|
|
221
|
+
sourceDirectory: bundle.sourceDirectory,
|
|
222
|
+
bundleSha256: bundle.bundleSha256,
|
|
223
|
+
indexSha256: bundle.indexSha256,
|
|
224
|
+
url,
|
|
225
|
+
...(await checkStaticRelease(url, bundle, signal)),
|
|
226
|
+
};
|
|
227
|
+
await save(join(operations, `${operationId}.json`), receipt);
|
|
228
|
+
return receipt;
|
|
229
|
+
},
|
|
230
|
+
async reconcile(operationId) {
|
|
231
|
+
if (!UUID.test(operationId)) throw new Error("Invalid local operation identity.");
|
|
232
|
+
return load(join(operations, `${operationId}.json`));
|
|
233
|
+
},
|
|
234
|
+
async close() {
|
|
235
|
+
server.closeAllConnections();
|
|
236
|
+
await new Promise((resolve) => server.close(resolve));
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function runInfrastructureWorkerCycle({
|
|
242
|
+
client,
|
|
243
|
+
baseUrl,
|
|
244
|
+
missionId,
|
|
245
|
+
candidateWorkItem,
|
|
246
|
+
recoveryOnly,
|
|
247
|
+
repositoryRoot,
|
|
248
|
+
stateDirectory,
|
|
249
|
+
expectedAgentId,
|
|
250
|
+
_session,
|
|
251
|
+
infrastructureProvider,
|
|
252
|
+
signal,
|
|
253
|
+
emit = () => {},
|
|
254
|
+
}) {
|
|
255
|
+
const kind = candidateWorkItem?.metadata?.kind;
|
|
256
|
+
if (!recoveryOnly && !KINDS.has(kind)) return false;
|
|
257
|
+
if (recoveryOnly && !infrastructureProvider) return false;
|
|
258
|
+
if (!_session || !infrastructureProvider)
|
|
259
|
+
throw new Error("Infrastructure work requires the connected agent service.");
|
|
260
|
+
assertMissionAgentSession(_session, baseUrl, expectedAgentId);
|
|
261
|
+
if (!UUID.test(missionId) || !UUID.test(expectedAgentId))
|
|
262
|
+
throw new Error("Invalid infrastructure task identity.");
|
|
263
|
+
const directory = join(stateDirectory, expectedAgentId, missionId);
|
|
264
|
+
await ensurePrivateRuntimeDirectory(join(stateDirectory, expectedAgentId));
|
|
265
|
+
await ensurePrivateRuntimeDirectory(directory);
|
|
266
|
+
let record;
|
|
267
|
+
let item = candidateWorkItem;
|
|
268
|
+
let path;
|
|
269
|
+
if (recoveryOnly) {
|
|
270
|
+
for (const file of await readdir(directory)) {
|
|
271
|
+
if (!UUID.test(file.slice(0, -5)) || !file.endsWith(".json")) continue;
|
|
272
|
+
const found = await load(join(directory, file));
|
|
273
|
+
if (KINDS.has(found?.kind) && found.state !== "done" && found.state !== "blocked") {
|
|
274
|
+
record = found;
|
|
275
|
+
path = join(directory, file);
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!record) return false;
|
|
280
|
+
} else {
|
|
281
|
+
path = join(directory, `${item.id}.json`);
|
|
282
|
+
record = await load(path);
|
|
283
|
+
if (record?.state === "done" || record?.state === "blocked") return true;
|
|
284
|
+
}
|
|
285
|
+
let launched =
|
|
286
|
+
!!record?.effectId && ["infra_dispatched", "result_settling", "ready"].includes(record.state);
|
|
287
|
+
let timer;
|
|
288
|
+
const controller = new AbortController();
|
|
289
|
+
const activeSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
290
|
+
const persist = () => save(path, record);
|
|
291
|
+
try {
|
|
292
|
+
const identity = await client.agent({ signal: activeSignal });
|
|
293
|
+
if (identity.id !== expectedAgentId)
|
|
294
|
+
throw new Error("The infrastructure agent's identity changed.");
|
|
295
|
+
if (!record) {
|
|
296
|
+
record = {
|
|
297
|
+
kind,
|
|
298
|
+
state: "preparing",
|
|
299
|
+
workItemId: item.id,
|
|
300
|
+
claimKey: `infra-claim:${item.id}:${item.attempts ?? 0}`,
|
|
301
|
+
};
|
|
302
|
+
await persist();
|
|
303
|
+
const mission = await client.missions.get(missionId, { signal: activeSignal });
|
|
304
|
+
if (mission.infrastructure?.provider !== "local-static")
|
|
305
|
+
throw new Error("Unsupported infrastructure provider.");
|
|
306
|
+
const repository = await inspectLocalRepository({ TOTEM_REPOSITORY_ROOT: repositoryRoot });
|
|
307
|
+
// Same canonical identity check as the coding worker's explicit repository binding.
|
|
308
|
+
if (!repository || repository.repository !== mission.repository?.url)
|
|
309
|
+
throw new Error("Bind the mission's repository before deploying its app.");
|
|
310
|
+
let source;
|
|
311
|
+
if (kind === "infrastructure_deploy") {
|
|
312
|
+
const review = mission.workItems.find(
|
|
313
|
+
(dependency) =>
|
|
314
|
+
item.dependsOnIds.includes(dependency.id) && dependency.metadata?.kind === "review",
|
|
315
|
+
);
|
|
316
|
+
const code = mission.workItems.find(
|
|
317
|
+
(dependency) =>
|
|
318
|
+
review?.dependsOnIds.includes(dependency.id) && dependency.metadata?.kind === "code",
|
|
319
|
+
);
|
|
320
|
+
if (
|
|
321
|
+
review?.result?.approved !== true ||
|
|
322
|
+
!code?.result?.headCommit ||
|
|
323
|
+
code.ownerMemberId === review.ownerMemberId
|
|
324
|
+
)
|
|
325
|
+
throw new Error("An approved independent review is required before deployment.");
|
|
326
|
+
source = {
|
|
327
|
+
commit: code.result.headCommit,
|
|
328
|
+
sourceDirectory: mission.infrastructure.sourceDirectory,
|
|
329
|
+
};
|
|
330
|
+
} else {
|
|
331
|
+
const deployment = mission.workItems.find(
|
|
332
|
+
(dependency) =>
|
|
333
|
+
item.dependsOnIds.includes(dependency.id) &&
|
|
334
|
+
dependency.metadata?.kind === "infrastructure_deploy",
|
|
335
|
+
);
|
|
336
|
+
if (!deployment?.result?.receipt)
|
|
337
|
+
throw new Error("The preceding deployment needs a receipt.");
|
|
338
|
+
source = { ...deployment.result.receipt, deploymentEffectId: deployment.result.effectId };
|
|
339
|
+
}
|
|
340
|
+
record.source = source;
|
|
341
|
+
record.repositoryResource = mission.repository.url;
|
|
342
|
+
record.state = "claiming";
|
|
343
|
+
await persist();
|
|
344
|
+
}
|
|
345
|
+
if (record.state === "claiming") {
|
|
346
|
+
const claim = await client.missions.claim(missionId, record.workItemId, {
|
|
347
|
+
signal: activeSignal,
|
|
348
|
+
idempotencyKey: record.claimKey,
|
|
349
|
+
});
|
|
350
|
+
record.leaseToken = claim.leaseToken;
|
|
351
|
+
record.state = "infra_source";
|
|
352
|
+
await persist();
|
|
353
|
+
}
|
|
354
|
+
if (record.state !== "ready")
|
|
355
|
+
await client.missions.renew(missionId, record.workItemId, record.leaseToken, {
|
|
356
|
+
signal: activeSignal,
|
|
357
|
+
});
|
|
358
|
+
timer =
|
|
359
|
+
record.state === "ready"
|
|
360
|
+
? undefined
|
|
361
|
+
: setInterval(() => {
|
|
362
|
+
client.missions
|
|
363
|
+
.renew(missionId, record.workItemId, record.leaseToken, { signal: activeSignal })
|
|
364
|
+
.catch((error) => controller.abort(error));
|
|
365
|
+
}, 10_000);
|
|
366
|
+
emit({ type: "work_started", missionId, workItemId: record.workItemId, stage: record.kind });
|
|
367
|
+
const action = record.kind === "infrastructure_deploy" ? "app.deploy" : "app.verify";
|
|
368
|
+
if (record.state === "infra_source") {
|
|
369
|
+
const effect = await client.missions.reserveEffect(
|
|
370
|
+
missionId,
|
|
371
|
+
{
|
|
372
|
+
workItemId: record.workItemId,
|
|
373
|
+
leaseToken: record.leaseToken,
|
|
374
|
+
action: "repository.read",
|
|
375
|
+
resource: record.repositoryResource,
|
|
376
|
+
estimatedCostCents: 0,
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
signal: activeSignal,
|
|
380
|
+
idempotencyKey: `infra-source:${record.workItemId}:${record.claimKey}`,
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
record.readEffectId = effect.id;
|
|
384
|
+
record.state = "infra_source_dispatched";
|
|
385
|
+
await persist();
|
|
386
|
+
const launch = await client.missions.dispatchEffect(missionId, effect.id, record.leaseToken, {
|
|
387
|
+
signal: activeSignal,
|
|
388
|
+
});
|
|
389
|
+
if (launch.launchPermitted !== true) throw new Error("The source read was not permitted.");
|
|
390
|
+
activeSignal.throwIfAborted();
|
|
391
|
+
const bundle = await packageStaticApp(
|
|
392
|
+
repositoryRoot,
|
|
393
|
+
record.source.commit,
|
|
394
|
+
record.source.sourceDirectory,
|
|
395
|
+
activeSignal,
|
|
396
|
+
);
|
|
397
|
+
if (
|
|
398
|
+
record.kind === "infrastructure_verify" &&
|
|
399
|
+
(bundle.bundleSha256 !== record.source.bundleSha256 ||
|
|
400
|
+
bundle.indexSha256 !== record.source.indexSha256)
|
|
401
|
+
)
|
|
402
|
+
throw new Error("The verification checkout does not match the deployed bundle.");
|
|
403
|
+
await infrastructureProvider.prepare(bundle);
|
|
404
|
+
record.parameters = {
|
|
405
|
+
commit: bundle.commit,
|
|
406
|
+
sourceDirectory: bundle.sourceDirectory,
|
|
407
|
+
bundleSha256: bundle.bundleSha256,
|
|
408
|
+
indexSha256: bundle.indexSha256,
|
|
409
|
+
...(record.source.deploymentEffectId
|
|
410
|
+
? { deploymentEffectId: record.source.deploymentEffectId }
|
|
411
|
+
: {}),
|
|
412
|
+
};
|
|
413
|
+
if (record.source.url) record.deploymentUrl = record.source.url;
|
|
414
|
+
record.state = "infra_source_settling";
|
|
415
|
+
await persist();
|
|
416
|
+
} else if (record.state === "infra_source_dispatched") {
|
|
417
|
+
// Source inspection has no external mutation. An interrupted read is reported failed; do not repeat it on a stale permission.
|
|
418
|
+
await client.missions.settleEffect(
|
|
419
|
+
missionId,
|
|
420
|
+
record.readEffectId,
|
|
421
|
+
{ state: "failed", actualCostCents: 0 },
|
|
422
|
+
{ signal: activeSignal },
|
|
423
|
+
);
|
|
424
|
+
record.state = "blocked";
|
|
425
|
+
throw new Error(
|
|
426
|
+
"The authorized source read was interrupted. Inspect it before assigning a retry.",
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (record.state === "infra_source_settling") {
|
|
430
|
+
await client.missions.settleEffect(
|
|
431
|
+
missionId,
|
|
432
|
+
record.readEffectId,
|
|
433
|
+
{ state: "succeeded", actualCostCents: 0 },
|
|
434
|
+
{ signal: activeSignal },
|
|
435
|
+
);
|
|
436
|
+
record.state = "infra_reserved";
|
|
437
|
+
await persist();
|
|
438
|
+
}
|
|
439
|
+
if (record.state === "infra_reserved") {
|
|
440
|
+
const effect = await client.missions.reserveEffect(
|
|
441
|
+
missionId,
|
|
442
|
+
{
|
|
443
|
+
workItemId: record.workItemId,
|
|
444
|
+
leaseToken: record.leaseToken,
|
|
445
|
+
action,
|
|
446
|
+
resource: `totem:mission:${missionId}:app`,
|
|
447
|
+
parameters: record.parameters,
|
|
448
|
+
estimatedCostCents: 0,
|
|
449
|
+
},
|
|
450
|
+
{ signal: activeSignal, idempotencyKey: `infra:${record.workItemId}:${record.claimKey}` },
|
|
451
|
+
);
|
|
452
|
+
record.effectId = effect.id;
|
|
453
|
+
// Save intent before the dispatch request; a lost dispatch response is never relaunched.
|
|
454
|
+
record.state = "infra_dispatched";
|
|
455
|
+
await persist();
|
|
456
|
+
launched = true;
|
|
457
|
+
const launch = await client.missions.dispatchEffect(missionId, effect.id, record.leaseToken, {
|
|
458
|
+
signal: activeSignal,
|
|
459
|
+
});
|
|
460
|
+
if (launch.launchPermitted !== true)
|
|
461
|
+
throw new Error("The infrastructure launch was not permitted.");
|
|
462
|
+
activeSignal.throwIfAborted();
|
|
463
|
+
record.receipt =
|
|
464
|
+
action === "app.deploy"
|
|
465
|
+
? await infrastructureProvider.deploy(
|
|
466
|
+
effect.id,
|
|
467
|
+
await infrastructureProvider.preparedBundle(record.parameters.bundleSha256),
|
|
468
|
+
activeSignal,
|
|
469
|
+
)
|
|
470
|
+
: {
|
|
471
|
+
...record.parameters,
|
|
472
|
+
provider: "local-static",
|
|
473
|
+
operationId: effect.id,
|
|
474
|
+
url: record.deploymentUrl,
|
|
475
|
+
...(await checkStaticRelease(record.deploymentUrl, record.parameters, activeSignal)),
|
|
476
|
+
};
|
|
477
|
+
record.state = "result_settling";
|
|
478
|
+
await persist();
|
|
479
|
+
} else if (record.state === "infra_dispatched") {
|
|
480
|
+
record.receipt =
|
|
481
|
+
action === "app.deploy" ? await infrastructureProvider.reconcile(record.effectId) : null;
|
|
482
|
+
if (!record.receipt) {
|
|
483
|
+
record.state = "blocked";
|
|
484
|
+
throw new Error(
|
|
485
|
+
"The operation's outcome is unknown; reconcile it before trying another deployment.",
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
record.state = "result_settling";
|
|
489
|
+
await persist();
|
|
490
|
+
}
|
|
491
|
+
if (record.state === "result_settling") {
|
|
492
|
+
await client.missions.settleEffect(
|
|
493
|
+
missionId,
|
|
494
|
+
record.effectId,
|
|
495
|
+
{
|
|
496
|
+
state: "succeeded",
|
|
497
|
+
actualCostCents: 0,
|
|
498
|
+
receipt: record.receipt,
|
|
499
|
+
},
|
|
500
|
+
{ signal: activeSignal },
|
|
501
|
+
);
|
|
502
|
+
record.state = "ready";
|
|
503
|
+
await persist();
|
|
504
|
+
}
|
|
505
|
+
const summary = `${action === "app.deploy" ? "Published" : "Verified"} the reviewed static app locally. The preview is available while its deployment agent is connected.`;
|
|
506
|
+
await client.missions.complete(
|
|
507
|
+
missionId,
|
|
508
|
+
record.workItemId,
|
|
509
|
+
{
|
|
510
|
+
leaseToken: record.leaseToken,
|
|
511
|
+
artifact: {
|
|
512
|
+
kind: "infrastructure",
|
|
513
|
+
title: action === "app.deploy" ? "Local app deployment" : "Local app verification",
|
|
514
|
+
summary,
|
|
515
|
+
uri: record.receipt.url,
|
|
516
|
+
contentText: JSON.stringify({ summary, receipt: record.receipt }),
|
|
517
|
+
metadata: { receipt: record.receipt },
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
{ signal: activeSignal, idempotencyKey: `infra-complete:${record.effectId}` },
|
|
521
|
+
);
|
|
522
|
+
record.state = "done";
|
|
523
|
+
await persist();
|
|
524
|
+
emit({ type: "work_completed", missionId, workItemId: record.workItemId, stage: record.kind });
|
|
525
|
+
} catch (error) {
|
|
526
|
+
const transient =
|
|
527
|
+
(error instanceof TypeError && /network|fetch/i.test(error.message)) ||
|
|
528
|
+
[408, 425, 429, 500, 502, 503, 504].includes(error.status) ||
|
|
529
|
+
["NETWORK_ERROR", "TIMEOUT", "ECONNRESET", "ECONNREFUSED", "ETIMEDOUT"].includes(error.code);
|
|
530
|
+
if (record?.state === "preparing" && (transient || signal?.aborted)) {
|
|
531
|
+
await rm(path, { force: true });
|
|
532
|
+
record = null;
|
|
533
|
+
}
|
|
534
|
+
if (record) {
|
|
535
|
+
if (record.state === "infra_source_dispatched" && !signal?.aborted && !transient) {
|
|
536
|
+
await client.missions
|
|
537
|
+
.settleEffect(
|
|
538
|
+
missionId,
|
|
539
|
+
record.readEffectId,
|
|
540
|
+
{ state: "failed", actualCostCents: 0 },
|
|
541
|
+
{ signal },
|
|
542
|
+
)
|
|
543
|
+
.catch(() => {});
|
|
544
|
+
record.state = "blocked";
|
|
545
|
+
}
|
|
546
|
+
if (launched && record.state === "infra_dispatched") {
|
|
547
|
+
await client.missions
|
|
548
|
+
.settleEffect(missionId, record.effectId, { state: "uncertain" }, { signal })
|
|
549
|
+
.catch(() => {});
|
|
550
|
+
}
|
|
551
|
+
// Keep recoverable results, including a lost completion response. Never relaunch a saved operation.
|
|
552
|
+
if (
|
|
553
|
+
![
|
|
554
|
+
"claiming",
|
|
555
|
+
"infra_source",
|
|
556
|
+
"infra_source_dispatched",
|
|
557
|
+
"infra_source_settling",
|
|
558
|
+
"infra_reserved",
|
|
559
|
+
"infra_dispatched",
|
|
560
|
+
"result_settling",
|
|
561
|
+
"ready",
|
|
562
|
+
].includes(record.state)
|
|
563
|
+
)
|
|
564
|
+
record.state = "blocked";
|
|
565
|
+
record.error = error.message;
|
|
566
|
+
await persist();
|
|
567
|
+
if (
|
|
568
|
+
!signal?.aborted &&
|
|
569
|
+
!transient &&
|
|
570
|
+
!record.blockerRecorded &&
|
|
571
|
+
client.missions.events?.post
|
|
572
|
+
) {
|
|
573
|
+
await client.missions.events
|
|
574
|
+
.post(
|
|
575
|
+
missionId,
|
|
576
|
+
{
|
|
577
|
+
version: 1,
|
|
578
|
+
type: "blocker",
|
|
579
|
+
workItemId: record.workItemId,
|
|
580
|
+
payload: {
|
|
581
|
+
summary: "Infrastructure task needs attention.",
|
|
582
|
+
helpNeeded: error.message.slice(0, 4000),
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
idempotencyKey: `infra-blocker:${record.workItemId}:${record.effectId ?? record.claimKey}`,
|
|
587
|
+
signal,
|
|
588
|
+
},
|
|
589
|
+
)
|
|
590
|
+
.then(async () => {
|
|
591
|
+
record.blockerRecorded = true;
|
|
592
|
+
await persist();
|
|
593
|
+
})
|
|
594
|
+
.catch(() => {});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
emit({
|
|
598
|
+
type: "needs_attention",
|
|
599
|
+
missionId,
|
|
600
|
+
workItemId: record?.workItemId ?? item?.id,
|
|
601
|
+
message: error.message,
|
|
602
|
+
});
|
|
603
|
+
if (signal?.aborted || transient || [401, 403, 409].includes(error.status)) throw error;
|
|
604
|
+
return true;
|
|
605
|
+
} finally {
|
|
606
|
+
clearInterval(timer);
|
|
607
|
+
}
|
|
608
|
+
return true;
|
|
609
|
+
}
|