totemlayer 0.1.5 → 0.1.11
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 +20 -10
- package/bin/agent-setup.mjs +141 -55
- package/bin/automatic-runtime.mjs +131 -0
- package/bin/cloud-bootstrap.mjs +64 -0
- package/bin/collaboration-listener.mjs +83 -83
- package/bin/connected-agent-service.mjs +84 -0
- package/bin/device-login.mjs +168 -0
- package/bin/docker-test-sandbox.mjs +1 -1
- package/bin/installation.mjs +160 -0
- package/bin/mcp-install.mjs +74 -18
- package/bin/mcp.mjs +56 -1
- package/bin/mission-agent-service.mjs +40 -2
- package/bin/mission-infrastructure.mjs +609 -0
- package/bin/mission-worker.mjs +259 -49
- 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
|
@@ -36,7 +36,7 @@ export async function runDockerNodeTests(checkout, signal, runFile) {
|
|
|
36
36
|
"--env",
|
|
37
37
|
"TMPDIR=/tmp",
|
|
38
38
|
"--mount",
|
|
39
|
-
`type=bind,source=${root},target=/workspace`,
|
|
39
|
+
`type=bind,source=${root},target=/workspace,readonly`,
|
|
40
40
|
"--workdir",
|
|
41
41
|
"/workspace",
|
|
42
42
|
NODE_TEST_IMAGE,
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { privateRead, privateWrite } from "./runtime-installation.mjs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { ensureInstalledRuntime, installRuntimeService } from "./runtime-installation.mjs";
|
|
7
|
+
import { beginDeviceLogin } from "./device-login.mjs";
|
|
8
|
+
import { detectInstalledProvider, providerArguments } from "./provider-detection.mjs";
|
|
9
|
+
|
|
10
|
+
export async function activateConnection(profile, env = process.env, options = {}) {
|
|
11
|
+
const runtime = await (options.ensureRuntime ?? ensureInstalledRuntime)(env);
|
|
12
|
+
const { automaticEnabled, setAutomaticEnabled } = await import("./automatic-runtime.mjs");
|
|
13
|
+
const enabled = await automaticEnabled(profile);
|
|
14
|
+
if (enabled) await setAutomaticEnabled(profile, true);
|
|
15
|
+
const service = await (options.installService ?? installRuntimeService)(runtime, {
|
|
16
|
+
...env,
|
|
17
|
+
...(profile.agentTokenEnv ? { TOTEM_AGENT_TOKEN_ENV: profile.agentTokenEnv } : {}),
|
|
18
|
+
});
|
|
19
|
+
if (!enabled) return { state: "paused", ...service, entry: runtime.cli };
|
|
20
|
+
const { agentStartMain } = await import("./agent-runner.mjs");
|
|
21
|
+
await (options.start ?? agentStartMain)(
|
|
22
|
+
["--profile", profile.profileName, "--background", "--json"],
|
|
23
|
+
{ env, automatic: true, stdout: { write() {} } },
|
|
24
|
+
);
|
|
25
|
+
return { state: "running", ...service, entry: runtime.cli };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function installationMain(args = [], options = {}) {
|
|
29
|
+
const env = options.env ?? process.env;
|
|
30
|
+
const stdout = options.stdout ?? process.stdout;
|
|
31
|
+
const flags = parseArgs({
|
|
32
|
+
args,
|
|
33
|
+
strict: true,
|
|
34
|
+
options: {
|
|
35
|
+
provider: { type: "string" },
|
|
36
|
+
"agent-id": { type: "string" },
|
|
37
|
+
"base-url": { type: "string" },
|
|
38
|
+
"no-browser": { type: "boolean" },
|
|
39
|
+
help: { type: "boolean", short: "h" },
|
|
40
|
+
json: { type: "boolean" },
|
|
41
|
+
},
|
|
42
|
+
}).values;
|
|
43
|
+
if (flags.help) {
|
|
44
|
+
stdout.write(
|
|
45
|
+
"totem login [--provider codex|claude|qwen-local|ollama-local|nim-local|command|http] [--no-browser]\nConnect this installation once. Totem securely saves its connection and automatically runs and restarts the listener.\n",
|
|
46
|
+
);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
const environment = {
|
|
50
|
+
...env,
|
|
51
|
+
...(flags.provider ? { TOTEM_PROVIDER: flags.provider } : {}),
|
|
52
|
+
...(flags["agent-id"] ? { TOTEM_AGENT_ID: flags["agent-id"] } : {}),
|
|
53
|
+
...(flags["base-url"] ? { TOTEM_BASE_URL: flags["base-url"] } : {}),
|
|
54
|
+
};
|
|
55
|
+
const runtime = await (options.ensureRuntime ?? ensureInstalledRuntime)(environment);
|
|
56
|
+
const service = await (options.installService ?? installRuntimeService)(runtime, environment);
|
|
57
|
+
if (environment.TOTEM_NETWORK_API_KEY || environment.TOTEM_NETWORK_API_KEY_FILE) {
|
|
58
|
+
const provider = await (options.detectProvider ?? detectInstalledProvider)(environment);
|
|
59
|
+
if (!provider) {
|
|
60
|
+
let key = environment.TOTEM_NETWORK_API_KEY;
|
|
61
|
+
if (!key && environment.TOTEM_NETWORK_API_KEY_FILE) {
|
|
62
|
+
const info = await stat(environment.TOTEM_NETWORK_API_KEY_FILE);
|
|
63
|
+
if (
|
|
64
|
+
!info.isFile() ||
|
|
65
|
+
info.size > 8192 ||
|
|
66
|
+
(process.platform !== "win32" && info.mode & 0o077)
|
|
67
|
+
)
|
|
68
|
+
throw new Error("Use a private connection key file.");
|
|
69
|
+
key = (await readFile(environment.TOTEM_NETWORK_API_KEY_FILE, "utf8")).trim();
|
|
70
|
+
}
|
|
71
|
+
if (!/^tn_(?:test|live)_[A-Za-z0-9_-]{43}$/.test(key ?? ""))
|
|
72
|
+
throw new Error("The connection key is invalid.");
|
|
73
|
+
await privateWrite(join(runtime.directory, "connection-ready.json"), {
|
|
74
|
+
key,
|
|
75
|
+
baseUrl: environment.TOTEM_BASE_URL ?? "https://totemlayer.com",
|
|
76
|
+
agentId: environment.TOTEM_AGENT_ID,
|
|
77
|
+
});
|
|
78
|
+
stdout.write(
|
|
79
|
+
"Totem's automatic runtime is installed. Install/sign in to your AI client, then run totem login again.\n",
|
|
80
|
+
);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
const { agentSetupMain } = await import("./agent-setup.mjs");
|
|
84
|
+
return agentSetupMain(
|
|
85
|
+
[
|
|
86
|
+
"--profile",
|
|
87
|
+
environment.TOTEM_AGENT_ID ?? "default",
|
|
88
|
+
...(environment.TOTEM_AGENT_ID ? ["--agent-id", environment.TOTEM_AGENT_ID] : []),
|
|
89
|
+
...(await providerArguments(provider, environment)),
|
|
90
|
+
...(flags.json ? ["--json"] : []),
|
|
91
|
+
],
|
|
92
|
+
{
|
|
93
|
+
...options,
|
|
94
|
+
env: environment,
|
|
95
|
+
activateConnection: options.activateConnection ?? activateConnection,
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await privateRead(join(runtime.directory, "connection-ready.json"));
|
|
101
|
+
stdout.write(
|
|
102
|
+
"Your Totem connection is saved privately. Sign in to your AI client; the listener connects automatically.\n",
|
|
103
|
+
);
|
|
104
|
+
return 0;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error.code !== "ENOENT") throw error;
|
|
107
|
+
}
|
|
108
|
+
// An upgrade reuses private profiles and starts their existing connections.
|
|
109
|
+
const { opendir } = await import("node:fs/promises");
|
|
110
|
+
let saved = false;
|
|
111
|
+
try {
|
|
112
|
+
const profiles = await opendir(join(runtime.root, "connections"));
|
|
113
|
+
for await (const entry of profiles)
|
|
114
|
+
if (
|
|
115
|
+
entry.isDirectory() &&
|
|
116
|
+
(!environment.TOTEM_AGENT_ID || entry.name === environment.TOTEM_AGENT_ID)
|
|
117
|
+
)
|
|
118
|
+
saved = true;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error.code !== "ENOENT") throw error;
|
|
121
|
+
}
|
|
122
|
+
if (saved && options.forceLogin !== true) {
|
|
123
|
+
const report = { installed: true, connection: "saved", automatic: true, service };
|
|
124
|
+
stdout.write(
|
|
125
|
+
flags.json
|
|
126
|
+
? JSON.stringify(report) + "\n"
|
|
127
|
+
: "Totem's automatic listener is installed and reuses your saved connections.\n",
|
|
128
|
+
);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
const connection = await (options.beginLogin ?? beginDeviceLogin)(runtime, environment, {
|
|
132
|
+
openBrowser: !flags["no-browser"] && environment.TOTEM_NO_BROWSER !== "1",
|
|
133
|
+
});
|
|
134
|
+
const report = {
|
|
135
|
+
installed: true,
|
|
136
|
+
connection: "sign_in_required",
|
|
137
|
+
automatic: true,
|
|
138
|
+
service,
|
|
139
|
+
...connection,
|
|
140
|
+
};
|
|
141
|
+
stdout.write(
|
|
142
|
+
flags.json
|
|
143
|
+
? JSON.stringify(report) + "\n"
|
|
144
|
+
: `Totem is installed. Connect your agent once at:\n${connection.url}\nConnection code: ${connection.code.match(/.{1,4}/g).join(" ")}\nThe listener starts automatically after you connect.\n`,
|
|
145
|
+
);
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
export async function postinstallMain(env = process.env) {
|
|
149
|
+
if (env.TOTEM_AUTO_START === "0" || env.CI) return 0;
|
|
150
|
+
try {
|
|
151
|
+
return await installationMain([], { env });
|
|
152
|
+
} catch {
|
|
153
|
+
process.stdout.write(
|
|
154
|
+
"Totem is installed. Automatic connection setup needs attention; run totem login to finish it.\n",
|
|
155
|
+
);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (process.argv[1] === fileURLToPath(import.meta.url) && process.argv[2] === "--postinstall")
|
|
160
|
+
await postinstallMain();
|
package/bin/mcp-install.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { stat } from "node:fs/promises";
|
|
4
|
-
import { resolve } from "node:path";
|
|
3
|
+
import { stat, lstat } from "node:fs/promises";
|
|
4
|
+
import { resolve, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
import { installedClientInvocation, localClientEnvironment } from "./platform-client.mjs";
|
|
8
|
+
import { runtimeRoot } from "./runtime-installation.mjs";
|
|
8
9
|
|
|
9
10
|
const DEFAULT_ORIGIN = "https://totemlayer.com";
|
|
10
11
|
const DEFAULT_NAME = "totem-local";
|
|
@@ -131,19 +132,28 @@ export async function installTotemMcp(args, environment = process.env, runtime =
|
|
|
131
132
|
];
|
|
132
133
|
const addArgs =
|
|
133
134
|
options.client === "codex"
|
|
134
|
-
? [
|
|
135
|
+
? [
|
|
136
|
+
"mcp",
|
|
137
|
+
"add",
|
|
138
|
+
options.name,
|
|
139
|
+
...envArgs,
|
|
140
|
+
"--",
|
|
141
|
+
process.execPath,
|
|
142
|
+
runtime.serverEntry ?? SERVER_ENTRY,
|
|
143
|
+
"mcp",
|
|
144
|
+
]
|
|
135
145
|
: [
|
|
136
146
|
"mcp",
|
|
137
147
|
"add",
|
|
148
|
+
options.name,
|
|
138
149
|
...envArgs,
|
|
139
150
|
"--scope",
|
|
140
151
|
"user",
|
|
141
152
|
"--transport",
|
|
142
153
|
"stdio",
|
|
143
|
-
options.name,
|
|
144
154
|
"--",
|
|
145
155
|
process.execPath,
|
|
146
|
-
SERVER_ENTRY,
|
|
156
|
+
runtime.serverEntry ?? SERVER_ENTRY,
|
|
147
157
|
"mcp",
|
|
148
158
|
];
|
|
149
159
|
const added = runClient(command, addArgs, environment, runtime);
|
|
@@ -169,7 +179,26 @@ export function managedMcpName(profileName) {
|
|
|
169
179
|
: `totem_${profileName.slice(0, 24)}_${createHash("sha256").update(profileName).digest("hex").slice(0, 32)}`;
|
|
170
180
|
}
|
|
171
181
|
|
|
172
|
-
function
|
|
182
|
+
async function installedServerEntry(profile, environment) {
|
|
183
|
+
if (!profile.stateDirectory) return SERVER_ENTRY;
|
|
184
|
+
const path = join(resolve(profile.stateDirectory, "../../../.."), "runtime", "totem.mjs");
|
|
185
|
+
try {
|
|
186
|
+
const info = await lstat(path);
|
|
187
|
+
if (
|
|
188
|
+
info.isFile() &&
|
|
189
|
+
!info.isSymbolicLink() &&
|
|
190
|
+
info.nlink === 1 &&
|
|
191
|
+
(!process.getuid || info.uid === process.getuid()) &&
|
|
192
|
+
(process.platform === "win32" || (info.mode & 0o777) === 0o600)
|
|
193
|
+
)
|
|
194
|
+
return path;
|
|
195
|
+
} catch {
|
|
196
|
+
/* Connections created without automatic installation retain their entry. */
|
|
197
|
+
}
|
|
198
|
+
return SERVER_ENTRY;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function managedArgs(profile, name, entry = SERVER_ENTRY) {
|
|
173
202
|
const variables = [
|
|
174
203
|
"--env",
|
|
175
204
|
`TOTEM_BASE_URL=${profile.baseUrl}`,
|
|
@@ -177,24 +206,24 @@ function managedArgs(profile, name) {
|
|
|
177
206
|
`TOTEM_NETWORK_API_KEY_FILE=${profile.credentialFile}`,
|
|
178
207
|
];
|
|
179
208
|
return profile.client === "codex"
|
|
180
|
-
? ["mcp", "add", name, ...variables, "--", process.execPath,
|
|
209
|
+
? ["mcp", "add", name, ...variables, "--", process.execPath, entry, "mcp"]
|
|
181
210
|
: [
|
|
182
211
|
"mcp",
|
|
183
212
|
"add",
|
|
213
|
+
name,
|
|
184
214
|
...variables,
|
|
185
215
|
"--scope",
|
|
186
216
|
"user",
|
|
187
217
|
"--transport",
|
|
188
218
|
"stdio",
|
|
189
|
-
name,
|
|
190
219
|
"--",
|
|
191
220
|
process.execPath,
|
|
192
|
-
|
|
221
|
+
entry,
|
|
193
222
|
"mcp",
|
|
194
223
|
];
|
|
195
224
|
}
|
|
196
225
|
|
|
197
|
-
function matchesManagedEntry(entry, profile, name) {
|
|
226
|
+
function matchesManagedEntry(entry, profile, name, serverEntry = SERVER_ENTRY) {
|
|
198
227
|
const transport = entry?.transport;
|
|
199
228
|
const expectedEnvironment = {
|
|
200
229
|
TOTEM_BASE_URL: profile.baseUrl,
|
|
@@ -209,7 +238,7 @@ function matchesManagedEntry(entry, profile, name) {
|
|
|
209
238
|
) &&
|
|
210
239
|
transport?.type === "stdio" &&
|
|
211
240
|
transport.command === process.execPath &&
|
|
212
|
-
JSON.stringify(transport.args) === JSON.stringify([
|
|
241
|
+
JSON.stringify(transport.args) === JSON.stringify([serverEntry, "mcp"]) &&
|
|
213
242
|
transport.cwd == null &&
|
|
214
243
|
(transport.env_vars == null ||
|
|
215
244
|
(Array.isArray(transport.env_vars) && transport.env_vars.length === 0)) &&
|
|
@@ -279,10 +308,11 @@ function reconnectAdvice(profile, name) {
|
|
|
279
308
|
}
|
|
280
309
|
|
|
281
310
|
export async function inspectManagedTotemMcp(profile, environment = process.env, runtime = {}) {
|
|
311
|
+
const serverEntry = await installedServerEntry(profile, environment);
|
|
282
312
|
const inspected = await inspectEntry(profile, localClientEnvironment(environment), runtime);
|
|
283
313
|
if (
|
|
284
314
|
inspected.state === "present" &&
|
|
285
|
-
matchesManagedEntry(inspected.entry, profile, inspected.name)
|
|
315
|
+
matchesManagedEntry(inspected.entry, profile, inspected.name, serverEntry)
|
|
286
316
|
)
|
|
287
317
|
return {
|
|
288
318
|
name: inspected.name,
|
|
@@ -308,6 +338,7 @@ export async function installManagedTotemMcp(
|
|
|
308
338
|
environment = process.env,
|
|
309
339
|
runtime = {},
|
|
310
340
|
) {
|
|
341
|
+
const serverEntry = await installedServerEntry(profile, environment);
|
|
311
342
|
if (!["codex", "claude"].includes(profile.client))
|
|
312
343
|
fail("Choose an existing AI app for MCP setup.");
|
|
313
344
|
try {
|
|
@@ -337,11 +368,14 @@ export async function installManagedTotemMcp(
|
|
|
337
368
|
name,
|
|
338
369
|
],
|
|
339
370
|
safeEnvironment,
|
|
340
|
-
runtime,
|
|
371
|
+
{ ...runtime, serverEntry },
|
|
341
372
|
);
|
|
342
373
|
if (profile.client === "codex") {
|
|
343
374
|
const after = await inspectEntry(profile, safeEnvironment, runtime);
|
|
344
|
-
if (
|
|
375
|
+
if (
|
|
376
|
+
after.state !== "present" ||
|
|
377
|
+
!matchesManagedEntry(after.entry, profile, name, serverEntry)
|
|
378
|
+
)
|
|
345
379
|
return {
|
|
346
380
|
name,
|
|
347
381
|
state: "needs_attention",
|
|
@@ -361,7 +395,10 @@ export async function installManagedTotemMcp(
|
|
|
361
395
|
};
|
|
362
396
|
}
|
|
363
397
|
}
|
|
364
|
-
if (
|
|
398
|
+
if (
|
|
399
|
+
inspected.state === "present" &&
|
|
400
|
+
matchesManagedEntry(inspected.entry, profile, name, serverEntry)
|
|
401
|
+
)
|
|
365
402
|
return {
|
|
366
403
|
name,
|
|
367
404
|
state: "configured_unverified",
|
|
@@ -371,7 +408,21 @@ export async function installManagedTotemMcp(
|
|
|
371
408
|
previousProfile?.profileName === profile.profileName &&
|
|
372
409
|
previousProfile.client === profile.client &&
|
|
373
410
|
inspected.state === "present" &&
|
|
374
|
-
matchesManagedEntry(
|
|
411
|
+
matchesManagedEntry(
|
|
412
|
+
inspected.entry,
|
|
413
|
+
previousProfile,
|
|
414
|
+
name,
|
|
415
|
+
inspected.entry?.transport?.args?.[0],
|
|
416
|
+
) &&
|
|
417
|
+
(inspected.entry.transport.args[0] === SERVER_ENTRY ||
|
|
418
|
+
new RegExp(
|
|
419
|
+
`^${runtimeRoot(environment)
|
|
420
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
421
|
+
.replaceAll(
|
|
422
|
+
"\\\\",
|
|
423
|
+
"[\\\\/]",
|
|
424
|
+
)}[\\\\/]runtime-packages[\\\\/]\\d+\\.\\d+\\.\\d+[\\\\/]node_modules[\\\\/]totemlayer[\\\\/]bin[\\\\/]totem\\.mjs$`,
|
|
425
|
+
).test(inspected.entry.transport.args[0]));
|
|
375
426
|
if (!ownsPrevious)
|
|
376
427
|
return {
|
|
377
428
|
name,
|
|
@@ -380,10 +431,15 @@ export async function installManagedTotemMcp(
|
|
|
380
431
|
};
|
|
381
432
|
// Codex add can update this exact named entry. No remove occurs: unsupported client
|
|
382
433
|
// versions fail safely, and an exact read-back is required before reporting an update.
|
|
383
|
-
const updated = await managedCall(
|
|
434
|
+
const updated = await managedCall(
|
|
435
|
+
profile,
|
|
436
|
+
managedArgs(profile, name, serverEntry),
|
|
437
|
+
safeEnvironment,
|
|
438
|
+
runtime,
|
|
439
|
+
);
|
|
384
440
|
if (updated.status === 0) {
|
|
385
441
|
const after = await inspectEntry(profile, safeEnvironment, runtime);
|
|
386
|
-
if (after.state === "present" && matchesManagedEntry(after.entry, profile, name))
|
|
442
|
+
if (after.state === "present" && matchesManagedEntry(after.entry, profile, name, serverEntry))
|
|
387
443
|
return {
|
|
388
444
|
name,
|
|
389
445
|
state: "updated_unverified",
|
package/bin/mcp.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import * as z from "zod/v4";
|
|
|
8
8
|
import { Totem } from "./sdk/index.js";
|
|
9
9
|
import { inspectLocalRepository, verifyLocalRepoClaim } from "./repository.mjs";
|
|
10
10
|
|
|
11
|
-
const VERSION = "0.1.
|
|
11
|
+
const VERSION = "0.1.6";
|
|
12
12
|
const MAX_KEY_BYTES = 8 * 1024;
|
|
13
13
|
const MAX_ATTACHMENT_BYTES = 2 * 1024 * 1024;
|
|
14
14
|
const collaborationSchema = z
|
|
@@ -148,6 +148,61 @@ export function createTotemMcpServer(config) {
|
|
|
148
148
|
() => client.agent(),
|
|
149
149
|
);
|
|
150
150
|
|
|
151
|
+
register(
|
|
152
|
+
server,
|
|
153
|
+
"totem_list_missions",
|
|
154
|
+
{
|
|
155
|
+
title: "List assigned missions",
|
|
156
|
+
description:
|
|
157
|
+
"Discover durable missions this authenticated agent can participate in. Offline agents must connect their own runtime to execute work.",
|
|
158
|
+
inputSchema: z.object({}),
|
|
159
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
160
|
+
},
|
|
161
|
+
() => client.missions.list(),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
register(
|
|
165
|
+
server,
|
|
166
|
+
"totem_get_mission",
|
|
167
|
+
{
|
|
168
|
+
title: "Inspect a mission",
|
|
169
|
+
description:
|
|
170
|
+
"Read the mission team, dependencies, owner-approved infrastructure scope, results, and receipts before delegating work.",
|
|
171
|
+
inputSchema: z.object({ missionId: z.uuid() }),
|
|
172
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
173
|
+
},
|
|
174
|
+
({ missionId }) => client.missions.get(missionId),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
register(
|
|
178
|
+
server,
|
|
179
|
+
"totem_delegate_mission_task",
|
|
180
|
+
{
|
|
181
|
+
title: "Delegate a mission task",
|
|
182
|
+
description:
|
|
183
|
+
"The mission coordinator assigns a child task to a consented member. Infrastructure deployment requires the selected executor and an approved review dependency; verification depends on a completed deployment. This does not grant permissions or wake an offline agent.",
|
|
184
|
+
inputSchema: z.object({
|
|
185
|
+
missionId: z.uuid(),
|
|
186
|
+
ownerMemberId: z.uuid(),
|
|
187
|
+
parentWorkItemId: z.uuid(),
|
|
188
|
+
dependsOnIds: z.array(z.uuid()).min(1).max(40),
|
|
189
|
+
kind: z.enum([
|
|
190
|
+
"engineering",
|
|
191
|
+
"code",
|
|
192
|
+
"review",
|
|
193
|
+
"infrastructure_deploy",
|
|
194
|
+
"infrastructure_verify",
|
|
195
|
+
]),
|
|
196
|
+
objective: z.string().min(1).max(10000),
|
|
197
|
+
requiredOutput: z.string().min(1).max(10000),
|
|
198
|
+
idempotencyKey: z.string().min(1).max(128),
|
|
199
|
+
}),
|
|
200
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
201
|
+
},
|
|
202
|
+
({ missionId, kind, idempotencyKey, ...input }) =>
|
|
203
|
+
client.missions.assign(missionId, { ...input, metadata: { kind } }, { idempotencyKey }),
|
|
204
|
+
);
|
|
205
|
+
|
|
151
206
|
if (localRepositoryTools)
|
|
152
207
|
register(
|
|
153
208
|
server,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createLocalStaticProvider } from "./mission-infrastructure.mjs";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
@@ -18,14 +19,34 @@ import {
|
|
|
18
19
|
import { repositoryIdFromRemote } from "./repository.mjs";
|
|
19
20
|
|
|
20
21
|
const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
|
|
21
|
-
const STAGES = new Set([
|
|
22
|
-
|
|
22
|
+
const STAGES = new Set([
|
|
23
|
+
"proposal",
|
|
24
|
+
"engineering",
|
|
25
|
+
"decision",
|
|
26
|
+
"code",
|
|
27
|
+
"review",
|
|
28
|
+
"handoff",
|
|
29
|
+
"infrastructure_deploy",
|
|
30
|
+
"infrastructure_verify",
|
|
31
|
+
]);
|
|
32
|
+
const REPOSITORY_STAGES = new Set([
|
|
33
|
+
"engineering",
|
|
34
|
+
"code",
|
|
35
|
+
"review",
|
|
36
|
+
"infrastructure_deploy",
|
|
37
|
+
"infrastructure_verify",
|
|
38
|
+
]);
|
|
23
39
|
const TERMINAL = new Set(["completed", "canceled", "failed"]);
|
|
24
40
|
const RECOVERABLE = new Set([
|
|
25
41
|
"ready",
|
|
26
42
|
"result_settling",
|
|
27
43
|
"local_test_failure_settling",
|
|
28
44
|
"claiming",
|
|
45
|
+
"infra_source",
|
|
46
|
+
"infra_source_dispatched",
|
|
47
|
+
"infra_source_settling",
|
|
48
|
+
"infra_reserved",
|
|
49
|
+
"infra_dispatched",
|
|
29
50
|
]);
|
|
30
51
|
const CHECKPOINT_BYTES = 2 * 1024 * 1024;
|
|
31
52
|
|
|
@@ -129,6 +150,7 @@ export async function runMissionAgentService(
|
|
|
129
150
|
nimUrl,
|
|
130
151
|
ollamaUrl,
|
|
131
152
|
reasoning = false,
|
|
153
|
+
adapterSettings = {},
|
|
132
154
|
stateDirectory = join(homedir(), ".totem", "missions"),
|
|
133
155
|
pollMs = 5000,
|
|
134
156
|
signal,
|
|
@@ -210,11 +232,21 @@ export async function runMissionAgentService(
|
|
|
210
232
|
};
|
|
211
233
|
const workerStateDirectory = join(resolve(stateDirectory), "mission-agents", session.key);
|
|
212
234
|
const directory = join(workerStateDirectory, agent.id);
|
|
235
|
+
let infrastructureProvider;
|
|
213
236
|
try {
|
|
214
237
|
await ensurePrivateRuntimeDirectory(resolve(stateDirectory));
|
|
215
238
|
await ensurePrivateRuntimeDirectory(join(resolve(stateDirectory), "mission-agents"));
|
|
216
239
|
await ensurePrivateRuntimeDirectory(workerStateDirectory);
|
|
217
240
|
await ensurePrivateRuntimeDirectory(directory);
|
|
241
|
+
const getInfrastructureProvider = async () => {
|
|
242
|
+
infrastructureProvider ??= await createLocalStaticProvider({
|
|
243
|
+
directory: join(workerStateDirectory, "local-static"),
|
|
244
|
+
});
|
|
245
|
+
return infrastructureProvider;
|
|
246
|
+
};
|
|
247
|
+
// Restore persisted releases on startup without repeating their deployment operations.
|
|
248
|
+
if ((await readdir(workerStateDirectory)).includes("local-static"))
|
|
249
|
+
await getInfrastructureProvider();
|
|
218
250
|
const checkpointPath = join(directory, `discovery-${session.key}.json`);
|
|
219
251
|
const state = await loadCheckpoint(checkpointPath, origin, agent.id);
|
|
220
252
|
const persist = () => saveCheckpoint(checkpointPath, state);
|
|
@@ -276,9 +308,14 @@ export async function runMissionAgentService(
|
|
|
276
308
|
nimUrl,
|
|
277
309
|
ollamaUrl,
|
|
278
310
|
reasoning,
|
|
311
|
+
adapterSettings,
|
|
279
312
|
signal,
|
|
280
313
|
candidateWorkItem: workItem,
|
|
281
314
|
recoveryOnly,
|
|
315
|
+
infrastructureProvider:
|
|
316
|
+
recoveryOnly || workItem?.metadata?.kind?.startsWith("infrastructure_")
|
|
317
|
+
? await getInfrastructureProvider()
|
|
318
|
+
: undefined,
|
|
282
319
|
emit(event) {
|
|
283
320
|
const type =
|
|
284
321
|
event.type === "work_started"
|
|
@@ -435,6 +472,7 @@ export async function runMissionAgentService(
|
|
|
435
472
|
}
|
|
436
473
|
}
|
|
437
474
|
} finally {
|
|
475
|
+
await infrastructureProvider?.close();
|
|
438
476
|
await session.release();
|
|
439
477
|
report({ type: "stopped" });
|
|
440
478
|
}
|