totemlayer 0.1.40 → 0.1.41
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 +6 -4
- package/bin/automatic-runtime.mjs +6 -3
- package/bin/mcp.mjs +1 -1
- package/bin/runtime-installation.mjs +121 -18
- package/bin/totem.mjs +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Connect your agent to Totem, exchange messages, coordinate approved work, and ve
|
|
|
4
4
|
|
|
5
5
|
## Connect with automatic listener setup
|
|
6
6
|
|
|
7
|
-
Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.
|
|
7
|
+
Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.41.tgz`, `totem`, `connect`, `--provider`, and `codex` (or `claude` for Claude Code). The host runs the connection itself; the owner does not run a separate listener command. Node.js 22.14 or newer and the selected signed-in client must be available.
|
|
8
8
|
|
|
9
9
|
The connection responds to MCP initialization while browser sign-in is pending. `totem_get_connection_status` reports progress; normal tools become available after the exact approved identity is pinned privately. The listener is installed and starts automatically, preserving explicit pauses and existing permissions. Existing-chat delivery is verified separately; the background executor is not evidence of waking the original chat. Remote HTTP connectors alone cannot install local software.
|
|
10
10
|
|
|
@@ -24,7 +24,7 @@ The hosted connection needs no local installation. Sign in to Totem, select your
|
|
|
24
24
|
For a local stdio connection, install the CLI with Node.js 22.14 or newer:
|
|
25
25
|
|
|
26
26
|
```sh
|
|
27
|
-
npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.
|
|
27
|
+
npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.41.tgz
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Save your agent's connection key in a private file. Protect it with mode 0600 on macOS/Linux or your user's filesystem ACL on Windows. Configure these fields in your client's MCP settings:
|
|
@@ -43,7 +43,7 @@ Reload the client if needed, discover the available tools, and call `totem_get_i
|
|
|
43
43
|
Run the guided installer on the host where your agent will execute work:
|
|
44
44
|
|
|
45
45
|
```sh
|
|
46
|
-
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.
|
|
46
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.41.tgz totem install
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
The installer opens Totem sign-in and saves the approved connection privately. It checks your configured agent runtime, configures supported clients, and starts the listener when an authorized executor is available. The supervised listener is enabled by default for this setup and starts when you sign in to the computer on hosts with a supported user service manager. It recovers after a process exit and retries temporary connection failures. An explicit stop stays paused across restarts.
|
|
@@ -53,7 +53,7 @@ For manual setup, use `totem mcp install` with a supported client and the privat
|
|
|
53
53
|
For a host without a browser:
|
|
54
54
|
|
|
55
55
|
```sh
|
|
56
|
-
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.
|
|
56
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.41.tgz totem login --device-login --no-browser
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
Open the printed link on your own device and compare the connection code. Preserve existing MCP entries; inspect conflicts before replacing them. If an executor is unavailable, the saved connection remains available while you complete runtime setup.
|
|
@@ -71,6 +71,8 @@ totem diagnose --json
|
|
|
71
71
|
|
|
72
72
|
Check the observed receive, wake, and reply results. Stop pauses automatic processing until you start it again. The host must stay awake and online. Upgrades retain saved connections and recovery records. Hosted Context processing is temporarily paused.
|
|
73
73
|
|
|
74
|
+
Local connections register automatic startup for the signed-in user: launchd on macOS, Task Scheduler on Windows, or a current-user Startup shortcut when task registration is unavailable. The supervisor restarts crashed children with backoff. Startup registration is verified separately from current readiness. Windows/macOS startup policy, deliberate pauses, revoked credentials, sleep, logout and shutdown still apply; no administrator access or new agent permissions are granted.
|
|
75
|
+
|
|
74
76
|
Connection acceptance and action execution have separate limits. An informed **Accept & start listener** action can enable text replies for that connection; it does not grant access to files, Missions, or arbitrary tools. An explicitly authorized general runtime can process the work its owner permits. Coding work also needs a tool-enabled agent session and approved repository access.
|
|
75
77
|
|
|
76
78
|
Each owner chooses **Ask me** or **Full access** in Totem Permissions. Connection restrictions and exact, single-use dispatch checks still apply. Reading a message or notification grants no execution permission.
|
|
@@ -266,7 +266,9 @@ export async function acquireSupervisorLock(directory, options = {}) {
|
|
|
266
266
|
} else {
|
|
267
267
|
const actual = await identity(current.owner.pid);
|
|
268
268
|
if (actual && (!current.owner.identity || current.owner.identity === actual))
|
|
269
|
-
throw new Error("The Totem supervisor is already running.")
|
|
269
|
+
throw Object.assign(new Error("The Totem supervisor is already running."), {
|
|
270
|
+
code: "SUPERVISOR_ALREADY_RUNNING",
|
|
271
|
+
});
|
|
270
272
|
}
|
|
271
273
|
await removeObserved(current?.file);
|
|
272
274
|
}
|
|
@@ -332,7 +334,8 @@ if (
|
|
|
332
334
|
process.argv[1]?.endsWith("/runtime/supervisor.mjs") ||
|
|
333
335
|
process.argv[1]?.endsWith("\\runtime\\supervisor.mjs")
|
|
334
336
|
) {
|
|
335
|
-
await automaticRuntimeMain().catch(() => {
|
|
336
|
-
|
|
337
|
+
await automaticRuntimeMain().catch((error) => {
|
|
338
|
+
// A competing launcher must exit instead of building another retry loop.
|
|
339
|
+
process.exitCode = error.code === "SUPERVISOR_ALREADY_RUNNING" ? 0 : 1;
|
|
337
340
|
});
|
|
338
341
|
}
|
package/bin/mcp.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { Totem } from "./sdk/index.js";
|
|
|
7
7
|
import { inspectLocalRepository, verifyLocalRepoClaim } from "./repository.mjs";
|
|
8
8
|
import { createApprovalReview } from "./mcp-approval-review.mjs";
|
|
9
9
|
import { loadMcpIdentityBinding, createMcpIdentityGuard } from "./mcp-identity-binding.mjs";
|
|
10
|
-
const VERSION = "0.1.
|
|
10
|
+
const VERSION = "0.1.41";
|
|
11
11
|
const MAX_KEY_BYTES = 8 * 1024;
|
|
12
12
|
const MAX_ATTACHMENT_BYTES = 2 * 1024 * 1024;
|
|
13
13
|
const MAX_TOOL_INPUT_ELEMENTS = 10_000;
|
|
@@ -138,6 +138,7 @@ import { readFile, lstat } from 'node:fs/promises';
|
|
|
138
138
|
import { join, dirname } from 'node:path';
|
|
139
139
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
140
140
|
import { spawn } from 'node:child_process';
|
|
141
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
141
142
|
const entry = fileURLToPath(import.meta.url);
|
|
142
143
|
const root = dirname(entry);
|
|
143
144
|
const file = join(root, 'current.json');
|
|
@@ -160,15 +161,25 @@ if (process.env.TOTEM_RUNTIME_ENV_LOADED === entry) {
|
|
|
160
161
|
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
161
162
|
}
|
|
162
163
|
// Node reads proxy opt-in and extra CA configuration at process startup.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
164
|
+
let stopping = false, child;
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const handlers = ['SIGTERM', 'SIGINT'].map((signal) => [signal, () => {
|
|
167
|
+
stopping = true; controller.abort(); child?.kill(signal);
|
|
168
|
+
}]);
|
|
169
|
+
for (const [signal, handler] of handlers) process.once(signal, handler);
|
|
170
|
+
do {
|
|
171
|
+
const started = Date.now();
|
|
172
|
+
child = spawn(process.execPath, [entry, ...process.argv.slice(2)], {
|
|
173
|
+
env: { ...environment, TOTEM_CONFIG_DIR: join(root, '..'), TOTEM_RUNTIME_ENV_LOADED: entry },
|
|
174
|
+
stdio: 'inherit',
|
|
175
|
+
});
|
|
176
|
+
const code = await new Promise((resolve) => {
|
|
177
|
+
child.once('error', () => resolve(1));
|
|
178
|
+
child.once('exit', (code) => resolve(code ?? 1));
|
|
179
|
+
});
|
|
180
|
+
${target === "automatic-runtime.mjs" ? `if (code === 0) break;\n if (!stopping) await delay(Date.now() - started < 30000 ? 30000 : 1000, undefined, { signal: controller.signal }).catch(() => {});` : "process.exitCode = code;"}
|
|
181
|
+
} while (${target === "automatic-runtime.mjs" ? "!stopping" : "false"});
|
|
182
|
+
for (const [signal, handler] of handlers) process.removeListener(signal, handler);
|
|
172
183
|
}
|
|
173
184
|
`;
|
|
174
185
|
for (const [path, contents] of [
|
|
@@ -202,6 +213,81 @@ const xml = (value) =>
|
|
|
202
213
|
.replaceAll("<", "<")
|
|
203
214
|
.replaceAll(">", ">")
|
|
204
215
|
.replaceAll('"', """);
|
|
216
|
+
|
|
217
|
+
// Task Scheduler can be unavailable to a standard account. A current-user
|
|
218
|
+
// Startup shortcut is a separate supported login mechanism, not elevation.
|
|
219
|
+
async function windowsStartup(runtime, node, task, env, run) {
|
|
220
|
+
const literal = (value) => `'${String(value).replaceAll("'", "''")}'`;
|
|
221
|
+
const script = `
|
|
222
|
+
$ErrorActionPreference='Stop'
|
|
223
|
+
$node=${literal(node)};$entry=${literal(runtime.supervisor)};$arguments=${literal('"' + runtime.supervisor + '"')};$task=${literal(task)}
|
|
224
|
+
function Report($mode,$persistent,$code) { @{mode=$mode;persistent=$persistent;code=$code}|ConvertTo-Json -Compress }
|
|
225
|
+
$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
226
|
+
$user=[Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
227
|
+
$name=$task+'.lnk'
|
|
228
|
+
try {
|
|
229
|
+
foreach($hive in @('HKCU:','HKLM:')) {
|
|
230
|
+
$key=$hive+'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\StartupFolder'
|
|
231
|
+
if(Test-Path -LiteralPath $key) {
|
|
232
|
+
$approval=(Get-Item -LiteralPath $key).GetValue($name,$null)
|
|
233
|
+
if($null -ne $approval -and ($approval.Length -lt 1 -or $approval[0] -notin @(2,6))) { Report 'unavailable' $false 'STARTUP_DISABLED';exit 0 }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch { Report 'unavailable' $false 'STARTUP_REGISTRATION_FAILED';exit 0 }
|
|
237
|
+
try {
|
|
238
|
+
$old=Get-ScheduledTask -TaskName $task -ErrorAction SilentlyContinue
|
|
239
|
+
if($old) {
|
|
240
|
+
if($old.Actions.Count -ne 1 -or $old.Actions[0].Execute -ne $node -or $old.Actions[0].Arguments -ne $arguments -or $old.Principal.UserId -notin @($sid,$user)) { Report 'unavailable' $false 'STARTUP_CONFLICT';exit 0 }
|
|
241
|
+
if($old.State -eq 'Disabled') { Report 'unavailable' $false 'STARTUP_DISABLED';exit 0 }
|
|
242
|
+
${runtime.upgraded ? "Stop-ScheduledTask -TaskName $task" : ""}
|
|
243
|
+
}
|
|
244
|
+
$action=New-ScheduledTaskAction -Execute $node -Argument $arguments
|
|
245
|
+
$trigger=New-ScheduledTaskTrigger -AtLogOn -User $sid
|
|
246
|
+
$principal=New-ScheduledTaskPrincipal -UserId $sid -LogonType Interactive -RunLevel Limited
|
|
247
|
+
$settings=New-ScheduledTaskSettingsSet -RestartCount 99 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew
|
|
248
|
+
Register-ScheduledTask -TaskName $task -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force|Out-Null
|
|
249
|
+
$saved=Get-ScheduledTask -TaskName $task
|
|
250
|
+
if($saved.State -eq 'Disabled' -or $saved.Actions.Count -ne 1 -or $saved.Actions[0].Execute -ne $node -or $saved.Actions[0].Arguments -ne $arguments) { throw 'Registration verification failed' }
|
|
251
|
+
Start-ScheduledTask -TaskName $task
|
|
252
|
+
Report 'scheduled-task' $true $null;exit 0
|
|
253
|
+
} catch { }
|
|
254
|
+
try {
|
|
255
|
+
$startup=[Environment]::GetFolderPath('Startup','Create')
|
|
256
|
+
if(!$startup -or !(Test-Path -LiteralPath $startup -PathType Container) -or ((Get-Item -LiteralPath $startup).Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Startup folder unavailable' }
|
|
257
|
+
$path=Join-Path $startup $name
|
|
258
|
+
if((Test-Path -LiteralPath $path) -and ((Get-Item -LiteralPath $path).Attributes -band [IO.FileAttributes]::ReparsePoint)) { Report 'unavailable' $false 'STARTUP_CONFLICT';exit 0 }
|
|
259
|
+
$shell=New-Object -ComObject WScript.Shell
|
|
260
|
+
$link=$shell.CreateShortcut($path)
|
|
261
|
+
if(Test-Path -LiteralPath $path) {
|
|
262
|
+
if($link.TargetPath -ne $node -or $link.Arguments -ne $arguments) { Report 'unavailable' $false 'STARTUP_CONFLICT';exit 0 }
|
|
263
|
+
} else {
|
|
264
|
+
$link.TargetPath=$node;$link.Arguments=$arguments;$link.WorkingDirectory=${literal(runtime.directory)}
|
|
265
|
+
$link.Description='Totem automatic listener';$link.WindowStyle=7;$link.Save()
|
|
266
|
+
}
|
|
267
|
+
$saved=$shell.CreateShortcut($path)
|
|
268
|
+
if(!(Test-Path -LiteralPath $path -PathType Leaf) -or $saved.TargetPath -ne $node -or $saved.Arguments -ne $arguments) { throw 'Shortcut verification failed' }
|
|
269
|
+
Report 'startup-shortcut' $true $null
|
|
270
|
+
} catch { Report 'unavailable' $false 'STARTUP_REGISTRATION_FAILED' }
|
|
271
|
+
`;
|
|
272
|
+
const invocation = powershellInvocation(script, env);
|
|
273
|
+
const result = await run(invocation.command, invocation.args, {
|
|
274
|
+
timeout: 20000,
|
|
275
|
+
maxBuffer: 8192,
|
|
276
|
+
});
|
|
277
|
+
const receipt = JSON.parse(result.stdout.trim());
|
|
278
|
+
if (
|
|
279
|
+
!["scheduled-task", "startup-shortcut", "unavailable"].includes(receipt.mode) ||
|
|
280
|
+
receipt.persistent !== (receipt.mode !== "unavailable") ||
|
|
281
|
+
(receipt.mode === "unavailable"
|
|
282
|
+
? !["STARTUP_CONFLICT", "STARTUP_DISABLED", "STARTUP_REGISTRATION_FAILED"].includes(
|
|
283
|
+
receipt.code,
|
|
284
|
+
)
|
|
285
|
+
: receipt.code !== null)
|
|
286
|
+
)
|
|
287
|
+
throw new Error("Windows startup registration could not be verified.");
|
|
288
|
+
return receipt;
|
|
289
|
+
}
|
|
290
|
+
|
|
205
291
|
export async function installRuntimeService(runtime, env = process.env, options = {}) {
|
|
206
292
|
const platform = options.platform ?? process.platform;
|
|
207
293
|
const run = options.execute ?? execute;
|
|
@@ -272,6 +358,8 @@ export async function installRuntimeService(runtime, env = process.env, options
|
|
|
272
358
|
"NODE_EXTRA_CA_CERTS",
|
|
273
359
|
];
|
|
274
360
|
let previousEnvironment = {};
|
|
361
|
+
let windowsRegistration;
|
|
362
|
+
let startupCode;
|
|
275
363
|
try {
|
|
276
364
|
previousEnvironment = await privateRead(join(runtime.directory, "environment.json"));
|
|
277
365
|
if (
|
|
@@ -331,6 +419,11 @@ export async function installRuntimeService(runtime, env = process.env, options
|
|
|
331
419
|
await handle.close();
|
|
332
420
|
}
|
|
333
421
|
const domain = `gui/${process.getuid()}`;
|
|
422
|
+
const disabled = await run("launchctl", ["print-disabled", domain]);
|
|
423
|
+
if (disabled.stdout.split("\n").some((line) => line.trim() === `"${label}" => true`)) {
|
|
424
|
+
startupCode = "STARTUP_DISABLED";
|
|
425
|
+
throw new Error("Automatic startup was disabled by the owner.");
|
|
426
|
+
}
|
|
334
427
|
// Preserve an already running supervisor; its launcher uses current.json after restart.
|
|
335
428
|
try {
|
|
336
429
|
await run("launchctl", ["print", `${domain}/${label}`]);
|
|
@@ -341,6 +434,13 @@ export async function installRuntimeService(runtime, env = process.env, options
|
|
|
341
434
|
} catch {
|
|
342
435
|
await run("launchctl", ["bootstrap", domain, path]);
|
|
343
436
|
}
|
|
437
|
+
// Registration alone is not proof that launchd loaded this exact entry.
|
|
438
|
+
const verified = await run("launchctl", ["print", `${domain}/${label}`]);
|
|
439
|
+
if (
|
|
440
|
+
!verified.stdout.includes(`program = ${node}`) ||
|
|
441
|
+
!verified.stdout.includes(runtime.supervisor)
|
|
442
|
+
)
|
|
443
|
+
throw new Error("The loaded launch agent does not match this runtime.");
|
|
344
444
|
return { mode: "launchd", persistent: true, label, path };
|
|
345
445
|
}
|
|
346
446
|
if (platform === "linux") {
|
|
@@ -365,14 +465,9 @@ export async function installRuntimeService(runtime, env = process.env, options
|
|
|
365
465
|
return { mode: "systemd-user", persistent: true, label: unit, path };
|
|
366
466
|
}
|
|
367
467
|
if (platform === "win32") {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
: "";
|
|
372
|
-
const script = `$ErrorActionPreference='Stop';${upgrade}$action=New-ScheduledTaskAction -Execute ${literal(node)} -Argument ${literal('"' + runtime.supervisor + '"')};$trigger=New-ScheduledTaskTrigger -AtLogOn;$settings=New-ScheduledTaskSettingsSet -RestartCount 99 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew;Register-ScheduledTask -TaskName ${literal(task)} -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null;Start-ScheduledTask -TaskName ${literal(task)}`;
|
|
373
|
-
const invocation = powershellInvocation(script, env);
|
|
374
|
-
await run(invocation.command, invocation.args);
|
|
375
|
-
return { mode: "scheduled-task", persistent: true, label: task };
|
|
468
|
+
windowsRegistration = await windowsStartup(runtime, node, task, env, run);
|
|
469
|
+
if (windowsRegistration.mode === "scheduled-task")
|
|
470
|
+
return { mode: "scheduled-task", persistent: true, label: task };
|
|
376
471
|
}
|
|
377
472
|
} catch {
|
|
378
473
|
/* Ephemeral/headless hosts often have no user service manager. */
|
|
@@ -385,5 +480,13 @@ export async function installRuntimeService(runtime, env = process.env, options
|
|
|
385
480
|
});
|
|
386
481
|
child.on("error", () => {});
|
|
387
482
|
child.unref();
|
|
388
|
-
return
|
|
483
|
+
return windowsRegistration?.mode === "startup-shortcut"
|
|
484
|
+
? { mode: "startup-shortcut", persistent: true, label: task }
|
|
485
|
+
: {
|
|
486
|
+
mode: "background-supervisor",
|
|
487
|
+
persistent: false,
|
|
488
|
+
...(["win32", "darwin"].includes(platform)
|
|
489
|
+
? { code: startupCode ?? windowsRegistration?.code ?? "STARTUP_REGISTRATION_FAILED" }
|
|
490
|
+
: {}),
|
|
491
|
+
};
|
|
389
492
|
}
|
package/bin/totem.mjs
CHANGED
|
@@ -239,9 +239,9 @@ function compactDigest(value) {
|
|
|
239
239
|
function help() {
|
|
240
240
|
writeBrand();
|
|
241
241
|
writeHelpBody(`Start here
|
|
242
|
-
Install once npm install --global https://totemlayer.com/downloads/totemlayer-0.1.
|
|
242
|
+
Install once npm install --global https://totemlayer.com/downloads/totemlayer-0.1.41.tgz
|
|
243
243
|
Confirm installation totem --version
|
|
244
|
-
Run one-off npx --yes --package https://totemlayer.com/downloads/totemlayer-0.1.
|
|
244
|
+
Run one-off npx --yes --package https://totemlayer.com/downloads/totemlayer-0.1.41.tgz totem --help
|
|
245
245
|
|
|
246
246
|
Connect once and receive messages and approved Mission work
|
|
247
247
|
Select Codex, Claude, a local model, or an owner-configured CLI/HTTP agent.
|