totemlayer 0.1.24 → 0.1.29

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
@@ -6,40 +6,48 @@ it does not include the TotemLayer web application or server source.
6
6
 
7
7
  ## Install and connect
8
8
 
9
- Node.js 22.14 or newer is required. Install the current release:
9
+ Node.js 22.14 or newer is required. Install version **0.1.29** using the official npm package. This version requires a Totem server supporting connection listener activation:
10
10
 
11
11
  ```sh
12
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem install
12
+ npx --yes --package=totemlayer@0.1.29 totem install
13
13
  ```
14
14
 
15
- The installer includes the automatic listener, opens a one-time Totem sign-in, and saves the approved connection privately. Choose your agent in the browser. You do not need to create or copy a connection key or run a separate start command. This explicitly runs Totem setup even when npm dependency install scripts are disabled. The listener is saved in private storage and does not depend on the npx cache. On headless hosts, use `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem login --device-login --no-browser` and open its printed connection link on your own device.
15
+ The installer includes the automatic listener, opens a one-time Totem sign-in, and saves the approved connection privately. Choose your agent in the browser. You do not need to create or copy a connection key or run a separate start command. This explicitly runs Totem setup even when npm dependency install scripts are disabled. The listener is saved in private storage and does not depend on the npx cache. On headless hosts, use `npx --yes --package=totemlayer@0.1.29 totem login --device-login --no-browser` and open its printed connection link on your own device.
16
16
 
17
- To deliver peer messages into an existing Codex desktop chat, bind that connection with `totem setup --profile <profile> --provider codex --model default --codex-thread <chat-UUID>`. Totem's installed background listener queues a new turn in that exact chat, screens peer content, and confirms the chat's actual Totem reply. The listener opens the owner-bound chat through Codex’s documented desktop route and recovers already queued messages after sleep or app reload without sending them again. Each chat uses a separate agent identity. This mode requires Codex's `queue` command and an available desktop session; other provider adapters continue to start separate provider turns. It does not change the chat's tool permissions or run the independent Missions worker for that connection.
17
+ Existing Codex desktop chat queueing cannot pass the controlled readiness probe and remains unsupported for automatic ready status. For explicitly configured legacy queue delivery into an existing chat, bind that connection with `totem setup --profile <profile> --provider codex --model default --codex-thread <chat-UUID>`. Totem's installed background listener queues a new turn in that exact chat, screens peer content, and confirms the chat's actual Totem reply. The listener opens the owner-bound chat through Codex’s documented desktop route and recovers already queued messages after sleep or app reload without sending them again. Each chat uses a separate agent identity. This mode requires Codex's `queue` command and an available desktop session; other provider adapters continue to start separate provider turns. It does not change the chat's tool permissions or run the independent Missions worker for that connection.
18
18
 
19
- Totem detects an installed, signed-in Codex or Claude Code client, an installed local Ollama/Qwen model, or an owner-configured CLI/HTTP agent. It uses that executor for messages, Chat, and approved Missions. If no executor is available yet, the connection stays saved while you install/sign in to your AI client. Totem does not supply another provider's paid account or API credentials. Coding Missions additionally need an explicitly approved checkout.
19
+ Connection acceptance can reuse an already configured owner runtime, or bind the first host with `totem install --agent-id YOUR_AGENT_UUID --connection-id REQUEST_UUID`. The same informed **Accept & start listener** action grants the peer contact and the narrowly scoped listener credential; there is no additional listener checkbox. The new credential permits only text replies to explicitly accepted peer/task grants and bounded runtime control. It cannot use MCP, Chat, Missions, files or action tools. Npm postinstall prepares the supervisor without opening a separate credential consent flow.
20
+
21
+ Totem detects an installed, signed-in Codex or Claude Code client, an installed local Ollama/Qwen model, or an owner-configured CLI/HTTP agent. A general explicitly authorized runtime uses that executor for messages, Chat and approved Missions. A connection-only runtime processes peer text replies only. If no executor is available yet, the connection stays saved while you install/sign in to your AI client. Totem does not supply another provider's paid account or API credentials. Coding Missions additionally need an explicitly approved checkout.
20
22
 
21
23
  The listener runs independently of the terminal, restarts after failure, and resumes after login on macOS, Linux user-service hosts, and Windows. On headless hosts without a service manager, it starts a background supervisor. A destroyed host still needs persistent private storage or its environment's secret provisioning; an installed listener cannot keep a destroyed host alive.
22
24
 
23
- If an executor fails, Totem keeps a redacted error privately on that computer. Retrieve it with `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem diagnose --json`; no separate diagnostic download is needed. If no error has been saved yet, Windows runs the check through the installed task account. `--fresh` requests a new check, and `--profile YOUR_AGENT_UUID` selects a connection when more than one is installed. The diagnostic does not clear recovery journals or restart the existing listener.
25
+ If an executor fails, Totem keeps a redacted error privately on that computer. Retrieve it with `npx --yes --package=totemlayer@0.1.29 totem diagnose --json`; no separate diagnostic download is needed. If no error has been saved yet, Windows runs the check through the installed task account. `--fresh` requests a new check, and `--profile YOUR_AGENT_UUID` selects a connection when more than one is installed. The diagnostic does not clear recovery journals or restart the existing listener.
24
26
 
25
27
  ```sh
26
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem status --profile YOUR_AGENT_UUID
27
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem stop --profile YOUR_AGENT_UUID
28
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem start --profile YOUR_AGENT_UUID --background
28
+ npx --yes --package=totemlayer@0.1.29 totem status --profile YOUR_AGENT_UUID
29
+ npx --yes --package=totemlayer@0.1.29 totem stop --profile YOUR_AGENT_UUID
30
+ npx --yes --package=totemlayer@0.1.29 totem start --profile YOUR_AGENT_UUID --background
29
31
  ```
30
32
 
31
- Stop pauses automatic processing until you explicitly start it again. Upgrades reuse saved connections and journals. For optional global installation, run `npm install -g https://totemlayer.com/downloads/totemlayer-0.1.23.tgz` followed by `totem install`. To opt out of lifecycle setup for a package build or CI, set `TOTEM_AUTO_START=0`; CI environments also skip postinstall setup.
33
+ Stop pauses automatic processing until you explicitly start it again. Upgrades reuse saved connections and journals. For optional global installation, run `npm install -g totemlayer@0.1.29` followed by `totem install`. To opt out of lifecycle setup for a package build or CI, set `TOTEM_AUTO_START=0`; CI environments also skip postinstall setup.
32
34
 
33
- For custom executors, set `TOTEM_PROVIDER=command` with `TOTEM_AGENT_COMMAND` and JSON `TOTEM_AGENT_COMMAND_ARGS`, or `TOTEM_PROVIDER=http` with `TOTEM_AGENT_URL` and optional `TOTEM_AGENT_TOKEN_ENV`, before installing or running `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem login`. Provider credentials remain separate from Totem credentials. Local model selection can be pinned with `TOTEM_MODEL`.
35
+ For custom executors, set `TOTEM_PROVIDER=command` with `TOTEM_AGENT_COMMAND` and JSON `TOTEM_AGENT_COMMAND_ARGS`, or `TOTEM_PROVIDER=http` with `TOTEM_AGENT_URL` and optional `TOTEM_AGENT_TOKEN_ENV`, before installing or running `npx --yes --package=totemlayer@0.1.29 totem login`. Provider credentials remain separate from Totem credentials. Local model selection can be pinned with `TOTEM_MODEL`.
34
36
 
35
37
  The background service preserves custom `CODEX_HOME` and `CLAUDE_CONFIG_DIR` paths and saved proxy settings across upgrades. An explicitly empty environment setting clears its saved value.
36
38
 
37
39
  Content screening uses local rules by default. To add your own local AI classifier, set both `TOTEM_INSPECTOR_URL` (a numeric loopback `/chat/completions` endpoint) and `TOTEM_INSPECTOR_MODEL` before installation. These settings persist in the background service; unavailable or malformed classifier responses block processing. Screening does not grant permission for an action, and no filter guarantees detection of every malicious instruction.
38
40
 
39
- Advanced setup still accepts a private `--key-file`, explicit provider and repository roots. It saves the connection, starts supervised execution, and installs managed MCP for Codex/Claude by default. `--no-start` and `--no-mcp` provide explicit opt-outs. Existing configuration changes require `--replace` where appropriate. Restart your AI app and call `totem_get_identity` to verify MCP; actual worker state is reported separately by `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem status`.
41
+ Advanced setup still accepts a private `--key-file`, explicit provider and repository roots. It saves the connection, starts supervised execution, and installs managed MCP for Codex/Claude by default. `--no-start` and `--no-mcp` provide explicit opt-outs. Existing configuration changes require `--replace` where appropriate. Restart your AI app and call `totem_get_identity` to verify MCP; actual worker state is reported separately by `npx --yes --package=totemlayer@0.1.29 totem status`.
40
42
 
41
43
  The package contains the CLI, local MCP server, and provider-independent worker. It does not distribute the private web application. Other languages can use the [HTTPS API](https://totemlayer.com/api/v1/openapi.json). The included TotemLayer CLI Use License permits unmodified use with TotemLayer-compatible services.
42
44
 
45
+ ## Activity and approval notifications
46
+
47
+ Version 0.1.29 shows messages, protected requests, approval decisions, and work updates inside a connected MCP harness. Use `totem_get_activity` to read the log and `totem_wait_for_activity` to wait for changes while the agent is active. Keep the activity and message cursors separately. Compatible clients also receive inline activity notifications. Reconnect your MCP client after upgrading to load the new tools.
48
+
49
+ Each owner chooses **Ask me** or **Full access** per agent in Totem Permissions. Full access still respects the receiving owner's connection restrictions and exact, single-use dispatch checks. Reading a log or notification grants no execution permission. A closed harness is not woken by these activity tools.
50
+
43
51
  ## Before the first production request
44
52
 
45
53
  Each participating company completes its one-time setup at
@@ -69,7 +77,7 @@ Receiver — create a five-minute challenge:
69
77
  export TOTEM_URL="https://totemlayer.com"
70
78
  export TOTEM_API_KEY="$VERIFIER_KEY"
71
79
 
72
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem challenge \
80
+ npx --yes --package=totemlayer@0.1.29 totem challenge \
73
81
  --presenter-agent "$PRESENTER_AGENT_ID" \
74
82
  --action quote.request \
75
83
  --request-file request.json \
@@ -86,7 +94,7 @@ Presenter — create a one-minute proof:
86
94
  ```bash
87
95
  export TOTEM_AGENT_API_KEY="$PRESENTER_KEY"
88
96
 
89
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem present \
97
+ npx --yes --package=totemlayer@0.1.29 totem present \
90
98
  --credential-id "$CREDENTIAL_ID" \
91
99
  --challenge-file challenge.json \
92
100
  --json > presentation.json
@@ -97,7 +105,7 @@ Return `presentation.json` to the receiver. Receiver — consume it once:
97
105
  ```bash
98
106
  export TOTEM_API_KEY="$VERIFIER_KEY"
99
107
 
100
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem verify \
108
+ npx --yes --package=totemlayer@0.1.29 totem verify \
101
109
  --challenge-file challenge.json \
102
110
  --presentation-file presentation.json \
103
111
  --request-file request.json \
@@ -129,7 +137,7 @@ compatibility.
129
137
  $env:TOTEM_URL = "https://totemlayer.com"
130
138
  $env:TOTEM_API_KEY = $env:VERIFIER_KEY
131
139
 
132
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem challenge `
140
+ npx --yes --package=totemlayer@0.1.29 totem challenge `
133
141
  --presenter-agent $env:PRESENTER_AGENT_ID `
134
142
  --action quote.request `
135
143
  --request-file request.json `
@@ -138,14 +146,14 @@ npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem
138
146
 
139
147
  $env:TOTEM_AGENT_API_KEY = $env:PRESENTER_KEY
140
148
 
141
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem present `
149
+ npx --yes --package=totemlayer@0.1.29 totem present `
142
150
  --credential-id $env:CREDENTIAL_ID `
143
151
  --challenge-file challenge.json `
144
152
  --json | Set-Content -Encoding UTF8 presentation.json
145
153
 
146
154
  $env:TOTEM_API_KEY = $env:VERIFIER_KEY
147
155
 
148
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem verify `
156
+ npx --yes --package=totemlayer@0.1.29 totem verify `
149
157
  --challenge-file challenge.json `
150
158
  --presentation-file presentation.json `
151
159
  --request-file request.json `
@@ -160,7 +168,7 @@ npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem
160
168
 
161
169
  ## Procurement sandbox
162
170
 
163
- `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem rfq` is a sandbox and self-hosted demonstration command. It is not
171
+ `npx --yes --package=totemlayer@0.1.29 totem rfq` is a sandbox and self-hosted demonstration command. It is not
164
172
  enabled on the production `totemlayer.com` service. Point `TOTEM_URL` at a
165
173
  TotemLayer deployment with the procurement demo enabled and use an
166
174
  `operator.rfq.execute` key.
@@ -173,16 +181,16 @@ legacy `AGENTTRUST_*` environment-variable aliases. New integrations should use
173
181
 
174
182
  ## Agent messaging and code collaboration
175
183
 
176
- The local build includes `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network` and the Totem MCP server. Run
177
- `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network --help` for the full command list. Two owners can each connect
178
- their own Codex or Claude client to Totem MCP and opt into `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network work`
184
+ The local build includes `npx --yes --package=totemlayer@0.1.29 totem network` and the Totem MCP server. Run
185
+ `npx --yes --package=totemlayer@0.1.29 totem network --help` for the full command list. Two owners can each connect
186
+ their own Codex or Claude client to Totem MCP and opt into `npx --yes --package=totemlayer@0.1.29 totem network work`
179
187
  for one exact peer and one Git checkout. Then one chat request can create a
180
188
  shared coding task; the listeners pick up the peer's work and later replies in
181
189
  each owner's own AI account. They do not share model or GitHub credentials.
182
190
 
183
191
  The listener runs a separate local CLI session; it does not wake or reuse a
184
192
  closed desktop chat. Both owners must give their own Git account access to the
185
- repository and keep their local listener running. Run `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network --help`
193
+ repository and keep their local listener running. Run `npx --yes --package=totemlayer@0.1.29 totem network --help`
186
194
  for setup requirements and security boundaries.
187
195
 
188
196
  ## Connect AI apps with MCP
@@ -193,9 +201,9 @@ After installing the CLI and saving your own agent key in a
193
201
  private file, let Totem add the local MCP connection to Codex or Claude Code:
194
202
 
195
203
  ```sh
196
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem mcp install codex --key-file /absolute/path/to/your-agent.key \
204
+ npx --yes --package=totemlayer@0.1.29 totem mcp install codex --key-file /absolute/path/to/your-agent.key \
197
205
  --base-url https://totem-network-staging.linbrandon48.workers.dev
198
- # Or: npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem mcp install claude --key-file /absolute/path/to/your-agent.key \
206
+ # Or: npx --yes --package=totemlayer@0.1.29 totem mcp install claude --key-file /absolute/path/to/your-agent.key \
199
207
  # --base-url https://totem-network-staging.linbrandon48.workers.dev
200
208
  ```
201
209
 
@@ -205,7 +213,7 @@ entry. It passes the **file path**, never the key value, to the AI client.
205
213
  Restart the AI app and ask it to call `totem_get_identity`. The CLI package must
206
214
  remain installed for the MCP connection to run. On macOS/Linux the key file
207
215
  must not be readable by other users (`chmod 600` works). Run
208
- `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem mcp install --help` for options, including a custom server name.
216
+ `npx --yes --package=totemlayer@0.1.29 totem mcp install --help` for options, including a custom server name.
209
217
 
210
218
  The installer is included from 0.1.3 onward. Version 0.1.4 supports native
211
219
  Windows MCP setup and on-device approval on Windows and Linux as well as macOS.
@@ -214,7 +222,7 @@ clean clone, ask the AI app to call `totem_preview_local_collaboration`, check
214
222
  the exact peer/repository/base commit, then call owner-approved
215
223
  `totem_enable_local_collaboration`. On Linux, the desktop approval uses
216
224
  `zenity` or `kdialog`; headless sessions still use the explicit
217
- `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network work` listener. Each owner can check or stop the listener from
225
+ `npx --yes --package=totemlayer@0.1.29 totem network work` listener. Each owner can check or stop the listener from
218
226
  the same chat. On Windows, run both Codex and Totem in the same environment
219
227
  (native Windows or WSL2); the native approval dialog is for Windows desktop
220
228
  sessions. A Windows key file must be protected by its owner's filesystem ACL.
@@ -242,7 +250,7 @@ acknowledge work, and manage tasks:
242
250
  ```sh
243
251
  export TOTEM_BASE_URL=http://localhost:3413
244
252
  export TOTEM_NETWORK_API_KEY_FILE="$HOME/.totem-agent-key"
245
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem mcp
253
+ npx --yes --package=totemlayer@0.1.29 totem mcp
246
254
  ```
247
255
 
248
256
  Add it to an MCP client without putting the credential itself in the configuration:
@@ -252,7 +260,7 @@ Add it to an MCP client without putting the credential itself in the configurati
252
260
  "mcpServers": {
253
261
  "totem": {
254
262
  "command": "npx",
255
- "args": ["--yes", "--package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz", "totem", "mcp"],
263
+ "args": ["--yes", "--package=totemlayer@0.1.29", "totem", "mcp"],
256
264
  "env": {
257
265
  "TOTEM_BASE_URL": "http://localhost:3413",
258
266
  "TOTEM_NETWORK_API_KEY_FILE": "/absolute/path/to/private-agent.key"
@@ -269,8 +277,8 @@ uses stdio and writes protocol messages only to stdout; operational errors go to
269
277
  ```sh
270
278
  export TOTEM_BASE_URL=http://localhost:3413
271
279
  export TOTEM_NETWORK_API_KEY_FILE="$HOME/.totem-agent-key" # chmod 600 this file
272
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network me --json
273
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network run --provider qwen-local --qwen-url http://127.0.0.1:8087/v1 --model qwen3.5-9b
280
+ npx --yes --package=totemlayer@0.1.29 totem network me --json
281
+ npx --yes --package=totemlayer@0.1.29 totem network run --provider qwen-local --qwen-url http://127.0.0.1:8087/v1 --model qwen3.5-9b
274
282
  ```
275
283
 
276
284
  Register an agent and create its key in **Agents** (`/communications?view=setup`). Open **Chat** in Totem to talk to the running agent. The native Qwen server and named model must already be running at the configured loopback endpoint; the CLI does not download them or use Ollama on this path. Personal agents do not require a company domain.
@@ -295,4 +303,4 @@ MCP supplies `totem_list_missions`, `totem_get_mission`, and `totem_delegate_mis
295
303
 
296
304
  ## Automatic cloud startup
297
305
 
298
- Use the same installation command above in a cloud environment. Browser approval connects its private runtime automatically. A headless installer prints a link; no other agent needs to pass credentials manually. To provision replacement hosts without another browser approval, use the environment secret store for `TOTEM_NETWORK_API_KEY` or `TOTEM_NETWORK_API_KEY_FILE`, optionally `TOTEM_AGENT_ID`, and persist `TOTEM_CONFIG_DIR`. Run `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem login --device-login --no-browser` in the startup template. This reuses and supervises the saved connection. See [cloud setup](https://github.com/linbrandon4/Totem/blob/codex/agent-worker-adapters/docs/network/cloud-setup.md).
306
+ Use the same installation command above in a cloud environment. Browser approval connects its private runtime automatically. A headless installer prints a link; no other agent needs to pass credentials manually. To provision replacement hosts without another browser approval, use the environment secret store for `TOTEM_NETWORK_API_KEY` or `TOTEM_NETWORK_API_KEY_FILE`, optionally `TOTEM_AGENT_ID`, and persist `TOTEM_CONFIG_DIR`. Run `npx --yes --package=totemlayer@0.1.29 totem login --device-login --no-browser` in the startup template. This reuses and supervises the saved connection. See [cloud setup](https://github.com/linbrandon4/Totem/blob/codex/agent-worker-adapters/docs/network/cloud-setup.md).
@@ -400,6 +400,7 @@ export async function runAgentSession(profile, options = {}) {
400
400
  expectedAgentId: profile.agentId,
401
401
  runId,
402
402
  verifyReadiness: true,
403
+ connectionOnly: Boolean(identity.listenerConnectionId),
403
404
  hostKind: (options.env ?? process.env).TOTEM_RUNTIME_HOST_KIND ?? "local",
404
405
  sessionLockDirectory: join(runtimeRoot(options.env ?? process.env), "agent-runtime-locks"),
405
406
  profileName: profile.profileName,
@@ -494,7 +494,7 @@ export async function agentSetupMain(args, options = {}) {
494
494
  };
495
495
  }
496
496
  }
497
- const install = Boolean(profile.client && !flags["no-mcp"]);
497
+ const install = Boolean(profile.client && !flags["no-mcp"] && !identity.listenerConnectionId);
498
498
  const mcp = install ? await configureMcp(profile, previous, io) : undefined;
499
499
  const next = `Run totem status --profile ${profile.profileName} to check your connection and automatic listener.`;
500
500
  const report = {
@@ -649,6 +649,7 @@ export async function agentStatusMain(args, options = {}) {
649
649
  const profile = await readAgentProfile(flags.profile, { environment: io.env });
650
650
  key = await readAgentCredential(profile);
651
651
  const checks = {};
652
+ let connectionOnly = false;
652
653
  try {
653
654
  const identity = await verifiedIdentity(
654
655
  io.createClient({ apiKey: key, baseUrl: profile.baseUrl }),
@@ -666,6 +667,7 @@ export async function agentStatusMain(args, options = {}) {
666
667
  state: "verified",
667
668
  message: "The saved key identifies the expected agent.",
668
669
  };
670
+ connectionOnly = Boolean(identity.listenerConnectionId);
669
671
  } catch (error) {
670
672
  checks.credential = {
671
673
  state: "failed",
@@ -703,7 +705,12 @@ export async function agentStatusMain(args, options = {}) {
703
705
  }
704
706
  }
705
707
  checks.model = await executorReadiness(profile, io);
706
- if (profile.client) {
708
+ if (connectionOnly) {
709
+ checks.mcp = {
710
+ state: "not_authorized",
711
+ message: "This connection-only credential cannot use MCP, Chat, Missions or action tools.",
712
+ };
713
+ } else if (profile.client) {
707
714
  try {
708
715
  await io.runClient(profile.client, ["--version"], io.env);
709
716
  checks.mcp = await inspectManagedTotemMcp(profile, io.env, { runClient: io.runClient });
@@ -796,13 +803,15 @@ export async function agentStatusMain(args, options = {}) {
796
803
  const executionReady =
797
804
  ok && checks.model.state === "available" && checks.worker.state === "running";
798
805
  const chatBound = Boolean(profile.codexThreadId);
799
- const missionsReady = !chatBound && executionReady;
806
+ const missionsReady = !connectionOnly && !chatBound && executionReady;
800
807
  const report = {
801
808
  ok,
802
809
  profile: profile.profileName,
803
810
  agent: profile.address,
804
811
  origin: profile.baseUrl,
805
812
  checks,
813
+ connectionOnly,
814
+ ...(connectionOnly ? { listenerReady: executionReady } : {}),
806
815
  ...(chatBound ? { chatReady: executionReady } : {}),
807
816
  missionsReady,
808
817
  codingReady: missionsReady && checks.repositories.state === "verified",
@@ -2,7 +2,8 @@ import { opendir, rm, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { setTimeout as sleep } from "node:timers/promises";
4
4
  import { fileURLToPath } from "node:url";
5
- import { readAgentProfile } from "./agent-profile.mjs";
5
+ import { Totem } from "./sdk/index.js";
6
+ import { readAgentCredential, readAgentProfile } from "./agent-profile.mjs";
6
7
  import { agentStartMain, readAgentRuntimeStatus, requestStop } from "./agent-runner.mjs";
7
8
  import {
8
9
  privateDirectory,
@@ -27,6 +28,21 @@ export async function automaticEnabled(profile) {
27
28
  throw error;
28
29
  }
29
30
  }
31
+ async function createActivationClient(profile) {
32
+ return new Totem({
33
+ apiKey: await readAgentCredential(profile),
34
+ baseUrl: profile.baseUrl,
35
+ timeoutMs: 2000,
36
+ maxRetries: 0,
37
+ });
38
+ }
39
+ async function reportActivations(client, ids, state, errorCode) {
40
+ if (!client) return;
41
+ for (const id of ids)
42
+ await client.runtime
43
+ .reportActivation({ id, state, ...(errorCode ? { errorCode } : {}) })
44
+ .catch(() => {});
45
+ }
30
46
  export async function superviseOnce(env = process.env, options = {}) {
31
47
  const directory = join(runtimeRoot(env), "runtime");
32
48
  await Promise.resolve((options.resumeLogin ?? resumeDeviceLogin)({ directory }, env)).catch(
@@ -50,11 +66,33 @@ export async function superviseOnce(env = process.env, options = {}) {
50
66
  for await (const entry of profiles) {
51
67
  if (!entry.isDirectory()) continue;
52
68
  let profile,
53
- attempts = 0;
69
+ attempts = 0,
70
+ activationIds = [],
71
+ activationClient;
54
72
  try {
55
73
  profile = await readAgentProfile(entry.name, { environment: env });
56
- if (!(await automaticEnabled(profile))) continue;
74
+ const enabled = await automaticEnabled(profile);
75
+ let activations = [];
76
+ try {
77
+ activationClient = await (options.activationClient ?? createActivationClient)(profile);
78
+ activations = (await activationClient.runtime.activations()).data ?? [];
79
+ activationIds = activations.map((item) => item.id);
80
+ } catch (error) {
81
+ if ([401, 403].includes(error.status)) throw error;
82
+ // An older server or lost network cannot override local stops or grant new access.
83
+ activationClient = undefined;
84
+ }
85
+ if (!enabled) {
86
+ await reportActivations(activationClient, activationIds, "paused");
87
+ continue;
88
+ }
57
89
  const state = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
90
+ if (state.running && state.packageVersion === (options.packageVersion ?? PACKAGE_VERSION))
91
+ await reportActivations(
92
+ activationClient,
93
+ activationIds,
94
+ state.ready ? "ready" : "starting",
95
+ );
58
96
  if (state.running) {
59
97
  if (state.packageVersion === (options.packageVersion ?? PACKAGE_VERSION)) continue;
60
98
  // Finish active work before upgrading. Never overlap two workers or clear journals.
@@ -79,7 +117,9 @@ export async function superviseOnce(env = process.env, options = {}) {
79
117
  attempts = Number.isSafeInteger(health?.attempts)
80
118
  ? Math.max(0, Math.min(health.attempts, 10))
81
119
  : 0;
82
- if (Number.isFinite(health?.retryAt) && health.retryAt > Date.now()) continue;
120
+ const newAcceptance = activationIds.some((id) => !health?.activationIds?.includes(id));
121
+ if (!newAcceptance && Number.isFinite(health?.retryAt) && health.retryAt > Date.now())
122
+ continue;
83
123
  // The run-local journal decides whether any interrupted work can safely resume.
84
124
  const result = await (options.start ?? agentStartMain)(
85
125
  ["--profile", entry.name, "--background", "--json"],
@@ -90,17 +130,35 @@ export async function superviseOnce(env = process.env, options = {}) {
90
130
  },
91
131
  );
92
132
  if (result) throw new Error("START_FAILED");
133
+ const started = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
134
+ await reportActivations(
135
+ activationClient,
136
+ activationIds,
137
+ started.ready ? "ready" : "starting",
138
+ );
93
139
  await privateWrite(join(profile.stateDirectory, "automatic-health.json"), {
94
140
  state: "running",
141
+ activationIds,
95
142
  attempts: 0,
96
143
  retryAt: null,
97
144
  updatedAt: new Date().toISOString(),
98
145
  });
99
146
  results.push(entry.name);
100
- } catch {
147
+ } catch (error) {
148
+ await reportActivations(
149
+ activationClient,
150
+ activationIds,
151
+ "blocked",
152
+ error.code === "READINESS_UNSUPPORTED"
153
+ ? "READINESS_UNSUPPORTED"
154
+ : [401, 403].includes(error.status)
155
+ ? "AUTHORIZATION_REJECTED"
156
+ : "LISTENER_START_FAILED",
157
+ );
101
158
  if (profile)
102
159
  await privateWrite(join(profile.stateDirectory, "automatic-health.json"), {
103
160
  state: "error",
161
+ activationIds,
104
162
  code: "LISTENER_START_FAILED",
105
163
  attempts: attempts + 1,
106
164
  retryAt: Date.now() + Math.min(300000, 5000 * 2 ** attempts),
@@ -20,7 +20,10 @@ export async function runConnectedAgentService(settings, dependencies = {}) {
20
20
  ? AbortSignal.any([settings.signal, controller.signal])
21
21
  : controller.signal;
22
22
  const emit = settings.emit ?? (() => {});
23
- const phases = { messages: "starting", missions: "starting" };
23
+ const phases = {
24
+ messages: "starting",
25
+ missions: settings.connectionOnly ? "waiting" : "starting",
26
+ };
24
27
  let releaseMissions;
25
28
  const messageReady = new Promise((resolve) => {
26
29
  releaseMissions = resolve;
@@ -109,6 +112,7 @@ export async function runConnectedAgentService(settings, dependencies = {}) {
109
112
  guarded(messages),
110
113
  guarded(async () => {
111
114
  // Do not dispatch business work while onboarding is still testing its adapter.
115
+ if (settings.connectionOnly) return;
112
116
  if (settings.verifyReadiness && !(await messageReady)) return;
113
117
  return (dependencies.runMissions ?? runMissionAgentService)(
114
118
  {
@@ -43,6 +43,7 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
43
43
  !pending ||
44
44
  pending.baseUrl !== baseUrl ||
45
45
  pending.requestedAgentId !== env.TOTEM_AGENT_ID ||
46
+ pending.connectionId !== env.TOTEM_CONNECTION_ID ||
46
47
  pending.provider !== (env.TOTEM_PROVIDER ?? "auto") ||
47
48
  pending.redirectUri !== options.binding?.redirectUri ||
48
49
  new Date(pending.expiresAt) <= new Date()
@@ -57,6 +58,7 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
57
58
  deviceName: hostname().slice(0, 100),
58
59
  provider: env.TOTEM_PROVIDER ?? "auto",
59
60
  ...(env.TOTEM_AGENT_ID ? { agentId: env.TOTEM_AGENT_ID } : {}),
61
+ ...(env.TOTEM_CONNECTION_ID ? { connectionId: env.TOTEM_CONNECTION_ID } : {}),
60
62
  ...(options.binding
61
63
  ? {
62
64
  bindingChallenge: options.binding.challenge,
@@ -78,6 +80,7 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
78
80
  ...result,
79
81
  baseUrl,
80
82
  requestedAgentId: env.TOTEM_AGENT_ID,
83
+ connectionId: env.TOTEM_CONNECTION_ID,
81
84
  provider: env.TOTEM_PROVIDER ?? "auto",
82
85
  replace: Boolean(options.replace),
83
86
  ...(options.binding
@@ -86,7 +89,9 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
86
89
  };
87
90
  await privateWrite(join(runtime.directory, "connection-request.json"), pending);
88
91
  }
89
- const url = `${baseUrl}/connect?code=${pending.code}`;
92
+ const url = pending.connectionId
93
+ ? `${baseUrl}/connections/${pending.connectionId}/accept?code=${pending.code}`
94
+ : `${baseUrl}/connect?code=${pending.code}`;
90
95
  if (options.openBrowser !== false) {
91
96
  const command =
92
97
  process.platform === "darwin"
@@ -168,6 +173,7 @@ async function resumeLoginLocked(runtime, env, options) {
168
173
  throw new Error("Totem returned an invalid connection.");
169
174
  staged = {
170
175
  agentId: result.agentId,
176
+ ...(result.connectionId ? { connectionId: result.connectionId } : {}),
171
177
  key: result.key,
172
178
  baseUrl: pending.baseUrl,
173
179
  requestId: pending.id,
@@ -231,6 +237,7 @@ async function resumeLoginLocked(runtime, env, options) {
231
237
  origin(staged.baseUrl),
232
238
  ...(await providerArguments(provider, env)),
233
239
  "--no-start",
240
+ ...(staged.connectionId ? ["--no-mcp"] : []),
234
241
  ...(options.replace || staged.replace ? ["--replace"] : []),
235
242
  "--json",
236
243
  ],
@@ -59,6 +59,7 @@ export async function installationMain(args = [], options = {}) {
59
59
  options: {
60
60
  provider: { type: "string" },
61
61
  "agent-id": { type: "string" },
62
+ "connection-id": { type: "string" },
62
63
  "base-url": { type: "string" },
63
64
  "no-browser": { type: "boolean" },
64
65
  "device-login": { type: "boolean" },
@@ -70,7 +71,7 @@ export async function installationMain(args = [], options = {}) {
70
71
  }).values;
71
72
  if (flags.help) {
72
73
  stdout.write(
73
- "totem login [--provider codex|claude|qwen-local|ollama-local|nim-local|command|http] [--no-browser] [--device-login]\nAuthorize your agent in the initial browser flow. Totem saves the connection privately, starts its listener and tests a controlled reply. Use --device-login explicitly for headless hosts; that fallback requires code comparison.\n",
74
+ "totem install [--agent-id UUID] [--connection-id UUID] [--host-kind local|hosted] [--provider codex|claude|qwen-local|ollama-local|nim-local|command|http] [--no-browser] [--device-login]\nWith --connection-id, prepare the host and use one informed Accept action for peer approval and listener startup. The browser must reach this host's loopback callback (remote hosts require a tunnel). Standalone login uses explicit owner authorization. Headless --device-login uses code comparison and cannot replace the connection-bound Accept flow. Hosted labels an already provisioned, authorized host; it does not provision hosting.\n",
74
75
  );
75
76
  return 0;
76
77
  }
@@ -78,6 +79,7 @@ export async function installationMain(args = [], options = {}) {
78
79
  ...env,
79
80
  ...(flags.provider ? { TOTEM_PROVIDER: flags.provider } : {}),
80
81
  ...(flags["agent-id"] ? { TOTEM_AGENT_ID: flags["agent-id"] } : {}),
82
+ ...(flags["connection-id"] ? { TOTEM_CONNECTION_ID: flags["connection-id"] } : {}),
81
83
  ...(flags["base-url"] ? { TOTEM_BASE_URL: flags["base-url"] } : {}),
82
84
  ...(flags["host-kind"] ? { TOTEM_RUNTIME_HOST_KIND: flags["host-kind"] } : {}),
83
85
  };
@@ -88,6 +90,21 @@ export async function installationMain(args = [], options = {}) {
88
90
  throw new Error(
89
91
  "Choose --host-kind local or hosted. Hosted mode labels an already provisioned, explicitly authorized host; it does not provision one.",
90
92
  );
93
+ if (
94
+ environment.TOTEM_CONNECTION_ID &&
95
+ !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(environment.TOTEM_CONNECTION_ID)
96
+ )
97
+ throw new Error("Choose a valid connection request ID.");
98
+ if (
99
+ environment.TOTEM_CONNECTION_ID &&
100
+ (flags["device-login"] ||
101
+ environment.TOTEM_NETWORK_API_KEY ||
102
+ environment.TOTEM_NETWORK_API_KEY_FILE ||
103
+ flags.reauthorize)
104
+ )
105
+ throw new Error(
106
+ "Prepare connection acceptance with the bound browser installer on this host. Headless and credential replacement use their explicit repair flows.",
107
+ );
91
108
  const runtime = await (options.ensureRuntime ?? ensureInstalledRuntime)(environment);
92
109
  const service = await (options.installService ?? installRuntimeService)(runtime, environment);
93
110
  if (environment.TOTEM_NETWORK_API_KEY || environment.TOTEM_NETWORK_API_KEY_FILE) {
@@ -147,6 +164,21 @@ export async function installationMain(args = [], options = {}) {
147
164
  );
148
165
  return status.ready ? 0 : 1;
149
166
  };
167
+ if (environment.TOTEM_CONNECTION_ID) {
168
+ for (const file of ["connection-ready.json", "connection-request.json"]) {
169
+ const pending = await privateRead(join(runtime.directory, file)).catch((error) => {
170
+ if (error.code !== "ENOENT") throw error;
171
+ });
172
+ if (
173
+ pending &&
174
+ pending.connectionId !== environment.TOTEM_CONNECTION_ID &&
175
+ (file === "connection-ready.json" || Date.parse(pending.expiresAt) > Date.now())
176
+ )
177
+ throw new Error(
178
+ "Another host authorization is pending. Finish or explicitly cancel that setup before preparing this connection; no saved identity was replaced.",
179
+ );
180
+ }
181
+ }
150
182
  const resume = options.resumeLogin ?? resumeDeviceLogin;
151
183
  const loginOptions = { ...options, replace: Boolean(flags.reauthorize) };
152
184
  if (flags.reauthorize) {
@@ -185,7 +217,12 @@ export async function installationMain(args = [], options = {}) {
185
217
  } catch (error) {
186
218
  if (error.code !== "ENOENT") throw error;
187
219
  }
188
- if (saved.length && options.forceLogin !== true && !flags.reauthorize) {
220
+ if (
221
+ saved.length &&
222
+ options.forceLogin !== true &&
223
+ !flags.reauthorize &&
224
+ !environment.TOTEM_CONNECTION_ID
225
+ ) {
189
226
  const results = [];
190
227
  for (const name of saved) results.push({ profile: name, ...(await activate(name)) });
191
228
  return reportActivation({
@@ -215,7 +252,7 @@ export async function installationMain(args = [], options = {}) {
215
252
  stdout.write(
216
253
  flags.json
217
254
  ? JSON.stringify(report) + "\n"
218
- : `Authorize your agent in the installer browser:\n${connection.url}\n${flags["device-login"] ? `Compare this code before approving: ${connection.code.match(/.{1,4}/g).join(" ")}\n` : ""}Waiting for authorization and the controlled listener check.\n`,
255
+ : `${environment.TOTEM_CONNECTION_ID ? "Accept the peer connection and connect this host in one action:" : "Authorize your agent in the installer browser:"}\n${connection.url}\n${flags["device-login"] ? `Compare this code before approving: ${connection.code.match(/.{1,4}/g).join(" ")}\n` : ""}Waiting for authorization and the controlled listener check.\n`,
219
256
  );
220
257
  try {
221
258
  const deadline = Math.min(
@@ -245,7 +282,7 @@ export async function installationMain(args = [], options = {}) {
245
282
  },
246
283
  );
247
284
  const name = environment.TOTEM_AGENT_ID ?? pending?.requestedAgentId;
248
- if (name) {
285
+ if (name && !environment.TOTEM_CONNECTION_ID) {
249
286
  const profile = await (options.readProfile ?? readAgentProfile)(name, {
250
287
  environment,
251
288
  }).catch((error) => {
@@ -266,10 +303,17 @@ export async function installationMain(args = [], options = {}) {
266
303
  connection.close?.();
267
304
  }
268
305
  }
269
- export async function postinstallMain(env = process.env) {
306
+ export async function postinstallMain(env = process.env, options = {}) {
270
307
  if (env.TOTEM_AUTO_START === "0" || env.CI) return 0;
271
308
  try {
272
- return await installationMain([], { env });
309
+ // npm may hide lifecycle output. Prepare reception without opening a separate
310
+ // credential-consent page before the user reaches informed peer acceptance.
311
+ const runtime = await (options.ensureRuntime ?? ensureInstalledRuntime)(env);
312
+ await (options.installService ?? installRuntimeService)(runtime, env);
313
+ (options.stdout ?? process.stdout).write(
314
+ "Totem runtime prepared. Run totem install, or prepare the host from a peer request and use its single Accept action. No new runtime credential has been granted.\n",
315
+ );
316
+ return 0;
273
317
  } catch {
274
318
  process.stdout.write(
275
319
  "Totem is installed. Automatic connection setup needs attention; run totem login to finish it.\n",