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 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.40.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.
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.40.tgz
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.40.tgz totem install
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.40.tgz totem login --device-login --no-browser
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
- process.exitCode = 1;
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.40";
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
- const child = spawn(process.execPath, [entry, ...process.argv.slice(2)], {
164
- env: { ...environment, TOTEM_CONFIG_DIR: join(root, '..'), TOTEM_RUNTIME_ENV_LOADED: entry },
165
- stdio: 'inherit',
166
- });
167
- for (const signal of ['SIGTERM', 'SIGINT']) process.once(signal, () => child.kill(signal));
168
- await new Promise((resolve, reject) => {
169
- child.once('error', reject);
170
- child.once('exit', (code) => { process.exitCode = code ?? 1; resolve(); });
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("<", "&lt;")
203
214
  .replaceAll(">", "&gt;")
204
215
  .replaceAll('"', "&quot;");
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
- const literal = (value) => `'${String(value).replaceAll("'", "''")}'`;
369
- const upgrade = runtime.upgraded
370
- ? `$old=Get-ScheduledTask -TaskName ${literal(task)} -ErrorAction SilentlyContinue;if($old -and $old.Actions.Count -eq 1 -and $old.Actions[0].Execute -eq ${literal(node)} -and $old.Actions[0].Arguments -eq ${literal('"' + runtime.supervisor + '"')}){Stop-ScheduledTask -TaskName ${literal(task)};}`
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 { mode: "background-supervisor", persistent: false };
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.40.tgz
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.40.tgz totem --help
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totemlayer",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "description": "Cross-platform CLI for TotemLayer agent identity and authorization",
5
5
  "type": "module",
6
6
  "bin": {