totemlayer 0.1.6 → 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/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,305 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { cp, mkdir, lstat, open, readFile, realpath, rename, rm } from "node:fs/promises";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { powershellInvocation } from "./platform-client.mjs";
|
|
10
|
+
|
|
11
|
+
const execute = promisify(execFile);
|
|
12
|
+
const packageRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
13
|
+
export function runtimeRoot(env = process.env) {
|
|
14
|
+
return resolve(env.TOTEM_CONFIG_DIR ?? join(homedir(), ".totem"));
|
|
15
|
+
}
|
|
16
|
+
export async function privateDirectory(path) {
|
|
17
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
18
|
+
const info = await lstat(path);
|
|
19
|
+
if (
|
|
20
|
+
!info.isDirectory() ||
|
|
21
|
+
info.isSymbolicLink() ||
|
|
22
|
+
(process.getuid && info.uid !== process.getuid()) ||
|
|
23
|
+
(process.platform !== "win32" && (info.mode & 0o777) !== 0o700)
|
|
24
|
+
)
|
|
25
|
+
throw new Error("The runtime directory must be private and owned by this user.");
|
|
26
|
+
}
|
|
27
|
+
export async function privateRead(path) {
|
|
28
|
+
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
29
|
+
try {
|
|
30
|
+
const info = await handle.stat();
|
|
31
|
+
if (
|
|
32
|
+
!info.isFile() ||
|
|
33
|
+
info.nlink !== 1 ||
|
|
34
|
+
info.size > 65536 ||
|
|
35
|
+
(process.getuid && info.uid !== process.getuid()) ||
|
|
36
|
+
(process.platform !== "win32" && (info.mode & 0o777) !== 0o600)
|
|
37
|
+
)
|
|
38
|
+
throw new Error("Unsafe runtime state.");
|
|
39
|
+
return JSON.parse(await handle.readFile("utf8"));
|
|
40
|
+
} finally {
|
|
41
|
+
await handle.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function privateWrite(path, value) {
|
|
45
|
+
await privateDirectory(dirname(path));
|
|
46
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
47
|
+
const handle = await open(
|
|
48
|
+
temporary,
|
|
49
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0),
|
|
50
|
+
0o600,
|
|
51
|
+
);
|
|
52
|
+
try {
|
|
53
|
+
await handle.writeFile(JSON.stringify(value));
|
|
54
|
+
await handle.sync();
|
|
55
|
+
} finally {
|
|
56
|
+
await handle.close();
|
|
57
|
+
}
|
|
58
|
+
await rename(temporary, path);
|
|
59
|
+
}
|
|
60
|
+
export async function ensureInstalledRuntime(env = process.env) {
|
|
61
|
+
const root = runtimeRoot(env);
|
|
62
|
+
await privateDirectory(root);
|
|
63
|
+
let previousVersion;
|
|
64
|
+
try {
|
|
65
|
+
previousVersion = (await privateRead(join(root, "runtime", "current.json"))).version;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error.code !== "ENOENT") throw error;
|
|
68
|
+
}
|
|
69
|
+
const metadata = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
|
|
70
|
+
if (!/^\d+\.\d+\.\d+$/.test(metadata.version)) throw new Error("Invalid runtime version.");
|
|
71
|
+
const runtime = join(root, "runtime");
|
|
72
|
+
const modules = join(root, "runtime-packages", metadata.version, "node_modules");
|
|
73
|
+
const destination = join(modules, "totemlayer");
|
|
74
|
+
await privateDirectory(runtime);
|
|
75
|
+
await privateDirectory(modules);
|
|
76
|
+
let exists = false;
|
|
77
|
+
try {
|
|
78
|
+
exists = (await lstat(join(destination, "bin", "automatic-runtime.mjs"))).isFile();
|
|
79
|
+
} catch {
|
|
80
|
+
/* First installation. */
|
|
81
|
+
}
|
|
82
|
+
if (!exists) {
|
|
83
|
+
const stage = join(modules, `totemlayer-stage-${process.pid}`);
|
|
84
|
+
await privateDirectory(stage);
|
|
85
|
+
for (const name of ["bin", "package.json", "LICENSE.md", "README.md"])
|
|
86
|
+
await cp(join(packageRoot, name), join(stage, name), { recursive: true, dereference: false });
|
|
87
|
+
const dependencies = [
|
|
88
|
+
{ name: "zod", from: packageRoot },
|
|
89
|
+
{ name: "@modelcontextprotocol/server", from: packageRoot },
|
|
90
|
+
];
|
|
91
|
+
const copied = new Map();
|
|
92
|
+
for (const dependency of dependencies) {
|
|
93
|
+
let selected;
|
|
94
|
+
for (
|
|
95
|
+
let current = dependency.from;
|
|
96
|
+
current !== dirname(current);
|
|
97
|
+
current = dirname(current)
|
|
98
|
+
) {
|
|
99
|
+
for (const candidate of [
|
|
100
|
+
join(current, "node_modules", dependency.name),
|
|
101
|
+
...(current.endsWith("node_modules") ? [join(current, dependency.name)] : []),
|
|
102
|
+
]) {
|
|
103
|
+
try {
|
|
104
|
+
selected = await realpath(candidate);
|
|
105
|
+
break;
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
if (selected) break;
|
|
109
|
+
}
|
|
110
|
+
if (!selected) throw new Error("Install Totem's runtime dependencies before starting it.");
|
|
111
|
+
const dependencyManifest = JSON.parse(await readFile(join(selected, "package.json"), "utf8"));
|
|
112
|
+
if (copied.has(dependency.name)) {
|
|
113
|
+
if (copied.get(dependency.name) !== dependencyManifest.version)
|
|
114
|
+
throw new Error("Conflicting runtime dependency versions.");
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
copied.set(dependency.name, dependencyManifest.version);
|
|
118
|
+
const target = join(modules, dependency.name);
|
|
119
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
120
|
+
await cp(selected, target, { recursive: true, dereference: false, errorOnExist: false });
|
|
121
|
+
for (const name of Object.keys(dependencyManifest.dependencies ?? {}))
|
|
122
|
+
dependencies.push({ name, from: selected });
|
|
123
|
+
}
|
|
124
|
+
// An earlier installation can already own this version directory; never delete it.
|
|
125
|
+
try {
|
|
126
|
+
await rename(stage, destination);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
await rm(stage, { recursive: true, force: true });
|
|
129
|
+
if (!(await lstat(join(destination, "bin", "automatic-runtime.mjs")).catch(() => null)))
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const cli = join(runtime, "totem.mjs");
|
|
134
|
+
const supervisor = join(runtime, "supervisor.mjs");
|
|
135
|
+
const launcher = (target) =>
|
|
136
|
+
`import{readFile,lstat}from'node:fs/promises';import{join,dirname}from'node:path';import{fileURLToPath,pathToFileURL}from'node:url';const root=dirname(fileURLToPath(import.meta.url));const file=join(root,'current.json');const info=await lstat(file);if(info.isSymbolicLink()||(process.getuid&&info.uid!==process.getuid())||(process.platform!=='win32'&&(info.mode&511)!==384))throw Error('Unsafe runtime installation');process.env.TOTEM_CONFIG_DIR=join(root,'..');const{version}=JSON.parse(await readFile(file,'utf8'));if(!/^\\d+\\.\\d+\\.\\d+$/.test(version))throw Error('Invalid runtime version');await import(pathToFileURL(join(root,'..','runtime-packages',version,'node_modules','totemlayer','bin','${target}')));`;
|
|
137
|
+
for (const [path, contents] of [
|
|
138
|
+
[cli, launcher("totem.mjs")],
|
|
139
|
+
[supervisor, launcher("automatic-runtime.mjs")],
|
|
140
|
+
]) {
|
|
141
|
+
const handle = await open(
|
|
142
|
+
path,
|
|
143
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | (constants.O_NOFOLLOW ?? 0),
|
|
144
|
+
0o600,
|
|
145
|
+
);
|
|
146
|
+
try {
|
|
147
|
+
await handle.writeFile(contents);
|
|
148
|
+
} finally {
|
|
149
|
+
await handle.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
await privateWrite(join(runtime, "current.json"), { version: metadata.version });
|
|
153
|
+
return {
|
|
154
|
+
root,
|
|
155
|
+
cli,
|
|
156
|
+
supervisor,
|
|
157
|
+
directory: runtime,
|
|
158
|
+
version: metadata.version,
|
|
159
|
+
upgraded: Boolean(previousVersion && previousVersion !== metadata.version),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const xml = (value) =>
|
|
163
|
+
String(value)
|
|
164
|
+
.replaceAll("&", "&")
|
|
165
|
+
.replaceAll("<", "<")
|
|
166
|
+
.replaceAll(">", ">")
|
|
167
|
+
.replaceAll('"', """);
|
|
168
|
+
export async function installRuntimeService(runtime, env = process.env, options = {}) {
|
|
169
|
+
const platform = options.platform ?? process.platform;
|
|
170
|
+
const run = options.execute ?? execute;
|
|
171
|
+
const node = process.execPath;
|
|
172
|
+
const home = env.HOME ?? env.USERPROFILE ?? homedir();
|
|
173
|
+
if (runtime.upgraded && platform !== "win32") {
|
|
174
|
+
try {
|
|
175
|
+
const status = await privateRead(join(runtime.directory, "supervisor.json"));
|
|
176
|
+
const owner = await privateRead(join(runtime.directory, "supervisor.lock", "owner.json"));
|
|
177
|
+
if (Number.isSafeInteger(status.pid) && status.pid > 1 && owner.pid === status.pid) {
|
|
178
|
+
const processInfo = await run("ps", ["-p", String(status.pid), "-o", "args="], {
|
|
179
|
+
encoding: "utf8",
|
|
180
|
+
timeout: 3000,
|
|
181
|
+
maxBuffer: 8192,
|
|
182
|
+
});
|
|
183
|
+
if (processInfo.stdout.trim() === `${node} ${runtime.supervisor}`) {
|
|
184
|
+
process.kill(status.pid, "SIGTERM");
|
|
185
|
+
const deadline = Date.now() + 20000;
|
|
186
|
+
while (Date.now() < deadline) {
|
|
187
|
+
try {
|
|
188
|
+
process.kill(status.pid, 0);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error.code === "ESRCH") break;
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
/* Only stop a verified previous supervisor; other processes are preserved. */
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const suffix =
|
|
203
|
+
runtime.root === resolve(join(homedir(), ".totem"))
|
|
204
|
+
? ""
|
|
205
|
+
: `.${createHash("sha256").update(runtime.root).digest("hex").slice(0, 12)}`;
|
|
206
|
+
const label = `com.totem.runtime${suffix}`;
|
|
207
|
+
const unit = `totem-runtime${suffix}.service`;
|
|
208
|
+
const task = `TotemRuntime${suffix}`;
|
|
209
|
+
const serviceEnv = { TOTEM_CONFIG_DIR: runtime.root, PATH: env.PATH ?? "", HOME: home };
|
|
210
|
+
for (const name of [
|
|
211
|
+
"TOTEM_PROVIDER",
|
|
212
|
+
"TOTEM_MODEL",
|
|
213
|
+
"TOTEM_BASE_URL",
|
|
214
|
+
"TOTEM_AGENT_ID",
|
|
215
|
+
"TOTEM_AGENT_URL",
|
|
216
|
+
"TOTEM_AGENT_TOKEN_ENV",
|
|
217
|
+
"TOTEM_AGENT_COMMAND",
|
|
218
|
+
"TOTEM_AGENT_COMMAND_ARGS",
|
|
219
|
+
])
|
|
220
|
+
if (env[name]) serviceEnv[name] = env[name];
|
|
221
|
+
await privateWrite(join(runtime.directory, "environment.json"), serviceEnv);
|
|
222
|
+
let providerSecrets = {};
|
|
223
|
+
try {
|
|
224
|
+
providerSecrets = await privateRead(join(runtime.directory, "provider-environment.json"));
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error.code !== "ENOENT") throw error;
|
|
227
|
+
}
|
|
228
|
+
const secretNames = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"];
|
|
229
|
+
if (
|
|
230
|
+
env.TOTEM_AGENT_TOKEN_ENV &&
|
|
231
|
+
/^[A-Z][A-Z0-9_]{0,127}$/.test(env.TOTEM_AGENT_TOKEN_ENV) &&
|
|
232
|
+
!/^(TOTEM|AGENTTRUST)_/.test(env.TOTEM_AGENT_TOKEN_ENV)
|
|
233
|
+
)
|
|
234
|
+
secretNames.push(env.TOTEM_AGENT_TOKEN_ENV);
|
|
235
|
+
for (const name of secretNames) if (env[name]) providerSecrets[name] = env[name];
|
|
236
|
+
await privateWrite(join(runtime.directory, "provider-environment.json"), providerSecrets);
|
|
237
|
+
try {
|
|
238
|
+
if (platform === "darwin") {
|
|
239
|
+
const path = join(home, "Library", "LaunchAgents", `${label}.plist`);
|
|
240
|
+
await mkdir(dirname(path), { recursive: true });
|
|
241
|
+
const plist = `<?xml version="1.0"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>${label}</string><key>ProgramArguments</key><array><string>${xml(node)}</string><string>${xml(runtime.supervisor)}</string></array><key>EnvironmentVariables</key><dict><key>TOTEM_CONFIG_DIR</key><string>${xml(runtime.root)}</string></dict><key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict><key>ThrottleInterval</key><integer>30</integer></dict></plist>`;
|
|
242
|
+
const handle = await open(
|
|
243
|
+
path,
|
|
244
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | (constants.O_NOFOLLOW ?? 0),
|
|
245
|
+
0o600,
|
|
246
|
+
);
|
|
247
|
+
try {
|
|
248
|
+
await handle.writeFile(plist);
|
|
249
|
+
} finally {
|
|
250
|
+
await handle.close();
|
|
251
|
+
}
|
|
252
|
+
const domain = `gui/${process.getuid()}`;
|
|
253
|
+
// Preserve an already running supervisor; its launcher uses current.json after restart.
|
|
254
|
+
try {
|
|
255
|
+
await run("launchctl", ["print", `${domain}/${label}`]);
|
|
256
|
+
if (runtime.upgraded) {
|
|
257
|
+
await run("launchctl", ["bootout", `${domain}/${label}`]);
|
|
258
|
+
await run("launchctl", ["bootstrap", domain, path]);
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
await run("launchctl", ["bootstrap", domain, path]);
|
|
262
|
+
}
|
|
263
|
+
return { mode: "launchd", persistent: true, label, path };
|
|
264
|
+
}
|
|
265
|
+
if (platform === "linux") {
|
|
266
|
+
const path = join(home, ".config", "systemd", "user", unit);
|
|
267
|
+
await mkdir(dirname(path), { recursive: true });
|
|
268
|
+
const quote = (value) =>
|
|
269
|
+
`"${String(value).replaceAll("%", "%%").replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
270
|
+
const text = `[Unit]\nDescription=Totem automatic agent runtime\nAfter=network-online.target\n[Service]\nExecStart=${quote(node)} ${quote(runtime.supervisor)}\nEnvironment=${quote(`TOTEM_CONFIG_DIR=${runtime.root}`)}\nRestart=on-failure\nRestartSec=30\n[Install]\nWantedBy=default.target\n`;
|
|
271
|
+
const handle = await open(
|
|
272
|
+
path,
|
|
273
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | (constants.O_NOFOLLOW ?? 0),
|
|
274
|
+
0o600,
|
|
275
|
+
);
|
|
276
|
+
try {
|
|
277
|
+
await handle.writeFile(text);
|
|
278
|
+
} finally {
|
|
279
|
+
await handle.close();
|
|
280
|
+
}
|
|
281
|
+
await run("systemctl", ["--user", "daemon-reload"]);
|
|
282
|
+
await run("systemctl", ["--user", "enable", "--now", unit]);
|
|
283
|
+
if (runtime.upgraded) await run("systemctl", ["--user", "restart", unit]);
|
|
284
|
+
return { mode: "systemd-user", persistent: true, label: unit, path };
|
|
285
|
+
}
|
|
286
|
+
if (platform === "win32") {
|
|
287
|
+
const literal = (value) => `'${String(value).replaceAll("'", "''")}'`;
|
|
288
|
+
const script = `$ErrorActionPreference='Stop';Stop-ScheduledTask -TaskName ${literal(task)} -ErrorAction SilentlyContinue;$action=New-ScheduledTaskAction -Execute ${literal(node)} -Argument ${literal('"' + runtime.supervisor + '"')};$trigger=New-ScheduledTaskTrigger -AtLogOn;$settings=New-ScheduledTaskSettingsSet -RestartCount 99 -RestartInterval (New-TimeSpan -Minutes 1);Register-ScheduledTask -TaskName ${literal(task)} -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null;Start-ScheduledTask -TaskName ${literal(task)}`;
|
|
289
|
+
const invocation = powershellInvocation(script, env);
|
|
290
|
+
await run(invocation.command, invocation.args);
|
|
291
|
+
return { mode: "scheduled-task", persistent: true, label: task };
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
/* Ephemeral/headless hosts often have no user service manager. */
|
|
295
|
+
}
|
|
296
|
+
const child = (options.spawn ?? spawn)(node, [runtime.supervisor], {
|
|
297
|
+
env: { ...env, TOTEM_CONFIG_DIR: runtime.root },
|
|
298
|
+
stdio: "ignore",
|
|
299
|
+
detached: true,
|
|
300
|
+
windowsHide: true,
|
|
301
|
+
});
|
|
302
|
+
child.on("error", () => {});
|
|
303
|
+
child.unref();
|
|
304
|
+
return { mode: "background-supervisor", persistent: false };
|
|
305
|
+
}
|
package/bin/sdk/index.js
CHANGED
|
@@ -164,6 +164,11 @@ export class Totem {
|
|
|
164
164
|
return { data: page.data.workItems, nextCursor: page.nextCursor ?? null };
|
|
165
165
|
},
|
|
166
166
|
iterateWorkItems: (id, query = {}, options) => paginate((cursor) => this.missions.workItemsPage(id, { ...query, cursor }, options), query.cursor),
|
|
167
|
+
assign: async (id, input, options = {}) => (await this.#transport.request("POST", `/missions/${encodeURIComponent(id)}/work-items`, {
|
|
168
|
+
...options,
|
|
169
|
+
body: input,
|
|
170
|
+
idempotencyKey: options.idempotencyKey ?? idempotencyKey(),
|
|
171
|
+
})).data,
|
|
167
172
|
claim: async (id, workItemId, options) => (await this.#transport.request("POST", `/missions/${encodeURIComponent(id)}/work-items/${encodeURIComponent(workItemId)}/claim`, { ...options, body: {} })).data,
|
|
168
173
|
renew: async (id, workItemId, leaseToken, options) => (await this.#transport.request("POST", `/missions/${encodeURIComponent(id)}/work-items/${encodeURIComponent(workItemId)}/renew`, { ...options, body: { leaseToken }, retrySafe: true })).data,
|
|
169
174
|
complete: async (id, workItemId, input, options = {}) => (await this.#transport.request("POST", `/missions/${encodeURIComponent(id)}/work-items/${encodeURIComponent(workItemId)}/complete`, { ...options, body: input, idempotencyKey: options.idempotencyKey ?? idempotencyKey() })).data,
|
package/bin/totem.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
4
4
|
import { createReadStream } from "node:fs";
|
|
5
5
|
import { stripVTControlCharacters } from "node:util";
|
|
6
6
|
|
|
7
|
-
const CLI_VERSION = "0.1.
|
|
7
|
+
const CLI_VERSION = "0.1.11";
|
|
8
8
|
const DEFAULT_BASE_URL = "https://totemlayer.com";
|
|
9
9
|
const API_KEY_PATTERN = /^at_(?:live|test)_[A-Za-z0-9_-]{20,}$/;
|
|
10
10
|
const ACTION_PATTERN = /^[a-z0-9][a-z0-9._:-]*$/;
|
|
@@ -241,8 +241,8 @@ function help() {
|
|
|
241
241
|
Confirm installation totem --version
|
|
242
242
|
Run one-off npx --yes totemlayer --help
|
|
243
243
|
|
|
244
|
-
Connect once and receive Mission work
|
|
245
|
-
|
|
244
|
+
Connect once and receive messages and approved Mission work
|
|
245
|
+
Select Codex, Claude, a local model, or an owner-configured CLI/HTTP agent.
|
|
246
246
|
totem setup
|
|
247
247
|
totem status
|
|
248
248
|
totem start --background
|
|
@@ -1748,6 +1748,11 @@ async function runRfq(options, baseUrl) {
|
|
|
1748
1748
|
}
|
|
1749
1749
|
|
|
1750
1750
|
async function main() {
|
|
1751
|
+
if (!process.argv[2] || ["login", "install"].includes(process.argv[2])) {
|
|
1752
|
+
const { installationMain } = await import("./installation.mjs");
|
|
1753
|
+
process.exitCode = await installationMain(process.argv.slice(3), {});
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1751
1756
|
if (["setup", "status", "doctor"].includes(process.argv[2])) {
|
|
1752
1757
|
const { agentSetupMain, agentStatusMain } = await import("./agent-setup.mjs");
|
|
1753
1758
|
const { readAgentRuntimeStatus } = await import("./agent-runner.mjs");
|
|
@@ -1786,6 +1791,11 @@ async function main() {
|
|
|
1786
1791
|
await mcpMain();
|
|
1787
1792
|
return;
|
|
1788
1793
|
}
|
|
1794
|
+
if (process.argv[2] === "cloud" && process.argv[3] === "setup") {
|
|
1795
|
+
const { cloudBootstrapMain } = await import("./cloud-bootstrap.mjs");
|
|
1796
|
+
process.exitCode = await cloudBootstrapMain(process.argv.slice(4));
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1789
1799
|
if (process.argv[2] === "network") {
|
|
1790
1800
|
const { networkMain } = await import("./network.mjs");
|
|
1791
1801
|
await networkMain(process.argv.slice(3));
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { WorkerError, validateResult } from "./errors.mjs";
|
|
4
|
+
|
|
5
|
+
const SYSTEM_ENV = [
|
|
6
|
+
"PATH",
|
|
7
|
+
"HOME",
|
|
8
|
+
"USER",
|
|
9
|
+
"LOGNAME",
|
|
10
|
+
"TMPDIR",
|
|
11
|
+
"TMP",
|
|
12
|
+
"TEMP",
|
|
13
|
+
"SystemRoot",
|
|
14
|
+
"SYSTEMROOT",
|
|
15
|
+
"COMSPEC",
|
|
16
|
+
"PATHEXT",
|
|
17
|
+
"LOCALAPPDATA",
|
|
18
|
+
"APPDATA",
|
|
19
|
+
];
|
|
20
|
+
const RESERVED_ENV = /^(TOTEM|AGENTTRUST)_/i;
|
|
21
|
+
|
|
22
|
+
function integer(value, fallback, maximum) {
|
|
23
|
+
const result = value ?? fallback;
|
|
24
|
+
if (!Number.isSafeInteger(result) || result < 1 || result > maximum) {
|
|
25
|
+
throw new WorkerError("INVALID_ADAPTER_CONFIG");
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function childEnvironment(values) {
|
|
31
|
+
const environment = {};
|
|
32
|
+
for (const key of SYSTEM_ENV) {
|
|
33
|
+
if (process.env[key] !== undefined) environment[key] = process.env[key];
|
|
34
|
+
}
|
|
35
|
+
for (const [key, value] of Object.entries(values ?? {})) {
|
|
36
|
+
if (RESERVED_ENV.test(key) || typeof value !== "string" || value.includes("\0")) {
|
|
37
|
+
throw new WorkerError("INVALID_ADAPTER_ENV");
|
|
38
|
+
}
|
|
39
|
+
environment[key] = value;
|
|
40
|
+
}
|
|
41
|
+
return environment;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** All process configuration comes from the owner's runtime, never a queued task. */
|
|
45
|
+
export function createCommandAdapter(config) {
|
|
46
|
+
if (
|
|
47
|
+
!config ||
|
|
48
|
+
typeof config.command !== "string" ||
|
|
49
|
+
!config.command.trim() ||
|
|
50
|
+
config.command.includes("\0") ||
|
|
51
|
+
!Array.isArray(config.args ?? []) ||
|
|
52
|
+
!(config.args ?? []).every((arg) => typeof arg === "string" && !arg.includes("\0")) ||
|
|
53
|
+
typeof config.cwd !== "string" ||
|
|
54
|
+
!isAbsolute(config.cwd)
|
|
55
|
+
) {
|
|
56
|
+
throw new WorkerError("INVALID_ADAPTER_CONFIG");
|
|
57
|
+
}
|
|
58
|
+
const args = [...(config.args ?? [])];
|
|
59
|
+
const env = childEnvironment(config.env);
|
|
60
|
+
const timeoutMs = integer(config.timeoutMs, 15 * 60_000, 24 * 60 * 60_000);
|
|
61
|
+
const maxOutputBytes = integer(config.maxOutputBytes, 1024 * 1024, 16 * 1024 * 1024);
|
|
62
|
+
const parseResult = config.parseResult ?? ((output) => ({ status: "completed", output }));
|
|
63
|
+
if (typeof parseResult !== "function") throw new WorkerError("INVALID_ADAPTER_CONFIG");
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
async run({ prompt, signal }) {
|
|
67
|
+
if (
|
|
68
|
+
typeof prompt !== "string" ||
|
|
69
|
+
!prompt.trim() ||
|
|
70
|
+
Buffer.byteLength(prompt) > maxOutputBytes
|
|
71
|
+
) {
|
|
72
|
+
throw new WorkerError("INVALID_PROMPT");
|
|
73
|
+
}
|
|
74
|
+
if (signal?.aborted) throw new WorkerError("RUN_CANCELLED");
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
let child;
|
|
77
|
+
let failure;
|
|
78
|
+
let output = "";
|
|
79
|
+
let bytes = 0;
|
|
80
|
+
let killTimer;
|
|
81
|
+
// Also remove surviving descendants if the parent exits before escalation.
|
|
82
|
+
const kill = (kind) => {
|
|
83
|
+
try {
|
|
84
|
+
if (process.platform !== "win32" && child?.pid) process.kill(-child.pid, kind);
|
|
85
|
+
else child?.kill(kind);
|
|
86
|
+
} catch {
|
|
87
|
+
/* The process may already have exited. */
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const stop = (code) => {
|
|
91
|
+
if (failure) return;
|
|
92
|
+
failure = new WorkerError(code);
|
|
93
|
+
// Kill the complete child process group on POSIX, including spawned tools.
|
|
94
|
+
kill("SIGTERM");
|
|
95
|
+
killTimer = setTimeout(() => kill("SIGKILL"), 1000);
|
|
96
|
+
killTimer.unref();
|
|
97
|
+
};
|
|
98
|
+
const cancel = () => stop("RUN_CANCELLED");
|
|
99
|
+
try {
|
|
100
|
+
child = spawn(config.command, args, {
|
|
101
|
+
cwd: config.cwd,
|
|
102
|
+
env,
|
|
103
|
+
shell: false,
|
|
104
|
+
detached: process.platform !== "win32",
|
|
105
|
+
windowsHide: true,
|
|
106
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
107
|
+
});
|
|
108
|
+
} catch {
|
|
109
|
+
reject(new WorkerError("AGENT_START_FAILED"));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const timeout = setTimeout(() => stop("RUN_TIMEOUT"), timeoutMs);
|
|
113
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
114
|
+
if (signal?.aborted) cancel();
|
|
115
|
+
child.stdout.setEncoding("utf8");
|
|
116
|
+
child.stdout.on("data", (chunk) => {
|
|
117
|
+
bytes += Buffer.byteLength(chunk);
|
|
118
|
+
if (bytes > maxOutputBytes) stop("OUTPUT_LIMIT_EXCEEDED");
|
|
119
|
+
else output += chunk;
|
|
120
|
+
});
|
|
121
|
+
// Drain stderr, but never expose provider diagnostics or credentials in logs.
|
|
122
|
+
child.stderr.on("data", (chunk) => {
|
|
123
|
+
bytes += chunk.length;
|
|
124
|
+
if (bytes > maxOutputBytes) stop("OUTPUT_LIMIT_EXCEEDED");
|
|
125
|
+
});
|
|
126
|
+
child.stdin.on("error", () => {
|
|
127
|
+
/* A failed child can close stdin early. */
|
|
128
|
+
});
|
|
129
|
+
child.on("error", () => {
|
|
130
|
+
failure ??= new WorkerError("AGENT_START_FAILED");
|
|
131
|
+
});
|
|
132
|
+
child.on("close", (code) => {
|
|
133
|
+
clearTimeout(timeout);
|
|
134
|
+
clearTimeout(killTimer);
|
|
135
|
+
signal?.removeEventListener("abort", cancel);
|
|
136
|
+
if (failure) {
|
|
137
|
+
kill("SIGKILL");
|
|
138
|
+
return reject(failure);
|
|
139
|
+
}
|
|
140
|
+
if (code !== 0) return reject(new WorkerError("AGENT_EXIT_FAILED"));
|
|
141
|
+
try {
|
|
142
|
+
resolve(validateResult(parseResult(output)));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
reject(error instanceof WorkerError ? error : new WorkerError("INVALID_AGENT_RESULT"));
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
// Prompts cannot become argv, executable names, paths, or shell syntax.
|
|
148
|
+
child.stdin.end(prompt);
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function createCodexAdapter(config) {
|
|
155
|
+
const { command = "codex", model, ...rest } = config;
|
|
156
|
+
if (model !== undefined && (typeof model !== "string" || !model || model.startsWith("-"))) {
|
|
157
|
+
throw new WorkerError("INVALID_ADAPTER_CONFIG");
|
|
158
|
+
}
|
|
159
|
+
return createCommandAdapter({
|
|
160
|
+
...rest,
|
|
161
|
+
command,
|
|
162
|
+
args: ["exec", "--sandbox", "workspace-write", ...(model ? ["--model", model] : []), "-"],
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function createClaudeCodeAdapter(config) {
|
|
167
|
+
const {
|
|
168
|
+
command = "claude",
|
|
169
|
+
model,
|
|
170
|
+
allowedTools = ["Read", "Glob", "Grep", "Edit", "Write"],
|
|
171
|
+
...rest
|
|
172
|
+
} = config;
|
|
173
|
+
if (
|
|
174
|
+
!Array.isArray(allowedTools) ||
|
|
175
|
+
!allowedTools.every((tool) => typeof tool === "string" && tool.trim()) ||
|
|
176
|
+
(model !== undefined && (typeof model !== "string" || !model || model.startsWith("-")))
|
|
177
|
+
) {
|
|
178
|
+
throw new WorkerError("INVALID_ADAPTER_CONFIG");
|
|
179
|
+
}
|
|
180
|
+
return createCommandAdapter({
|
|
181
|
+
...rest,
|
|
182
|
+
command,
|
|
183
|
+
args: [
|
|
184
|
+
"-p",
|
|
185
|
+
"--output-format",
|
|
186
|
+
"json",
|
|
187
|
+
"--permission-mode",
|
|
188
|
+
"dontAsk",
|
|
189
|
+
...(allowedTools.length ? ["--allowedTools", allowedTools.join(",")] : []),
|
|
190
|
+
...(model ? ["--model", model] : []),
|
|
191
|
+
],
|
|
192
|
+
parseResult(output) {
|
|
193
|
+
let result;
|
|
194
|
+
try {
|
|
195
|
+
result = JSON.parse(output);
|
|
196
|
+
} catch {
|
|
197
|
+
throw new WorkerError("INVALID_AGENT_RESULT");
|
|
198
|
+
}
|
|
199
|
+
if (result.is_error || result.subtype !== "success") {
|
|
200
|
+
throw new WorkerError("AGENT_REPORTED_FAILURE");
|
|
201
|
+
}
|
|
202
|
+
return { status: "completed", output: result.result };
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A hosted agent's owner-configured HTTP endpoint implementing the result contract. */
|
|
208
|
+
export function createHttpAdapter(config) {
|
|
209
|
+
let endpoint;
|
|
210
|
+
try {
|
|
211
|
+
endpoint = new URL(config.endpoint);
|
|
212
|
+
} catch {
|
|
213
|
+
throw new WorkerError("INVALID_ADAPTER_CONFIG");
|
|
214
|
+
}
|
|
215
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname);
|
|
216
|
+
if (
|
|
217
|
+
!["http:", "https:"].includes(endpoint.protocol) ||
|
|
218
|
+
(endpoint.protocol === "http:" && !local) ||
|
|
219
|
+
endpoint.username ||
|
|
220
|
+
endpoint.password ||
|
|
221
|
+
endpoint.search ||
|
|
222
|
+
endpoint.hash ||
|
|
223
|
+
(config.authorization !== undefined && typeof config.authorization !== "string")
|
|
224
|
+
)
|
|
225
|
+
throw new WorkerError("INVALID_ADAPTER_CONFIG");
|
|
226
|
+
const timeoutMs = integer(config.timeoutMs, 15 * 60_000, 24 * 60 * 60_000);
|
|
227
|
+
const maximum = integer(config.maxOutputBytes, 1024 * 1024, 16 * 1024 * 1024);
|
|
228
|
+
return {
|
|
229
|
+
async run({ prompt, signal, runId }) {
|
|
230
|
+
if (
|
|
231
|
+
typeof runId !== "string" ||
|
|
232
|
+
!/^[A-Za-z0-9_.:-]{1,200}$/.test(runId) ||
|
|
233
|
+
typeof prompt !== "string" ||
|
|
234
|
+
!prompt.trim() ||
|
|
235
|
+
Buffer.byteLength(prompt) > maximum
|
|
236
|
+
) {
|
|
237
|
+
throw new WorkerError("INVALID_PROMPT");
|
|
238
|
+
}
|
|
239
|
+
if (signal?.aborted) throw new WorkerError("RUN_CANCELLED");
|
|
240
|
+
let response;
|
|
241
|
+
try {
|
|
242
|
+
response = await fetch(endpoint, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
redirect: "manual",
|
|
245
|
+
headers: {
|
|
246
|
+
"content-type": "application/json",
|
|
247
|
+
"idempotency-key": runId,
|
|
248
|
+
...(config.authorization ? { authorization: config.authorization } : {}),
|
|
249
|
+
},
|
|
250
|
+
body: JSON.stringify({ runId, prompt }),
|
|
251
|
+
signal: signal
|
|
252
|
+
? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)])
|
|
253
|
+
: AbortSignal.timeout(timeoutMs),
|
|
254
|
+
});
|
|
255
|
+
} catch {
|
|
256
|
+
throw new WorkerError("UNKNOWN_HTTP_OUTCOME");
|
|
257
|
+
}
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
await response.body?.cancel();
|
|
260
|
+
throw new WorkerError("HTTP_AGENT_FAILED");
|
|
261
|
+
}
|
|
262
|
+
let size = 0;
|
|
263
|
+
const chunks = [];
|
|
264
|
+
try {
|
|
265
|
+
for await (const chunk of response.body) {
|
|
266
|
+
size += chunk.length;
|
|
267
|
+
if (size > maximum) {
|
|
268
|
+
await response.body.cancel().catch(() => {});
|
|
269
|
+
throw new WorkerError("OUTPUT_LIMIT_EXCEEDED");
|
|
270
|
+
}
|
|
271
|
+
chunks.push(chunk);
|
|
272
|
+
}
|
|
273
|
+
return validateResult(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw error instanceof WorkerError ? error : new WorkerError("INVALID_AGENT_RESULT");
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|