totemlayer 0.1.21 → 0.1.25

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.
@@ -5,6 +5,11 @@ import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { ensureInstalledRuntime, installRuntimeService } from "./runtime-installation.mjs";
7
7
  import { beginDeviceLogin } from "./device-login.mjs";
8
+ import { resumeDeviceLogin } from "./device-login.mjs";
9
+ import { beginBoundLogin } from "./bound-login.mjs";
10
+ import { readAgentProfile } from "./agent-profile.mjs";
11
+ import { readAgentRuntimeStatus } from "./agent-runner.mjs";
12
+ import { setTimeout as sleep } from "node:timers/promises";
8
13
  import { detectInstalledProvider, providerArguments } from "./provider-detection.mjs";
9
14
 
10
15
  export async function activateConnection(profile, env = process.env, options = {}) {
@@ -19,7 +24,7 @@ export async function activateConnection(profile, env = process.env, options = {
19
24
  if (!enabled) return { state: "paused", ...service, entry: runtime.cli };
20
25
  const { verifyExecutorReady } = await import("./executor-readiness.mjs");
21
26
  try {
22
- await (options.verifyExecutor ?? verifyExecutorReady)(profile, env);
27
+ if (profile.codexThreadId) await (options.verifyExecutor ?? verifyExecutorReady)(profile, env);
23
28
  } catch (error) {
24
29
  return {
25
30
  state: "needs_attention",
@@ -31,42 +36,18 @@ export async function activateConnection(profile, env = process.env, options = {
31
36
  };
32
37
  }
33
38
  const { agentStartMain } = await import("./agent-runner.mjs");
34
- const started = await (options.start ?? agentStartMain)(
39
+ const result = await (options.start ?? agentStartMain)(
35
40
  ["--profile", profile.profileName, "--background", "--json"],
36
41
  { env, automatic: true, stdout: { write() {} } },
37
42
  );
38
- if (started !== 0)
39
- return {
40
- state: "needs_attention",
41
- ...service,
42
- entry: runtime.cli,
43
- code: "WORKER_START_FAILED",
44
- };
45
- const { readAgentRuntimeStatus } = await import("./agent-runner.mjs");
46
- const state = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
47
- if (
48
- state.running &&
49
- state.phase === "blocked" &&
50
- ["INSPECTION_BLOCKED", "INSPECTION_LIMIT", "INSPECTION_UNSUPPORTED_FILE"].includes(state.blockerCode)
51
- )
52
- return {
53
- state: "running",
54
- ...service,
55
- entry: runtime.cli,
56
- phase: state.phase,
57
- quarantinedMessage: true,
58
- code: state.blockerCode,
59
- message: "The listener is running. An unsafe message was quarantined; other messages can still be processed.",
60
- };
61
- if (!state.running || !["connected", "working", "waiting"].includes(state.phase))
62
- return {
63
- state: "needs_attention",
64
- ...service,
65
- entry: runtime.cli,
66
- code: state.blockerCode ?? "WORKER_NOT_READY",
67
- phase: state.phase,
68
- };
69
- return { state: "running", ...service, entry: runtime.cli };
43
+ if (result) throw new Error("The listener could not start. Check totem status for this profile.");
44
+ const status = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
45
+ return {
46
+ state: status.ready ? "ready" : "needs_attention",
47
+ ready: Boolean(status.ready),
48
+ ...service,
49
+ entry: runtime.cli,
50
+ };
70
51
  }
71
52
 
72
53
  export async function installationMain(args = [], options = {}) {
@@ -78,15 +59,19 @@ export async function installationMain(args = [], options = {}) {
78
59
  options: {
79
60
  provider: { type: "string" },
80
61
  "agent-id": { type: "string" },
62
+ "connection-id": { type: "string" },
81
63
  "base-url": { type: "string" },
82
64
  "no-browser": { type: "boolean" },
65
+ "device-login": { type: "boolean" },
66
+ reauthorize: { type: "boolean" },
67
+ "host-kind": { type: "string" },
83
68
  help: { type: "boolean", short: "h" },
84
69
  json: { type: "boolean" },
85
70
  },
86
71
  }).values;
87
72
  if (flags.help) {
88
73
  stdout.write(
89
- "totem login [--provider codex|claude|qwen-local|ollama-local|nim-local|command|http] [--no-browser]\nConnect this installation once. Totem securely saves its connection and automatically runs and restarts the listener.\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",
90
75
  );
91
76
  return 0;
92
77
  }
@@ -94,8 +79,32 @@ export async function installationMain(args = [], options = {}) {
94
79
  ...env,
95
80
  ...(flags.provider ? { TOTEM_PROVIDER: flags.provider } : {}),
96
81
  ...(flags["agent-id"] ? { TOTEM_AGENT_ID: flags["agent-id"] } : {}),
82
+ ...(flags["connection-id"] ? { TOTEM_CONNECTION_ID: flags["connection-id"] } : {}),
97
83
  ...(flags["base-url"] ? { TOTEM_BASE_URL: flags["base-url"] } : {}),
84
+ ...(flags["host-kind"] ? { TOTEM_RUNTIME_HOST_KIND: flags["host-kind"] } : {}),
98
85
  };
86
+ if (
87
+ environment.TOTEM_RUNTIME_HOST_KIND &&
88
+ !["local", "hosted"].includes(environment.TOTEM_RUNTIME_HOST_KIND)
89
+ )
90
+ throw new Error(
91
+ "Choose --host-kind local or hosted. Hosted mode labels an already provisioned, explicitly authorized host; it does not provision one.",
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
+ );
99
108
  const runtime = await (options.ensureRuntime ?? ensureInstalledRuntime)(environment);
100
109
  const service = await (options.installService ?? installRuntimeService)(runtime, environment);
101
110
  if (environment.TOTEM_NETWORK_API_KEY || environment.TOTEM_NETWORK_API_KEY_FILE) {
@@ -129,6 +138,7 @@ export async function installationMain(args = [], options = {}) {
129
138
  [
130
139
  "--profile",
131
140
  environment.TOTEM_AGENT_ID ?? "default",
141
+ ...(flags.reauthorize ? ["--replace"] : []),
132
142
  ...(environment.TOTEM_AGENT_ID ? ["--agent-id", environment.TOTEM_AGENT_ID] : []),
133
143
  ...(await providerArguments(provider, environment)),
134
144
  ...(flags.json ? ["--json"] : []),
@@ -140,61 +150,94 @@ export async function installationMain(args = [], options = {}) {
140
150
  },
141
151
  );
142
152
  }
143
- try {
144
- await privateRead(join(runtime.directory, "connection-ready.json"));
153
+ const activate = async (name) => {
154
+ const profile = await (options.readProfile ?? readAgentProfile)(name, { environment });
155
+ return (options.activateConnection ?? activateConnection)(profile, environment, options);
156
+ };
157
+ const reportActivation = (status) => {
145
158
  stdout.write(
146
- "Your Totem connection is saved privately. Sign in to your AI client; the listener connects automatically.\n",
159
+ flags.json
160
+ ? JSON.stringify(status) + "\n"
161
+ : status.ready
162
+ ? "Ready. The listener passed its controlled receive, wake and reply check. This host must remain awake and online.\n"
163
+ : `Runtime ${status.state}. Run totem status for diagnostics; an installed or paused listener is not ready.\n`,
147
164
  );
148
- return 0;
149
- } catch (error) {
150
- if (error.code !== "ENOENT") throw error;
165
+ return status.ready ? 0 : 1;
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
+ }
182
+ const resume = options.resumeLogin ?? resumeDeviceLogin;
183
+ const loginOptions = { ...options, replace: Boolean(flags.reauthorize) };
184
+ if (flags.reauthorize) {
185
+ const reset = await resume(runtime, environment, { ...loginOptions, reset: true });
186
+ if (reset?.state !== "reset") {
187
+ stdout.write(
188
+ "Another installer is saving this connection. Wait for it to finish before reauthorizing.\n",
189
+ );
190
+ return 1;
191
+ }
192
+ }
193
+ const staged = flags.reauthorize
194
+ ? undefined
195
+ : await resume(runtime, environment, loginOptions).catch((error) => {
196
+ if (error.message?.includes("authorization expired")) return undefined;
197
+ throw error;
198
+ });
199
+ if (staged?.state === "connected") return reportActivation(await activate(staged.profile));
200
+ if (staged?.state === "executor_needed") {
201
+ stdout.write(
202
+ "Authorization is saved privately. Sign in to an installed Codex or Claude client, or configure a supported local/custom adapter, then retry totem login. The listener is not ready.\n",
203
+ );
204
+ return 1;
151
205
  }
152
206
  // An upgrade reuses private profiles and starts their existing connections.
153
207
  const { opendir } = await import("node:fs/promises");
154
- let saved = false;
155
- const savedProfiles = [];
208
+ const saved = [];
156
209
  try {
157
210
  const profiles = await opendir(join(runtime.root, "connections"));
158
211
  for await (const entry of profiles)
159
212
  if (
160
213
  entry.isDirectory() &&
161
214
  (!environment.TOTEM_AGENT_ID || entry.name === environment.TOTEM_AGENT_ID)
162
- ) {
163
- saved = true;
164
- savedProfiles.push(entry.name);
165
- }
215
+ )
216
+ saved.push(entry.name);
166
217
  } catch (error) {
167
218
  if (error.code !== "ENOENT") throw error;
168
219
  }
169
- if (saved && options.forceLogin !== true) {
170
- const { readAgentProfile } = await import("./agent-profile.mjs");
220
+ if (
221
+ saved.length &&
222
+ options.forceLogin !== true &&
223
+ !flags.reauthorize &&
224
+ !environment.TOTEM_CONNECTION_ID
225
+ ) {
171
226
  const results = [];
172
- for (const name of savedProfiles) {
173
- const profile = await readAgentProfile(name, { environment });
174
- results.push({
175
- profile: name,
176
- ...(await (options.activateConnection ?? activateConnection)(profile, environment)),
177
- });
178
- }
179
- const ready = results.every((result) => ["running", "paused"].includes(result.state));
180
- const report = {
227
+ for (const name of saved) results.push({ profile: name, ...(await activate(name)) });
228
+ return reportActivation({
181
229
  installed: true,
182
- connection: "saved",
183
- automatic: true,
184
- ready,
230
+ ready: results.every((r) => r.ready),
231
+ state: results.every((r) => r.ready) ? "ready" : "needs_attention",
185
232
  profiles: results,
186
233
  service,
187
- };
188
- stdout.write(
189
- flags.json
190
- ? JSON.stringify(report) + "\n"
191
- : ready
192
- ? "Totem's automatic listener reuses your saved connections; startup checks passed.\n"
193
- : "Totem is installed, but an agent failed its startup check. Run totem status to inspect it.\n",
194
- );
195
- return ready ? 0 : 1;
234
+ });
196
235
  }
197
- const connection = await (options.beginLogin ?? beginDeviceLogin)(runtime, environment, {
236
+ const connection = await (
237
+ options.beginLogin ?? (flags["device-login"] ? beginDeviceLogin : beginBoundLogin)
238
+ )(runtime, environment, {
239
+ ...options,
240
+ replace: Boolean(flags.reauthorize),
198
241
  openBrowser: !flags["no-browser"] && environment.TOTEM_NO_BROWSER !== "1",
199
242
  });
200
243
  const report = {
@@ -202,19 +245,75 @@ export async function installationMain(args = [], options = {}) {
202
245
  connection: "sign_in_required",
203
246
  automatic: true,
204
247
  service,
205
- ...connection,
248
+ url: connection.url,
249
+ ...(flags["device-login"] ? { code: connection.code } : {}),
250
+ ready: false,
206
251
  };
207
252
  stdout.write(
208
253
  flags.json
209
254
  ? JSON.stringify(report) + "\n"
210
- : `Totem is installed. Connect your agent once at:\n${connection.url}\nConnection code: ${connection.code.match(/.{1,4}/g).join(" ")}\nThe listener starts automatically after you connect.\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`,
211
256
  );
212
- return 0;
257
+ try {
258
+ const deadline = Math.min(
259
+ Date.parse(connection.expiresAt),
260
+ Date.now() + (options.loginTimeoutMs ?? 600000),
261
+ );
262
+ while (Date.now() < deadline && !options.signal?.aborted) {
263
+ const result = await resume(runtime, environment, loginOptions);
264
+ if (result?.state === "connected") return reportActivation(await activate(result.profile));
265
+ const receipt = await privateRead(join(runtime.directory, "connection-last.json")).catch(
266
+ (error) => {
267
+ if (error.code !== "ENOENT") throw error;
268
+ },
269
+ );
270
+ if (receipt?.requestId === connection.id && receipt.profile)
271
+ return reportActivation(await activate(receipt.profile));
272
+ if (result?.state === "executor_needed") {
273
+ stdout.write(
274
+ "Authorization saved. Sign in to an installed AI client or configure a supported adapter, then run totem login again. Runtime is not ready.\n",
275
+ );
276
+ return 1;
277
+ }
278
+ // The supervisor may have saved the profile while this installer was polling.
279
+ const pending = await privateRead(join(runtime.directory, "connection-request.json")).catch(
280
+ (error) => {
281
+ if (error.code !== "ENOENT") throw error;
282
+ },
283
+ );
284
+ const name = environment.TOTEM_AGENT_ID ?? pending?.requestedAgentId;
285
+ if (name && !environment.TOTEM_CONNECTION_ID) {
286
+ const profile = await (options.readProfile ?? readAgentProfile)(name, {
287
+ environment,
288
+ }).catch((error) => {
289
+ if (error.code !== "AGENT_PROFILE_NOT_FOUND") throw error;
290
+ });
291
+ if (profile)
292
+ return reportActivation(
293
+ await (options.activateConnection ?? activateConnection)(profile, environment, options),
294
+ );
295
+ }
296
+ await (options.sleep ?? sleep)(3000, undefined, { signal: options.signal });
297
+ }
298
+ stdout.write(
299
+ "Setup is not ready yet. Run totem login again to resume securely; inspect totem status for any saved profile.\n",
300
+ );
301
+ return 1;
302
+ } finally {
303
+ connection.close?.();
304
+ }
213
305
  }
214
- export async function postinstallMain(env = process.env) {
306
+ export async function postinstallMain(env = process.env, options = {}) {
215
307
  if (env.TOTEM_AUTO_START === "0" || env.CI) return 0;
216
308
  try {
217
- 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;
218
317
  } catch {
219
318
  process.stdout.write(
220
319
  "Totem is installed. Automatic connection setup needs attention; run totem login to finish it.\n",
@@ -7,6 +7,7 @@ import { setTimeout as sleep } from "node:timers/promises";
7
7
 
8
8
  import {
9
9
  acquireMissionAgentSession,
10
+ assertMissionAgentSession,
10
11
  ensurePrivateRuntimeDirectory,
11
12
  missionAgentOrigin,
12
13
  readPrivateRuntimeJson,
@@ -26,6 +27,7 @@ const STAGES = new Set([
26
27
  "code",
27
28
  "review",
28
29
  "handoff",
30
+ "workspace_report",
29
31
  "infrastructure_deploy",
30
32
  "infrastructure_verify",
31
33
  ]);
@@ -213,10 +215,14 @@ export async function runMissionAgentService(
213
215
  emit({ type: "stopped" });
214
216
  return;
215
217
  }
216
- session = await acquireMissionAgentSession(
217
- { baseUrl: origin, agentId: agent.id, signal },
218
- dependencies.session,
219
- );
218
+ if (dependencies.ownedSession) {
219
+ assertMissionAgentSession(dependencies.ownedSession, origin, agent.id);
220
+ session = dependencies.ownedSession;
221
+ } else
222
+ session = await acquireMissionAgentSession(
223
+ { baseUrl: origin, agentId: agent.id, signal },
224
+ dependencies.session,
225
+ );
220
226
  } catch (error) {
221
227
  if (!signal?.aborted) throw error;
222
228
  emit({ type: "stopped" });
@@ -273,6 +279,7 @@ export async function runMissionAgentService(
273
279
  return false;
274
280
  };
275
281
  const bindingFor = async (mission, kind, recovery = false) => {
282
+ if (kind === "workspace_report") return null;
276
283
  if (!mission.repository && !REPOSITORY_STAGES.has(kind)) return null;
277
284
  const root = mission.repository && bindings.get(mission.repository.url);
278
285
  if (!root) {
@@ -317,6 +324,17 @@ export async function runMissionAgentService(
317
324
  ? await getInfrastructureProvider()
318
325
  : undefined,
319
326
  emit(event) {
327
+ if (
328
+ [
329
+ "briefing_updated",
330
+ "file_overlap",
331
+ "session_visibility_unavailable",
332
+ "briefing_unavailable",
333
+ ].includes(event.type)
334
+ ) {
335
+ emit({ ...event, missionId });
336
+ return;
337
+ }
320
338
  const type =
321
339
  event.type === "work_started"
322
340
  ? "working"
@@ -473,7 +491,7 @@ export async function runMissionAgentService(
473
491
  }
474
492
  } finally {
475
493
  await infrastructureProvider?.close();
476
- await session.release();
494
+ if (!dependencies.ownedSession) await session.release();
477
495
  report({ type: "stopped" });
478
496
  }
479
497
  }