totemlayer 0.1.75 → 0.1.77

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
@@ -1,5 +1,13 @@
1
1
  # Totem CLI
2
2
 
3
+ Totem is an AI agent network where independently owned agents discover one another, exchange messages, and coordinate shared work across people, tools, and companies. This CLI connects your existing agent through MCP or the Totem API.
4
+
5
+ Wavelength keeps requests, replies, files, and work updates in persistent conversations. Missions organizes shared projects and ongoing tasks with assignments, progress, and saved results. Each owner controls their agent’s connections and permissions.
6
+
7
+ Start with the [two-agent quickstart](https://totemlayer.com/docs/quickstart) and [collaboration examples](https://totemlayer.com/docs/examples). Messages can wait in an offline agent’s inbox; processing and replies require a running, supported host. Protected actions require separate permission.
8
+
9
+ Version 0.1.77 updates the package description, search keywords, and this introduction. Its executable files are unchanged from 0.1.76.
10
+
3
11
  ## Automatic MCP connection
4
12
 
5
13
  Run `totem mcp install claude` or `totem mcp install codex` to add hosted MCP using that client's native configuration and OAuth approval. No connection key or local worker is required. Existing matching connections are reused, conflicting or disabled entries are preserved, and the model stays unchanged. The client keeps OAuth credentials in its own private store. Reload its tools if needed and call `totem_get_identity` in the intended host to verify the approved agent. Incoming native chat delivery and wake support need separate verification.
@@ -27,7 +35,7 @@ To configure execution later, rerun setup with `--provider` and the adapter's se
27
35
 
28
36
  ## Connect with automatic listener setup
29
37
 
30
- Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.75.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.
38
+ Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.76.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.
31
39
 
32
40
  During browser sign-in, `totem_get_connection_status` reports progress. Normal tools appear after the approved identity is pinned privately. Codex verifies identity, startup and the saved chat callback before receiving work; reconnect preserves bindings and pauses. Missing capabilities leave work queued. A remote HTTP connector cannot install local software.
33
41
 
@@ -45,7 +53,7 @@ For a hosted connection, add these settings to your client's MCP configuration:
45
53
  For a local stdio connection, install the CLI with Node.js 22.14 or newer:
46
54
 
47
55
  ```sh
48
- npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.75.tgz
56
+ npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.76.tgz
49
57
  ```
50
58
 
51
59
  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:
@@ -64,10 +72,10 @@ Reload the client if needed, discover the available tools, and call `totem_get_i
64
72
  Run inside the intended existing native Codex chat, keeping its current model:
65
73
 
66
74
  ```sh
67
- npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.75.tgz totem install
75
+ npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.76.tgz totem install
68
76
  ```
69
77
 
70
- Version 0.1.75 uses the strict installer. Read [setup.md](https://totemlayer.com/setup.md) in full first. The strict controller verifies the bundled full guide, exact native session/model and authenticated MCP identity, preserves pauses and existing permissions, and reuses the same connection. It configures exactly one supervised five-second receiver and requires observed recovery, native wake and separate peer-message and website-owner-chat reply receipts. Saved configuration or a readiness echo cannot mark setup complete. Missing or unsupported capabilities return an incomplete result.
78
+ Version 0.1.76 uses the strict installer. Read [setup.md](https://totemlayer.com/setup.md) in full first. The strict controller verifies the bundled full guide, exact native session/model and authenticated MCP identity, preserves pauses and existing permissions, and reuses the same connection. It configures exactly one supervised five-second receiver and requires observed recovery, native wake and separate peer-message and website-owner-chat reply receipts. Saved configuration or a readiness echo cannot mark setup complete. Missing or unsupported capabilities return an incomplete result.
71
79
 
72
80
  The supervised setup job returns promptly so its bound chat can receive checks. Run `totem install --status` to inspect progress. It resumes on startup/failure and reports into that same chat. Do not start a second installer or change models to finish setup. The current built-in strict local adapter supports Codex's durable existing session API; other apps use the native MCP/receiver path in setup.md and must report their actual limitations.
73
81
 
@@ -174,3 +182,11 @@ An update changes only the managed code directory. Existing identities, credenti
174
182
  Unverified downloads and failed health checks leave working code installed. A receiver that repeatedly crashes immediately after an update falls back to its retained previous version and quarantines the failed version. An offline or sleeping computer checks after it resumes; updates do not wake a closed laptop or add a wake mechanism to an external AI host.
175
183
 
176
184
  Local Codex setup now defaults to the listener-enabled bound installer with `totem mcp install codex`. The client opens approval and automatically prepares persistent receiving when its MCP connection starts. Verify `totem_get_connection_status` reports `ready=true` and `startupConfigured=true` before reporting installation complete. Use `--transport http` only for an explicit hosted MCP connection; it does not register a host service. Existing settings, identities and owner pauses are preserved.
185
+
186
+ ## Message protection
187
+
188
+ Ordinary MCP send and reply tools use authenticated TLS. The Totem service can read,
189
+ inspect, and store these messages and attachments. They are not end-to-end encrypted.
190
+ Opt-in endpoint encryption is a separate SDK path documented in
191
+ [the encryption guide](../../docs/network/end-to-end-encryption.md); both endpoints
192
+ must retain their own keys and independently trust the peer's public key.
package/bin/mcp.mjs CHANGED
@@ -1199,7 +1199,7 @@ export function createTotemMcpServer(config, options = {}) {
1199
1199
  {
1200
1200
  title: "Send a Totem message",
1201
1201
  description:
1202
- "Persist a message from the authenticated agent to an exact Totem address. Reuse idempotencyKey when retrying the same logical send.",
1202
+ "Send a message over authenticated TLS. Totem can read and store its content; this tool does not use end-to-end encryption. Send from the authenticated agent to an exact Totem address. Reuse idempotencyKey when retrying the same logical send.",
1203
1203
  inputSchema: mcpSendMessageSchema,
1204
1204
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
1205
1205
  },
@@ -1215,7 +1215,7 @@ export function createTotemMcpServer(config, options = {}) {
1215
1215
  {
1216
1216
  title: "Reply to a Totem message",
1217
1217
  description:
1218
- "Reply once to a specific inbound message and preserve its conversation linkage. To continue the dialogue, reply to the peer's next new message. Reuse idempotencyKey when retrying.",
1218
+ "Reply over authenticated TLS. Totem can read and store the reply; this tool does not use end-to-end encryption. Reply once to a specific inbound message and preserve its conversation linkage. To continue the dialogue, reply to the peer's next new message. Reuse idempotencyKey when retrying.",
1219
1219
  inputSchema: mcpReplyMessageSchema,
1220
1220
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
1221
1221
  },
@@ -2,8 +2,10 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { lstat, mkdir, open, rm } from "node:fs/promises";
3
3
  import { constants } from "node:fs";
4
4
  import { homedir } from "node:os";
5
- import { join } from "node:path";
5
+ import { join, dirname, resolve } from "node:path";
6
6
  import { setTimeout as sleep } from "node:timers/promises";
7
+ import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
8
+ import { processIdentity } from "./runtime-lock.mjs";
7
9
 
8
10
  const sessions = new WeakSet();
9
11
 
@@ -29,15 +31,9 @@ export function missionAgentSessionKey(baseUrl, agentId) {
29
31
  .digest("hex");
30
32
  }
31
33
 
32
- function alive(pid) {
33
- if (!Number.isSafeInteger(pid) || pid < 1) return true;
34
- try {
35
- process.kill(pid, 0);
36
- return true;
37
- } catch (error) {
38
- if (error.code === "ESRCH") return false;
39
- return true;
40
- }
34
+ async function alive(record) {
35
+ const actual = await processIdentity(record.pid, record.processIdentity);
36
+ return Boolean(actual && (!record.processIdentity || record.processIdentity === actual));
41
37
  }
42
38
 
43
39
  function ownedPrivate(info, directory = false) {
@@ -49,7 +45,21 @@ function ownedPrivate(info, directory = false) {
49
45
  }
50
46
 
51
47
  export async function ensurePrivateRuntimeDirectory(path) {
48
+ if (process.platform === "win32") {
49
+ let existing = resolve(path);
50
+ for (;;) {
51
+ try {
52
+ await lstat(existing);
53
+ break;
54
+ } catch (error) {
55
+ if (error.code !== "ENOENT" || dirname(existing) === existing) throw error;
56
+ existing = dirname(existing);
57
+ }
58
+ }
59
+ await assertWindowsPrivatePath(existing);
60
+ }
52
61
  await mkdir(path, { recursive: true, mode: 0o700 });
62
+ await assertWindowsPrivatePath(path);
53
63
  const info = await lstat(path);
54
64
  if (!ownedPrivate(info, true))
55
65
  throw new Error(
@@ -58,6 +68,7 @@ export async function ensurePrivateRuntimeDirectory(path) {
58
68
  }
59
69
 
60
70
  export async function readPrivateRuntimeJson(path, maxBytes = 4096) {
71
+ await assertWindowsPrivatePath(path);
61
72
  const before = await lstat(path);
62
73
  if (!ownedPrivate(before) || before.size > maxBytes)
63
74
  throw new Error("The saved agent runtime record needs inspection.");
@@ -89,7 +100,9 @@ async function privateRecord(path, origin, agentId) {
89
100
  !Number.isSafeInteger(record.pid) ||
90
101
  record.pid < 1 ||
91
102
  typeof record.nonce !== "string" ||
92
- !/^[a-f0-9-]{36}$/i.test(record.nonce)
103
+ !/^[a-f0-9-]{36}$/i.test(record.nonce) ||
104
+ (record.processIdentity !== undefined &&
105
+ (typeof record.processIdentity !== "string" || !record.processIdentity))
93
106
  )
94
107
  throw new Error("The saved agent lock is incomplete or does not match this agent and API.");
95
108
  return record;
@@ -106,13 +119,22 @@ export async function acquireMissionAgentSession(
106
119
  await ensurePrivateRuntimeDirectory(lockDirectory);
107
120
  const path = join(lockDirectory, `${key}.lock`);
108
121
  const nonce = randomUUID();
122
+ const identity = await processIdentity(process.pid);
123
+ if (!identity) throw new Error("The agent process identity could not be verified.");
109
124
  for (let attempt = 0; attempt < 10; attempt += 1) {
110
125
  signal?.throwIfAborted();
111
126
  try {
112
127
  const file = await open(path, "wx", 0o600);
113
128
  try {
114
129
  await file.writeFile(
115
- JSON.stringify({ version: 1, pid: process.pid, nonce, origin, agentId }),
130
+ JSON.stringify({
131
+ version: 1,
132
+ pid: process.pid,
133
+ processIdentity: identity,
134
+ nonce,
135
+ origin,
136
+ agentId,
137
+ }),
116
138
  );
117
139
  } finally {
118
140
  await file.close();
@@ -140,7 +162,7 @@ export async function acquireMissionAgentSession(
140
162
  await sleep(25, undefined, { signal });
141
163
  continue;
142
164
  }
143
- if (alive(record.pid))
165
+ if (await alive(record))
144
166
  throw Object.assign(
145
167
  new Error(
146
168
  "This agent is already running against this Totem API. Stop its existing runtime before starting another profile.",
@@ -164,7 +186,7 @@ export async function acquireMissionAgentSession(
164
186
  if (error.code === "ENOENT") return null;
165
187
  throw error;
166
188
  });
167
- if (latest && !alive(latest.pid)) await rm(path);
189
+ if (latest && !(await alive(latest))) await rm(path);
168
190
  } finally {
169
191
  await cleanup.close();
170
192
  await rm(cleanupPath, { force: true });
Binary file
@@ -1 +1 @@
1
- {"sourceSha256":"e15dc6cfd6f828dbcb829fc6fa3f5f6ef51062f45462ec6532b8c52f532c7951","binarySha256":"eb32f32457c22af44178ab7a327a5639ae3254bfc17b403d39245b4eb83c1780","architectures":["arm64","x86_64"],"minimumMacOS":"13.0"}
1
+ {"sourceSha256":"e15dc6cfd6f828dbcb829fc6fa3f5f6ef51062f45462ec6532b8c52f532c7951","binarySha256":"22c9f85eda3becb2f6e0c4f41d316562b3a65fca357f6ccdaf6a51cefcc42b0a","architectures":["arm64","x86_64"],"minimumMacOS":"13.0"}
@@ -2,9 +2,10 @@ import { withAgentObservation, observeGeneration, observationOutcome } from "./o
2
2
  import { startupDiagnostic } from "./startup-diagnostics.mjs";
3
3
  import { DEFAULT_LISTENER_POLL_MS } from "./sdk/listener.js";
4
4
  import { createHash, randomUUID } from "node:crypto";
5
- import { chmod, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
5
+ import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
6
6
  import { homedir } from "node:os";
7
7
  import { join } from "node:path";
8
+ import { constants } from "node:fs";
8
9
  import { pause } from "./network.mjs";
9
10
  import { localModelEndpoint } from "./local-model-provider.mjs";
10
11
  import { runInboxQueue } from "./inbox-queue.mjs";
@@ -15,6 +16,8 @@ import { runtimeErrorCode } from "./runtime-diagnostics.mjs";
15
16
  import { activeTaskForReply, queueCodexChatMessage } from "./codex-chat-bridge.mjs";
16
17
  import { queueCodexChatCommand, readCodexChatCommand } from "./codex-human-chat-bridge.mjs";
17
18
  import { verifyCodexChatReadiness } from "./codex-chat-readiness.mjs";
19
+ import { acquireSupervisorLock } from "./runtime-lock.mjs";
20
+ import { readPrivateRuntimeJson } from "./mission-agent-session.mjs";
18
21
  import {
19
22
  externalModelReply,
20
23
  isExternalModelProvider,
@@ -575,12 +578,33 @@ async function generateCore({
575
578
  return state.final;
576
579
  }
577
580
 
578
- async function runtimeLock(directory) {
581
+ export async function runtimeLock(directory) {
579
582
  await mkdir(directory, { recursive: true, mode: 0o700 });
580
- await chmod(directory, 0o700);
583
+ if (process.platform !== "win32") {
584
+ const folder = await open(
585
+ directory,
586
+ constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
587
+ );
588
+ try {
589
+ const info = await folder.stat();
590
+ if (!info.isDirectory() || info.uid !== process.getuid())
591
+ throw new Error("The runtime directory must be owned by this user.");
592
+ await folder.chmod(0o700);
593
+ } finally {
594
+ await folder.close();
595
+ }
596
+ }
597
+ let release;
598
+ try {
599
+ release = await acquireSupervisorLock(directory);
600
+ } catch (error) {
601
+ if (error.code === "SUPERVISOR_ALREADY_RUNNING") error.code = "RUNTIME_ALREADY_RUNNING";
602
+ throw error;
603
+ }
581
604
  const path = join(directory, "runtime.lock");
605
+ const nonce = randomUUID();
582
606
  try {
583
- const existing = JSON.parse(await readFile(path, "utf8"));
607
+ const existing = await readPrivateRuntimeJson(path);
584
608
  try {
585
609
  process.kill(existing.pid, 0);
586
610
  throw Object.assign(new Error("A runtime already owns this agent's state directory."), {
@@ -591,12 +615,35 @@ async function runtimeLock(directory) {
591
615
  }
592
616
  await rm(path);
593
617
  } catch (error) {
594
- if (error.code !== "ENOENT") throw error;
618
+ if (error.code !== "ENOENT") {
619
+ await release();
620
+ throw error;
621
+ }
622
+ }
623
+ try {
624
+ const file = await open(path, "wx", 0o600);
625
+ try {
626
+ await file.writeFile(JSON.stringify({ pid: process.pid, nonce }));
627
+ } finally {
628
+ await file.close();
629
+ }
630
+ } catch (error) {
631
+ await release();
632
+ throw error;
595
633
  }
596
- const file = await open(path, "wx", 0o600);
597
- await file.writeFile(JSON.stringify({ pid: process.pid }));
598
- await file.close();
599
- return () => rm(path, { force: true });
634
+ let released = false;
635
+ return async () => {
636
+ if (released) return;
637
+ released = true;
638
+ try {
639
+ const owner = await readPrivateRuntimeJson(path).catch((error) => {
640
+ if (error.code !== "ENOENT") throw error;
641
+ });
642
+ if (owner?.nonce === nonce) await rm(path);
643
+ } finally {
644
+ await release();
645
+ }
646
+ };
600
647
  }
601
648
 
602
649
  export async function runRuntime(settings) {
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { readPrivateRuntimeJson } from "./mission-agent-session.mjs";
6
+ import { isCredentialField, redactCredentialText } from "./sdk/credential-redaction.js";
6
7
 
7
8
  const context = new AsyncLocalStorage();
8
9
  const capabilities = new WeakMap();
@@ -13,23 +14,7 @@ const failureCode = (error, fallback) =>
13
14
  /** Export data-plane text only. Never pass request headers or provider configuration here. */
14
15
  export function redactObservation(value, depth = 0) {
15
16
  if (depth > 8) return "[depth limit]";
16
- if (typeof value === "string")
17
- return value
18
- .replace(
19
- /\b(?:tn_(?:live|test)_[A-Za-z0-9_-]{20,}|sk-(?:ant-)?[A-Za-z0-9_-]{16,}|AKIA[A-Z0-9]{16})\b/g,
20
- "[credential redacted]",
21
- )
22
- .replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+\/-]+=*/gi, "[authorization redacted]")
23
- .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[token redacted]")
24
- .replace(
25
- /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/g,
26
- "[private key redacted]",
27
- )
28
- .replace(
29
- /((?:["']?)(?:[A-Z0-9_]*secret[A-Z0-9_]*|[A-Z0-9_]*password|api[_ -]?(?:key|token)|access[_ -]?token|AWS_SESSION_TOKEN|authorization)(?:["']?)\s*[=:]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;"'}]+)/gi,
30
- "$1[redacted]",
31
- )
32
- .slice(0, MAX_TEXT);
17
+ if (typeof value === "string") return redactCredentialText(value).slice(0, MAX_TEXT);
33
18
  if (Array.isArray(value))
34
19
  return value.slice(0, 80).map((item) => redactObservation(item, depth + 1));
35
20
  if (value && typeof value === "object")
@@ -37,10 +22,9 @@ export function redactObservation(value, depth = 0) {
37
22
  Object.entries(value)
38
23
  .slice(0, 80)
39
24
  .map(([key, item]) => [
40
- key,
41
- /(?:authorization|password|secret|api[_-]?(?:key|token)|access[_-]?token|AWS_SESSION_TOKEN|leaseToken)/i.test(
42
- key,
43
- )
25
+ redactCredentialText(key),
26
+ // Session IDs on observation records are correlation metadata, never auth sessions.
27
+ isCredentialField(key) && !(depth === 0 && key === "sessionId")
44
28
  ? "[redacted]"
45
29
  : redactObservation(item, depth + 1),
46
30
  ]),
@@ -0,0 +1 @@
1
+ const CREDENTIAL_TOKEN_PATTERN=/\b(?:(?:tn|at)_(?:live|test)_[A-Za-z0-9_-]{8,}|t[acr]_[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9]{8,}|github_pat_[A-Za-z0-9_]{8,}|sk-(?:proj-|svcacct-|ant-)?[A-Za-z0-9_-]{16,}|(?:AKIA|ASIA)[A-Z0-9]{16}|[rs]k_(?:live|test)_[A-Za-z0-9]{8,}|xox[a-z][.-][A-Za-z0-9.-]{10,}|xapp-[A-Za-z0-9-]{12,})\b/g;function isCredentialField(key){const normalized=key.replace(/[^a-z0-9]/gi,"").toLowerCase();return/(?:authorization|authentication|password|passwd|secret|apikey|apitoken|accesstoken|refreshtoken|sessiontoken|sessionid|sessionkey|sessioncookie|cookie|leasetoken|privatekey)/.test(normalized)||normalized==="token"}function redactCredentialText(value){return value.replace(/(\b(?:set[-_ ]?cookie|cookie)\s*[:=]\s*)[^\r\n]*/gi,"$1[redacted]").replace(CREDENTIAL_TOKEN_PATTERN,"[credential redacted]").replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+\/-]+=*/gi,"[authorization redacted]").replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,"[token redacted]").replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-]*PRIVATE KEY-----|$)/g,"[private key redacted]").replace(/(["']?(?:[A-Z0-9_]*secret[A-Z0-9_]*|[A-Z0-9_]*password|api[_ -]?(?:key|token)|(?:access|refresh|session)[_ -]?(?:token|key|id|cookie)|(?:set[_ -]?)?cookie|AWS_SESSION_TOKEN|authorization|authentication|token)["']?\s*[=:]\s*)(?:"(?:\\.|[^"\\])*(?:"|$)|'(?:\\.|[^'\\])*(?:'|$)|[^\r\n,;"'}]+)/gi,"$1[redacted]")}export{CREDENTIAL_TOKEN_PATTERN,isCredentialField,redactCredentialText};
@@ -1 +1 @@
1
- import{apiError,TotemCanceledError,TotemError,TotemNetworkError,TotemTimeoutError}from"./errors.js";function idempotencyKey(){return globalThis.crypto.randomUUID()}function sleep(ms,signal){return new Promise((resolve,reject)=>{const cancel=()=>{clearTimeout(timer);signal?.removeEventListener("abort",cancel);reject(new TotemCanceledError("Totem request canceled.",{code:"CANCELED"}))};const timer=setTimeout(()=>{signal?.removeEventListener("abort",cancel);resolve()},ms);signal?.addEventListener("abort",cancel,{once:true});if(signal?.aborted)cancel()})}class Transport{#apiKey;#baseUrl;#fetch;#timeoutMs;#maxRetries;#retryDelayMs;#responseEnvelope;constructor(options,api=void 0){if(!options.apiKey?.trim())throw new TotemError("An agent API key is required.",{code:"INVALID_CONFIG"});const url=new URL(options.baseUrl??"https://totemlayer.com");const loopback=["localhost","127.0.0.1","[::1]"].includes(url.hostname);if(url.protocol!=="https:"&&!(url.protocol==="http:"&&loopback)||url.username||url.password||url.search||url.hash||url.pathname!=="/"){throw new TotemError("baseUrl must be an HTTPS origin or a local HTTP origin.",{code:"INVALID_CONFIG"})}this.#apiKey=options.apiKey;this.#baseUrl=`${url.origin}${api?.basePath??"/api/v1/network"}`;this.#responseEnvelope=api?.responseEnvelope??"data";this.#fetch=options.fetch??globalThis.fetch.bind(globalThis);this.#timeoutMs=options.timeoutMs??15e3;this.#maxRetries=options.maxRetries??2;this.#retryDelayMs=options.retryDelayMs??250;if(!Number.isFinite(this.#timeoutMs)||this.#timeoutMs<1||!Number.isInteger(this.#maxRetries)||this.#maxRetries<0||this.#maxRetries>10||!Number.isFinite(this.#retryDelayMs)||this.#retryDelayMs<0){throw new TotemError("Invalid timeout or retry configuration.",{code:"INVALID_CONFIG"})}}async request(method,path,options={}){const url=new URL(`${this.#baseUrl}${path}`);for(const[key,value]of Object.entries(options.query??{})){if(value!==void 0&&value!==null)url.searchParams.set(key,String(value))}const retrySafe=method==="GET"||options.retrySafe===true||Boolean(options.idempotencyKey);const body=options.binaryBody??(options.body===void 0?void 0:JSON.stringify(options.body));const headers={Authorization:`Bearer ${this.#apiKey}`,Accept:"application/json",...body===void 0||options.binaryBody?{}:{"Content-Type":"application/json"},...options.idempotencyKey?{"Idempotency-Key":options.idempotencyKey}:{},...options.headers??{}};for(let attempt=0;;attempt++){if(options.signal?.aborted)throw new TotemCanceledError("Totem request canceled.",{code:"CANCELED"});const controller=new AbortController;let timer;let cancel;let retryAfter=null;try{const aborted=new Promise((_,reject)=>{cancel=()=>{controller.abort();reject(new TotemCanceledError("Totem request canceled.",{code:"CANCELED"}))};options.signal?.addEventListener("abort",cancel,{once:true});if(options.signal?.aborted)cancel();timer=setTimeout(()=>{controller.abort();reject(new TotemTimeoutError("Totem request timed out.",{code:"TIMEOUT"}))},this.#timeoutMs)});const response=(async()=>{const result=await this.#fetch(url.toString(),{method,headers,body:body instanceof Uint8Array?new Uint8Array(body):body,signal:controller.signal,redirect:"error",cache:"no-store"});retryAfter=result.headers.get("Retry-After");const requestId=result.headers.get("X-Request-Id")??void 0;let payload;try{payload=await result.json()}catch{if(!result.ok)throw apiError(result.status,void 0,requestId);throw new TotemError("Totem returned invalid JSON.",{code:"INVALID_RESPONSE",requestId})}if(!result.ok)throw apiError(result.status,payload,requestId);if(!payload||typeof payload!=="object"||Array.isArray(payload)||this.#responseEnvelope==="data"&&!("data"in payload)){throw new TotemError("Totem returned an invalid response envelope.",{code:"INVALID_RESPONSE",requestId})}return payload})();return await Promise.race([response,aborted])}catch(cause){const error=cause instanceof TotemError?cause:new TotemNetworkError("Unable to reach Totem.",{code:"NETWORK_ERROR",cause});const transient=error instanceof TotemNetworkError||error instanceof TotemTimeoutError||error.status!==void 0&&[408,429,500,502,503,504].includes(error.status);if(!retrySafe||!transient||attempt>=this.#maxRetries)throw error;clearTimeout(timer);if(cancel)options.signal?.removeEventListener("abort",cancel);const seconds=retryAfter===null?NaN:Number(retryAfter);const parsedDate=retryAfter===null?NaN:Date.parse(retryAfter);const requestedDelay=Number.isFinite(seconds)?seconds*1e3:parsedDate-Date.now();const fallback=this.#retryDelayMs*2**attempt*(.75+Math.random()*.5);await sleep(Math.min(3e4,Math.max(0,Number.isFinite(requestedDelay)?requestedDelay:fallback)),options.signal)}finally{clearTimeout(timer);if(cancel)options.signal?.removeEventListener("abort",cancel)}}}async download(path,options={}){const signal=AbortSignal.any([options.signal,AbortSignal.timeout(this.#timeoutMs)].filter(Boolean));let response;try{response=await this.#fetch(`${this.#baseUrl}${path}`,{method:"GET",headers:{Authorization:`Bearer ${this.#apiKey}`},signal,redirect:"error",cache:"no-store"})}catch(cause){throw new TotemNetworkError("Unable to download the attachment.",{code:"NETWORK_ERROR",cause})}if(!response.ok){const payload=await response.json().catch(()=>void 0);throw apiError(response.status,payload,response.headers.get("x-request-id")??void 0)}const reported=Number(response.headers.get("content-length"));if(reported>2*1024*1024)throw new TotemError("The attachment exceeds the 2 MiB limit.",{code:"ATTACHMENT_TOO_LARGE"});const bytes=new Uint8Array(await response.arrayBuffer());if(bytes.byteLength>2*1024*1024)throw new TotemError("The attachment exceeds the 2 MiB limit.",{code:"ATTACHMENT_TOO_LARGE"});const disposition=response.headers.get("content-disposition")??"";const encoded=disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1];return{bytes,mediaType:response.headers.get("content-type")??"application/octet-stream",name:encoded?decodeURIComponent(encoded):"attachment",sha256:response.headers.get("x-content-sha256")}}}export{Transport,idempotencyKey,sleep};
1
+ import{apiError,TotemCanceledError,TotemError,TotemNetworkError,TotemTimeoutError}from"./errors.js";function idempotencyKey(){return globalThis.crypto.randomUUID()}function sleep(ms,signal){return new Promise((resolve,reject)=>{const cancel=()=>{clearTimeout(timer);signal?.removeEventListener("abort",cancel);reject(new TotemCanceledError("Totem request canceled.",{code:"CANCELED"}))};const timer=setTimeout(()=>{signal?.removeEventListener("abort",cancel);resolve()},ms);signal?.addEventListener("abort",cancel,{once:true});if(signal?.aborted)cancel()})}class Transport{#apiKey;#baseUrl;#fetch;#timeoutMs;#maxRetries;#retryDelayMs;#responseEnvelope;constructor(options,api=void 0){if(!options.apiKey?.trim())throw new TotemError("An agent API key is required.",{code:"INVALID_CONFIG"});const url=new URL(options.baseUrl??"https://totemlayer.com");const loopback=["localhost","127.0.0.1","[::1]"].includes(url.hostname);if(url.protocol!=="https:"&&!(url.protocol==="http:"&&loopback)||url.username||url.password||url.search||url.hash||url.pathname!=="/"){throw new TotemError("baseUrl must be an HTTPS origin or a local HTTP origin.",{code:"INVALID_CONFIG"})}this.#apiKey=options.apiKey;this.#baseUrl=`${url.origin}${api?.basePath??"/api/v1/network"}`;this.#responseEnvelope=api?.responseEnvelope??"data";this.#fetch=options.fetch??globalThis.fetch.bind(globalThis);this.#timeoutMs=options.timeoutMs??15e3;this.#maxRetries=options.maxRetries??2;this.#retryDelayMs=options.retryDelayMs??250;if(!Number.isFinite(this.#timeoutMs)||this.#timeoutMs<1||!Number.isInteger(this.#maxRetries)||this.#maxRetries<0||this.#maxRetries>10||!Number.isFinite(this.#retryDelayMs)||this.#retryDelayMs<0){throw new TotemError("Invalid timeout or retry configuration.",{code:"INVALID_CONFIG"})}}async request(method,path,options={}){const url=new URL(`${this.#baseUrl}${path}`);for(const[key,value]of Object.entries(options.query??{})){if(value!==void 0&&value!==null)url.searchParams.set(key,String(value))}const retrySafe=method==="GET"||options.retrySafe===true||Boolean(options.idempotencyKey);const body=options.binaryBody??(options.body===void 0?void 0:JSON.stringify(options.body));const headers={Authorization:`Bearer ${this.#apiKey}`,Accept:"application/json",...body===void 0||options.binaryBody?{}:{"Content-Type":"application/json"},...options.idempotencyKey?{"Idempotency-Key":options.idempotencyKey}:{},...options.headers??{}};for(let attempt=0;;attempt++){if(options.signal?.aborted)throw new TotemCanceledError("Totem request canceled.",{code:"CANCELED"});const controller=new AbortController;let timer;let cancel;let retryAfter=null;try{const aborted=new Promise((_,reject)=>{cancel=()=>{controller.abort();reject(new TotemCanceledError("Totem request canceled.",{code:"CANCELED"}))};options.signal?.addEventListener("abort",cancel,{once:true});if(options.signal?.aborted)cancel();timer=setTimeout(()=>{controller.abort();reject(new TotemTimeoutError("Totem request timed out.",{code:"TIMEOUT"}))},this.#timeoutMs)});const response=(async()=>{const result=await this.#fetch(url.toString(),{method,headers,body:body instanceof Uint8Array?new Uint8Array(body):body,signal:controller.signal,redirect:"error",cache:"no-store"});retryAfter=result.headers.get("Retry-After");const requestId=result.headers.get("X-Request-Id")??void 0;let payload;try{payload=await result.json()}catch{if(!result.ok)throw apiError(result.status,void 0,requestId);throw new TotemError("Totem returned invalid JSON.",{code:"INVALID_RESPONSE",requestId})}if(!result.ok)throw apiError(result.status,payload,requestId);if(!payload||typeof payload!=="object"||Array.isArray(payload)||this.#responseEnvelope==="data"&&!("data"in payload)){throw new TotemError("Totem returned an invalid response envelope.",{code:"INVALID_RESPONSE",requestId})}return payload})();return await Promise.race([response,aborted])}catch(cause){const error=cause instanceof TotemError?cause:new TotemNetworkError("Unable to reach Totem.",{code:"NETWORK_ERROR",cause});const transient=error instanceof TotemNetworkError||error instanceof TotemTimeoutError||error.status!==void 0&&[408,429,500,502,503,504].includes(error.status);if(!retrySafe||!transient||attempt>=this.#maxRetries)throw error;clearTimeout(timer);if(cancel)options.signal?.removeEventListener("abort",cancel);const seconds=retryAfter===null?NaN:Number(retryAfter);const parsedDate=retryAfter===null?NaN:Date.parse(retryAfter);const requestedDelay=Number.isFinite(seconds)?seconds*1e3:parsedDate-Date.now();const fallback=this.#retryDelayMs*2**attempt*(.75+Math.random()*.5);await sleep(Math.min(3e4,Math.max(0,Number.isFinite(requestedDelay)?requestedDelay:fallback)),options.signal)}finally{clearTimeout(timer);if(cancel)options.signal?.removeEventListener("abort",cancel)}}}async download(path,options={}){const signal=AbortSignal.any([options.signal,AbortSignal.timeout(this.#timeoutMs)].filter(Boolean));let response;try{response=await this.#fetch(`${this.#baseUrl}${path}`,{method:"GET",headers:{Authorization:`Bearer ${this.#apiKey}`},signal,redirect:"error",cache:"no-store"})}catch(cause){throw new TotemNetworkError("Unable to download the attachment.",{code:"NETWORK_ERROR",cause})}if(!response.ok){const payload=await response.json().catch(()=>void 0);throw apiError(response.status,payload,response.headers.get("x-request-id")??void 0)}const reported=Number(response.headers.get("content-length"));if(reported>2*1024*1024){await response.body?.cancel().catch(()=>{});throw new TotemError("The attachment exceeds the 2 MiB limit.",{code:"ATTACHMENT_TOO_LARGE"})}const reader=response.body?.getReader();const chunks=[];let size=0;if(reader){try{for(;;){const{done,value}=await reader.read();if(done)break;size+=value.byteLength;if(size>2*1024*1024){await reader.cancel().catch(()=>{});throw new TotemError("The attachment exceeds the 2 MiB limit.",{code:"ATTACHMENT_TOO_LARGE"})}chunks.push(value)}}finally{reader.releaseLock()}}const bytes=new Uint8Array(size);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.byteLength}const disposition=response.headers.get("content-disposition")??"";const encoded=disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1];return{bytes,mediaType:response.headers.get("content-type")??"application/octet-stream",name:encoded?decodeURIComponent(encoded):"attachment",sha256:response.headers.get("x-content-sha256")}}}export{Transport,idempotencyKey,sleep};
@@ -1 +1 @@
1
- {"version":1,"packageVersion":"0.1.75","bytes":27189,"sha256":"c62a0116c76177d905051b40ddcbb0503e042b1fc8f5d024c1ebf41c7f173983"}
1
+ {"version":1,"packageVersion":"0.1.76","bytes":27189,"sha256":"c62a0116c76177d905051b40ddcbb0503e042b1fc8f5d024c1ebf41c7f173983"}
@@ -1 +1 @@
1
- {"version":1,"packageVersion":"0.1.75","bytes":36144,"sha256":"b9e6518f5e1d15b83e013469753220a26ab61fb9a4bc45db74cc007ea2178279"}
1
+ {"version":1,"packageVersion":"0.1.76","bytes":36144,"sha256":"66535754c594ad559c91b68c93348ec5579f10a0d791c462cecf98cb618ea2ae"}
@@ -39,10 +39,10 @@ Use an actual native user-message tool if available and wait for delivery. Other
39
39
  Run on the actual command-capable agent host, from the intended native session:
40
40
 
41
41
  ~~~sh
42
- npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.75.tgz totem install
42
+ npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.76.tgz totem install
43
43
  ~~~
44
44
 
45
- This command pins release **0.1.75**. Verify the published package and full guide before making setup changes. Node.js 22.14 or newer is required for this package. Pin the tested release. `--ignore-scripts` prevents package lifecycle setup before the strict controller. Installing the package alone grants no connection or background authority.
45
+ This command pins release **0.1.76**. Verify the published package and full guide before making setup changes. Node.js 22.14 or newer is required for this package. Pin the tested release. `--ignore-scripts` prevents package lifecycle setup before the strict controller. Installing the package alone grants no connection or background authority.
46
46
 
47
47
  The command verifies the full bundled guide against its byte length/SHA-256 manifest, with a complete alternate copy. `totem install --read-guide` prints it without setup changes. Corrupt/incomplete required guides are a hard stop. The native adapter must verify the current model and unchanged host permissions, exact session, intended identity, transport and current credential generation. “default” or a guessed model is not evidence. There is no force-success, skip-tests, model-supplied proof or arbitrary adapter-script URL.
48
48
 
@@ -39,10 +39,10 @@ Use an actual native user-message tool if available and wait for delivery. Other
39
39
  Run on the actual command-capable agent host, from the intended native session:
40
40
 
41
41
  ~~~sh
42
- npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.75.tgz totem install
42
+ npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.76.tgz totem install
43
43
  ~~~
44
44
 
45
- This command pins release **0.1.75**. Verify the published package and full guide before making setup changes. Node.js 22.14 or newer is required for this package. Pin the tested release. `--ignore-scripts` prevents package lifecycle setup before the strict controller. Installing the package alone grants no connection or background authority.
45
+ This command pins release **0.1.76**. Verify the published package and full guide before making setup changes. Node.js 22.14 or newer is required for this package. Pin the tested release. `--ignore-scripts` prevents package lifecycle setup before the strict controller. Installing the package alone grants no connection or background authority.
46
46
 
47
47
  The command verifies the full bundled guide against its byte length/SHA-256 manifest, with a complete alternate copy. `totem install --read-guide` prints it without setup changes. Corrupt/incomplete required guides are a hard stop. The native adapter must verify the current model and unchanged host permissions, exact session, intended identity, transport and current credential generation. “default” or a guessed model is not evidence. There is no force-success, skip-tests, model-supplied proof or arbitrary adapter-script URL.
48
48
 
@@ -16,6 +16,7 @@ import { readReleaseSetupGuide } from "./setup-guide.mjs";
16
16
  import { prepareSetupTests, readSetupTests } from "./setup-receipts.mjs";
17
17
  import { defaultCodexQueue } from "./codex-chat-bridge.mjs";
18
18
  import { installedClientInvocation, localClientEnvironment } from "./platform-client.mjs";
19
+ import { validOrganizationId } from "./strict-setup.mjs";
19
20
 
20
21
  const execute = promisify(execFile);
21
22
  const fail = (code) => {
@@ -126,7 +127,7 @@ export function createStrictCodexAdapter(flags, options = {}) {
126
127
  own.id !== saved.agentId ||
127
128
  own.address !== saved.address ||
128
129
  own.environment !== saved.environment ||
129
- !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(own.organization?.id ?? "")
130
+ !validOrganizationId(own.organization?.id)
130
131
  )
131
132
  fail("AGENT_IDENTITY_MISMATCH");
132
133
  const checked = await snapshot();
@@ -17,6 +17,9 @@ const token = (value) =>
17
17
  typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,319}$/.test(value);
18
18
  const uuid = (value) =>
19
19
  typeof value === "string" && /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value);
20
+ export const validOrganizationId = (value) =>
21
+ uuid(value) ||
22
+ (typeof value === "string" && /^org_(?:[A-Za-z0-9_-]{24}|personal_[a-f0-9]{20})$/.test(value));
20
23
  const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
21
24
  const stop = (code) => {
22
25
  throw Object.assign(new Error(code), { code });
@@ -26,7 +29,7 @@ export function setupBinding(identity, host, transport, connection) {
26
29
  if (
27
30
  !uuid(identity?.id) ||
28
31
  !token(identity.address) ||
29
- !uuid(identity.organization?.id) ||
32
+ !validOrganizationId(identity.organization?.id) ||
30
33
  !["PRODUCTION", "SANDBOX"].includes(identity.environment) ||
31
34
  !token(host?.name) ||
32
35
  !token(host.model) ||
@@ -245,6 +248,7 @@ export async function runStrictSetup(adapter, options = {}) {
245
248
  if (
246
249
  expected &&
247
250
  (identity?.id !== expected.id ||
251
+ identity.address !== expected.address ||
248
252
  identity.environment !== expected.environment ||
249
253
  (expected.organization?.id && identity.organization?.id !== expected.organization.id))
250
254
  )
@@ -47,7 +47,7 @@ export function validateWindowsPrivateAcl(snapshot) {
47
47
  !rule.inheritOnly &&
48
48
  rule.type === "Allow" &&
49
49
  !trustedAncestors.has(rule.sid) &&
50
- (rule.rights & (64 | 262144 | 524288)) !== 0,
50
+ (rule.rights & (64 | 65536 | 262144 | 524288)) !== 0,
51
51
  ),
52
52
  )
53
53
  )
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "totemlayer",
3
- "version": "0.1.75",
4
- "description": "Cross-platform CLI for TotemLayer agent identity and authorization",
3
+ "version": "0.1.77",
4
+ "description": "Connect AI agents to Totem Network for discovery, messaging, and shared work.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "totem": "bin/totem.mjs",
@@ -20,11 +20,16 @@
20
20
  },
21
21
  "keywords": [
22
22
  "ai-agents",
23
- "authorization",
24
- "cli",
25
- "identity",
23
+ "agent-network",
24
+ "agent-discovery",
25
+ "agent-to-agent",
26
+ "messaging",
27
+ "multi-agent",
28
+ "collaboration",
29
+ "mcp",
26
30
  "totemlayer",
27
- "verification"
31
+ "cli",
32
+ "authorization"
28
33
  ],
29
34
  "homepage": "https://totemlayer.com",
30
35
  "license": "SEE LICENSE IN LICENSE.md",
@@ -36,7 +41,7 @@
36
41
  "@modelcontextprotocol/client": "2.3.0",
37
42
  "@modelcontextprotocol/server": "2.3.0",
38
43
  "undici": "7.30.1",
39
- "zod": "4.6.6"
44
+ "zod": "4.6.7"
40
45
  },
41
46
  "scripts": {
42
47
  "postinstall": "node bin/installation.mjs --postinstall",