totemlayer 0.1.6 → 0.1.11

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.
@@ -1,10 +1,11 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- import { stat } from "node:fs/promises";
4
- import { resolve } from "node:path";
3
+ import { stat, lstat } from "node:fs/promises";
4
+ import { resolve, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
7
  import { installedClientInvocation, localClientEnvironment } from "./platform-client.mjs";
8
+ import { runtimeRoot } from "./runtime-installation.mjs";
8
9
 
9
10
  const DEFAULT_ORIGIN = "https://totemlayer.com";
10
11
  const DEFAULT_NAME = "totem-local";
@@ -131,19 +132,28 @@ export async function installTotemMcp(args, environment = process.env, runtime =
131
132
  ];
132
133
  const addArgs =
133
134
  options.client === "codex"
134
- ? ["mcp", "add", options.name, ...envArgs, "--", process.execPath, SERVER_ENTRY, "mcp"]
135
+ ? [
136
+ "mcp",
137
+ "add",
138
+ options.name,
139
+ ...envArgs,
140
+ "--",
141
+ process.execPath,
142
+ runtime.serverEntry ?? SERVER_ENTRY,
143
+ "mcp",
144
+ ]
135
145
  : [
136
146
  "mcp",
137
147
  "add",
148
+ options.name,
138
149
  ...envArgs,
139
150
  "--scope",
140
151
  "user",
141
152
  "--transport",
142
153
  "stdio",
143
- options.name,
144
154
  "--",
145
155
  process.execPath,
146
- SERVER_ENTRY,
156
+ runtime.serverEntry ?? SERVER_ENTRY,
147
157
  "mcp",
148
158
  ];
149
159
  const added = runClient(command, addArgs, environment, runtime);
@@ -169,7 +179,26 @@ export function managedMcpName(profileName) {
169
179
  : `totem_${profileName.slice(0, 24)}_${createHash("sha256").update(profileName).digest("hex").slice(0, 32)}`;
170
180
  }
171
181
 
172
- function managedArgs(profile, name) {
182
+ async function installedServerEntry(profile, environment) {
183
+ if (!profile.stateDirectory) return SERVER_ENTRY;
184
+ const path = join(resolve(profile.stateDirectory, "../../../.."), "runtime", "totem.mjs");
185
+ try {
186
+ const info = await lstat(path);
187
+ if (
188
+ info.isFile() &&
189
+ !info.isSymbolicLink() &&
190
+ info.nlink === 1 &&
191
+ (!process.getuid || info.uid === process.getuid()) &&
192
+ (process.platform === "win32" || (info.mode & 0o777) === 0o600)
193
+ )
194
+ return path;
195
+ } catch {
196
+ /* Connections created without automatic installation retain their entry. */
197
+ }
198
+ return SERVER_ENTRY;
199
+ }
200
+
201
+ function managedArgs(profile, name, entry = SERVER_ENTRY) {
173
202
  const variables = [
174
203
  "--env",
175
204
  `TOTEM_BASE_URL=${profile.baseUrl}`,
@@ -177,24 +206,24 @@ function managedArgs(profile, name) {
177
206
  `TOTEM_NETWORK_API_KEY_FILE=${profile.credentialFile}`,
178
207
  ];
179
208
  return profile.client === "codex"
180
- ? ["mcp", "add", name, ...variables, "--", process.execPath, SERVER_ENTRY, "mcp"]
209
+ ? ["mcp", "add", name, ...variables, "--", process.execPath, entry, "mcp"]
181
210
  : [
182
211
  "mcp",
183
212
  "add",
213
+ name,
184
214
  ...variables,
185
215
  "--scope",
186
216
  "user",
187
217
  "--transport",
188
218
  "stdio",
189
- name,
190
219
  "--",
191
220
  process.execPath,
192
- SERVER_ENTRY,
221
+ entry,
193
222
  "mcp",
194
223
  ];
195
224
  }
196
225
 
197
- function matchesManagedEntry(entry, profile, name) {
226
+ function matchesManagedEntry(entry, profile, name, serverEntry = SERVER_ENTRY) {
198
227
  const transport = entry?.transport;
199
228
  const expectedEnvironment = {
200
229
  TOTEM_BASE_URL: profile.baseUrl,
@@ -209,7 +238,7 @@ function matchesManagedEntry(entry, profile, name) {
209
238
  ) &&
210
239
  transport?.type === "stdio" &&
211
240
  transport.command === process.execPath &&
212
- JSON.stringify(transport.args) === JSON.stringify([SERVER_ENTRY, "mcp"]) &&
241
+ JSON.stringify(transport.args) === JSON.stringify([serverEntry, "mcp"]) &&
213
242
  transport.cwd == null &&
214
243
  (transport.env_vars == null ||
215
244
  (Array.isArray(transport.env_vars) && transport.env_vars.length === 0)) &&
@@ -279,10 +308,11 @@ function reconnectAdvice(profile, name) {
279
308
  }
280
309
 
281
310
  export async function inspectManagedTotemMcp(profile, environment = process.env, runtime = {}) {
311
+ const serverEntry = await installedServerEntry(profile, environment);
282
312
  const inspected = await inspectEntry(profile, localClientEnvironment(environment), runtime);
283
313
  if (
284
314
  inspected.state === "present" &&
285
- matchesManagedEntry(inspected.entry, profile, inspected.name)
315
+ matchesManagedEntry(inspected.entry, profile, inspected.name, serverEntry)
286
316
  )
287
317
  return {
288
318
  name: inspected.name,
@@ -308,6 +338,7 @@ export async function installManagedTotemMcp(
308
338
  environment = process.env,
309
339
  runtime = {},
310
340
  ) {
341
+ const serverEntry = await installedServerEntry(profile, environment);
311
342
  if (!["codex", "claude"].includes(profile.client))
312
343
  fail("Choose an existing AI app for MCP setup.");
313
344
  try {
@@ -337,11 +368,14 @@ export async function installManagedTotemMcp(
337
368
  name,
338
369
  ],
339
370
  safeEnvironment,
340
- runtime,
371
+ { ...runtime, serverEntry },
341
372
  );
342
373
  if (profile.client === "codex") {
343
374
  const after = await inspectEntry(profile, safeEnvironment, runtime);
344
- if (after.state !== "present" || !matchesManagedEntry(after.entry, profile, name))
375
+ if (
376
+ after.state !== "present" ||
377
+ !matchesManagedEntry(after.entry, profile, name, serverEntry)
378
+ )
345
379
  return {
346
380
  name,
347
381
  state: "needs_attention",
@@ -361,7 +395,10 @@ export async function installManagedTotemMcp(
361
395
  };
362
396
  }
363
397
  }
364
- if (inspected.state === "present" && matchesManagedEntry(inspected.entry, profile, name))
398
+ if (
399
+ inspected.state === "present" &&
400
+ matchesManagedEntry(inspected.entry, profile, name, serverEntry)
401
+ )
365
402
  return {
366
403
  name,
367
404
  state: "configured_unverified",
@@ -371,7 +408,21 @@ export async function installManagedTotemMcp(
371
408
  previousProfile?.profileName === profile.profileName &&
372
409
  previousProfile.client === profile.client &&
373
410
  inspected.state === "present" &&
374
- matchesManagedEntry(inspected.entry, previousProfile, name);
411
+ matchesManagedEntry(
412
+ inspected.entry,
413
+ previousProfile,
414
+ name,
415
+ inspected.entry?.transport?.args?.[0],
416
+ ) &&
417
+ (inspected.entry.transport.args[0] === SERVER_ENTRY ||
418
+ new RegExp(
419
+ `^${runtimeRoot(environment)
420
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
421
+ .replaceAll(
422
+ "\\\\",
423
+ "[\\\\/]",
424
+ )}[\\\\/]runtime-packages[\\\\/]\\d+\\.\\d+\\.\\d+[\\\\/]node_modules[\\\\/]totemlayer[\\\\/]bin[\\\\/]totem\\.mjs$`,
425
+ ).test(inspected.entry.transport.args[0]));
375
426
  if (!ownsPrevious)
376
427
  return {
377
428
  name,
@@ -380,10 +431,15 @@ export async function installManagedTotemMcp(
380
431
  };
381
432
  // Codex add can update this exact named entry. No remove occurs: unsupported client
382
433
  // versions fail safely, and an exact read-back is required before reporting an update.
383
- const updated = await managedCall(profile, managedArgs(profile, name), safeEnvironment, runtime);
434
+ const updated = await managedCall(
435
+ profile,
436
+ managedArgs(profile, name, serverEntry),
437
+ safeEnvironment,
438
+ runtime,
439
+ );
384
440
  if (updated.status === 0) {
385
441
  const after = await inspectEntry(profile, safeEnvironment, runtime);
386
- if (after.state === "present" && matchesManagedEntry(after.entry, profile, name))
442
+ if (after.state === "present" && matchesManagedEntry(after.entry, profile, name, serverEntry))
387
443
  return {
388
444
  name,
389
445
  state: "updated_unverified",
package/bin/mcp.mjs CHANGED
@@ -148,6 +148,61 @@ export function createTotemMcpServer(config) {
148
148
  () => client.agent(),
149
149
  );
150
150
 
151
+ register(
152
+ server,
153
+ "totem_list_missions",
154
+ {
155
+ title: "List assigned missions",
156
+ description:
157
+ "Discover durable missions this authenticated agent can participate in. Offline agents must connect their own runtime to execute work.",
158
+ inputSchema: z.object({}),
159
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
160
+ },
161
+ () => client.missions.list(),
162
+ );
163
+
164
+ register(
165
+ server,
166
+ "totem_get_mission",
167
+ {
168
+ title: "Inspect a mission",
169
+ description:
170
+ "Read the mission team, dependencies, owner-approved infrastructure scope, results, and receipts before delegating work.",
171
+ inputSchema: z.object({ missionId: z.uuid() }),
172
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
173
+ },
174
+ ({ missionId }) => client.missions.get(missionId),
175
+ );
176
+
177
+ register(
178
+ server,
179
+ "totem_delegate_mission_task",
180
+ {
181
+ title: "Delegate a mission task",
182
+ description:
183
+ "The mission coordinator assigns a child task to a consented member. Infrastructure deployment requires the selected executor and an approved review dependency; verification depends on a completed deployment. This does not grant permissions or wake an offline agent.",
184
+ inputSchema: z.object({
185
+ missionId: z.uuid(),
186
+ ownerMemberId: z.uuid(),
187
+ parentWorkItemId: z.uuid(),
188
+ dependsOnIds: z.array(z.uuid()).min(1).max(40),
189
+ kind: z.enum([
190
+ "engineering",
191
+ "code",
192
+ "review",
193
+ "infrastructure_deploy",
194
+ "infrastructure_verify",
195
+ ]),
196
+ objective: z.string().min(1).max(10000),
197
+ requiredOutput: z.string().min(1).max(10000),
198
+ idempotencyKey: z.string().min(1).max(128),
199
+ }),
200
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
201
+ },
202
+ ({ missionId, kind, idempotencyKey, ...input }) =>
203
+ client.missions.assign(missionId, { ...input, metadata: { kind } }, { idempotencyKey }),
204
+ );
205
+
151
206
  if (localRepositoryTools)
152
207
  register(
153
208
  server,
@@ -1,3 +1,4 @@
1
+ import { createLocalStaticProvider } from "./mission-infrastructure.mjs";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
3
4
  import { homedir } from "node:os";
@@ -18,14 +19,34 @@ import {
18
19
  import { repositoryIdFromRemote } from "./repository.mjs";
19
20
 
20
21
  const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
21
- const STAGES = new Set(["proposal", "engineering", "decision", "code", "review", "handoff"]);
22
- const REPOSITORY_STAGES = new Set(["engineering", "code", "review"]);
22
+ const STAGES = new Set([
23
+ "proposal",
24
+ "engineering",
25
+ "decision",
26
+ "code",
27
+ "review",
28
+ "handoff",
29
+ "infrastructure_deploy",
30
+ "infrastructure_verify",
31
+ ]);
32
+ const REPOSITORY_STAGES = new Set([
33
+ "engineering",
34
+ "code",
35
+ "review",
36
+ "infrastructure_deploy",
37
+ "infrastructure_verify",
38
+ ]);
23
39
  const TERMINAL = new Set(["completed", "canceled", "failed"]);
24
40
  const RECOVERABLE = new Set([
25
41
  "ready",
26
42
  "result_settling",
27
43
  "local_test_failure_settling",
28
44
  "claiming",
45
+ "infra_source",
46
+ "infra_source_dispatched",
47
+ "infra_source_settling",
48
+ "infra_reserved",
49
+ "infra_dispatched",
29
50
  ]);
30
51
  const CHECKPOINT_BYTES = 2 * 1024 * 1024;
31
52
 
@@ -129,6 +150,7 @@ export async function runMissionAgentService(
129
150
  nimUrl,
130
151
  ollamaUrl,
131
152
  reasoning = false,
153
+ adapterSettings = {},
132
154
  stateDirectory = join(homedir(), ".totem", "missions"),
133
155
  pollMs = 5000,
134
156
  signal,
@@ -210,11 +232,21 @@ export async function runMissionAgentService(
210
232
  };
211
233
  const workerStateDirectory = join(resolve(stateDirectory), "mission-agents", session.key);
212
234
  const directory = join(workerStateDirectory, agent.id);
235
+ let infrastructureProvider;
213
236
  try {
214
237
  await ensurePrivateRuntimeDirectory(resolve(stateDirectory));
215
238
  await ensurePrivateRuntimeDirectory(join(resolve(stateDirectory), "mission-agents"));
216
239
  await ensurePrivateRuntimeDirectory(workerStateDirectory);
217
240
  await ensurePrivateRuntimeDirectory(directory);
241
+ const getInfrastructureProvider = async () => {
242
+ infrastructureProvider ??= await createLocalStaticProvider({
243
+ directory: join(workerStateDirectory, "local-static"),
244
+ });
245
+ return infrastructureProvider;
246
+ };
247
+ // Restore persisted releases on startup without repeating their deployment operations.
248
+ if ((await readdir(workerStateDirectory)).includes("local-static"))
249
+ await getInfrastructureProvider();
218
250
  const checkpointPath = join(directory, `discovery-${session.key}.json`);
219
251
  const state = await loadCheckpoint(checkpointPath, origin, agent.id);
220
252
  const persist = () => saveCheckpoint(checkpointPath, state);
@@ -276,9 +308,14 @@ export async function runMissionAgentService(
276
308
  nimUrl,
277
309
  ollamaUrl,
278
310
  reasoning,
311
+ adapterSettings,
279
312
  signal,
280
313
  candidateWorkItem: workItem,
281
314
  recoveryOnly,
315
+ infrastructureProvider:
316
+ recoveryOnly || workItem?.metadata?.kind?.startsWith("infrastructure_")
317
+ ? await getInfrastructureProvider()
318
+ : undefined,
282
319
  emit(event) {
283
320
  const type =
284
321
  event.type === "work_started"
@@ -435,6 +472,7 @@ export async function runMissionAgentService(
435
472
  }
436
473
  }
437
474
  } finally {
475
+ await infrastructureProvider?.close();
438
476
  await session.release();
439
477
  report({ type: "stopped" });
440
478
  }