totemlayer 0.1.68 → 0.1.74

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.
Files changed (42) hide show
  1. package/README.md +9 -13
  2. package/bin/agent-runner.mjs +23 -1
  3. package/bin/automatic-runtime.mjs +23 -12
  4. package/bin/codex-chat-bridge.mjs +72 -38
  5. package/bin/codex-desktop-wake.mjs +47 -20
  6. package/bin/codex-human-chat-bridge.mjs +57 -21
  7. package/bin/codex-mission-bridge.mjs +167 -0
  8. package/bin/connected-agent-service.mjs +12 -2
  9. package/bin/executor-diagnostic.mjs +169 -61
  10. package/bin/installation.mjs +8 -16
  11. package/bin/local-inspector.mjs +6 -0
  12. package/bin/mcp-install.mjs +12 -1
  13. package/bin/mcp.mjs +43 -1
  14. package/bin/mission-worker.mjs +62 -39
  15. package/bin/model-adapter.mjs +30 -1
  16. package/bin/native/darwin-lifetime +0 -0
  17. package/bin/native/darwin-lifetime.json +1 -1
  18. package/bin/network-contract.mjs +125 -0
  19. package/bin/network-runtime.mjs +367 -315
  20. package/bin/observability.mjs +336 -0
  21. package/bin/platform-client.mjs +6 -3
  22. package/bin/plugin-connect.mjs +10 -0
  23. package/bin/provider-diagnostics.mjs +40 -11
  24. package/bin/runtime-installation.mjs +4 -1
  25. package/bin/runtime-lock.mjs +43 -6
  26. package/bin/sdk/index.d.ts +1 -0
  27. package/bin/sdk/index.js +1 -1
  28. package/bin/setup-assets/api-connect.backup.md +129 -0
  29. package/bin/setup-assets/api-connect.md +129 -0
  30. package/bin/setup-assets/api-manifest.json +1 -0
  31. package/bin/setup-assets/manifest.json +1 -0
  32. package/bin/setup-assets/setup.backup.md +178 -0
  33. package/bin/setup-assets/setup.md +178 -0
  34. package/bin/setup-guide.mjs +39 -0
  35. package/bin/setup-receipts.mjs +181 -0
  36. package/bin/strict-codex-adapter.mjs +456 -0
  37. package/bin/strict-installation.mjs +206 -0
  38. package/bin/strict-setup-jobs.mjs +253 -0
  39. package/bin/strict-setup.mjs +385 -0
  40. package/bin/totem.mjs +11 -1
  41. package/package.json +4 -3
  42. package/scripts/prepare.mjs +2 -0
@@ -0,0 +1,336 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { randomUUID } from "node:crypto";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { readPrivateRuntimeJson } from "./mission-agent-session.mjs";
6
+
7
+ const context = new AsyncLocalStorage();
8
+ const capabilities = new WeakMap();
9
+ const MAX_TEXT = 24000;
10
+ const failureCode = (error, fallback) =>
11
+ typeof error?.code === "string" && /^[A-Z0-9_]{1,100}$/.test(error.code) ? error.code : fallback;
12
+
13
+ /** Export data-plane text only. Never pass request headers or provider configuration here. */
14
+ export function redactObservation(value, depth = 0) {
15
+ 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);
33
+ if (Array.isArray(value))
34
+ return value.slice(0, 80).map((item) => redactObservation(item, depth + 1));
35
+ if (value && typeof value === "object")
36
+ return Object.fromEntries(
37
+ Object.entries(value)
38
+ .slice(0, 80)
39
+ .map(([key, item]) => [
40
+ key,
41
+ /(?:authorization|password|secret|api[_-]?(?:key|token)|access[_-]?token|AWS_SESSION_TOKEN|leaseToken)/i.test(
42
+ key,
43
+ )
44
+ ? "[redacted]"
45
+ : redactObservation(item, depth + 1),
46
+ ]),
47
+ );
48
+ return value;
49
+ }
50
+
51
+ export function providerUsage(result) {
52
+ const u = result?.usage ?? result ?? {};
53
+ const count = (value) => (Number.isSafeInteger(value) && value >= 0 ? value : undefined);
54
+ const input = count(u.inputTokens ?? u.input_tokens ?? u.prompt_tokens ?? u.prompt_eval_count);
55
+ const output = count(u.outputTokens ?? u.output_tokens ?? u.completion_tokens ?? u.eval_count);
56
+ const cached = count(
57
+ u.cached_input_tokens ??
58
+ u.cacheReadInputTokens ??
59
+ u.cache_read_input_tokens ??
60
+ u.input_tokens_details?.cached_tokens ??
61
+ u.prompt_tokens_details?.cached_tokens,
62
+ );
63
+ const inclusiveCache =
64
+ u.cached_input_tokens ??
65
+ u.input_tokens_details?.cached_tokens ??
66
+ u.prompt_tokens_details?.cached_tokens;
67
+ const write = count(u.cacheWriteInputTokens ?? u.cache_creation_input_tokens);
68
+ return Object.fromEntries(
69
+ Object.entries({
70
+ input,
71
+ output,
72
+ ...(cached === undefined
73
+ ? {}
74
+ : {
75
+ input: input === undefined ? undefined : Math.max(0, input - (inclusiveCache ?? 0)),
76
+ cache_read_input_tokens: cached,
77
+ }),
78
+ cache_creation_input_tokens: write,
79
+ }).filter(([, v]) => v !== undefined),
80
+ );
81
+ }
82
+
83
+ export function providerCost(result) {
84
+ const value = result?.total_cost_usd ?? result?.cost_usd ?? result?.usage?.cost;
85
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
86
+ ? { total: value }
87
+ : undefined;
88
+ }
89
+
90
+ async function telemetryDeadline(operation) {
91
+ let timer;
92
+ try {
93
+ return await Promise.race([
94
+ operation(),
95
+ new Promise((_, reject) => {
96
+ timer = setTimeout(() => reject(Error("TELEMETRY_TIMEOUT")), 1500);
97
+ }),
98
+ ]);
99
+ } finally {
100
+ clearTimeout(timer);
101
+ }
102
+ }
103
+
104
+ async function enabled(agentId, client) {
105
+ if (process.env.TOTEM_OBSERVABILITY_ENABLED !== undefined)
106
+ return process.env.TOTEM_OBSERVABILITY_ENABLED === "true";
107
+ try {
108
+ const settings = await readPrivateRuntimeJson(
109
+ join(process.env.TOTEM_CONFIG_DIR ?? join(homedir(), ".totem"), "observability.json"),
110
+ );
111
+ return settings.enabled === true && settings.agentIds?.includes(agentId);
112
+ } catch (error) {
113
+ if (error.code !== "ENOENT") {
114
+ console.error("Totem observability configuration unavailable");
115
+ return false;
116
+ }
117
+ if (!client?.observability?.status) return false;
118
+ const cached = capabilities.get(client);
119
+ if (cached?.expires > Date.now()) return cached.enabled;
120
+ let active = false;
121
+ try {
122
+ active = (await telemetryDeadline(() => client.observability.status())).enabled === true;
123
+ } catch {
124
+ /* Older collectors and opted-out owners send no content. */
125
+ }
126
+ capabilities.set(client, { enabled: active, expires: Date.now() + 60000 });
127
+ return active;
128
+ }
129
+ }
130
+
131
+ export async function submitObservation(client, observation) {
132
+ try {
133
+ if (!client?.observability?.record) throw Error("REPORTER_UNAVAILABLE");
134
+ await telemetryDeadline(() =>
135
+ client.observability.record({
136
+ observations: observation.map((record) => {
137
+ const safe = redactObservation(record);
138
+ for (const key of ["input", "output"]) {
139
+ const serialized = JSON.stringify(safe[key]);
140
+ if (serialized?.length > 20000) {
141
+ safe[key] = { truncated: true, preview: serialized.slice(0, 20000) };
142
+ safe.metadata = { ...safe.metadata, contentTruncated: true };
143
+ }
144
+ }
145
+ return safe;
146
+ }),
147
+ }),
148
+ );
149
+ } catch {
150
+ // Observability never changes a tool authorization or turns a completed model call into a retry.
151
+ console.error("Totem observability delivery unavailable");
152
+ }
153
+ }
154
+
155
+ export function observationOutcome({ output, error }) {
156
+ const root = context.getStore();
157
+ if (!root) return;
158
+ if (output !== undefined) root.output = output;
159
+ if (error) root.error = failureCode(error, "AGENT_FAILED");
160
+ }
161
+
162
+ export async function withAgentObservation(
163
+ {
164
+ client,
165
+ agentId,
166
+ sessionId,
167
+ traceId = randomUUID(),
168
+ input,
169
+ name = "agent.conversation",
170
+ metadata = {},
171
+ output: outputValue,
172
+ },
173
+ operation,
174
+ ) {
175
+ const parent = context.getStore();
176
+ if (parent?.agentId === agentId) {
177
+ const value = await operation();
178
+ if (typeof value === "string") parent.output = value;
179
+ return value;
180
+ }
181
+ if (!(await enabled(agentId, client))) return operation();
182
+ const root = {
183
+ client,
184
+ traceId,
185
+ sessionId: sessionId ?? traceId,
186
+ id: traceId + ":" + name,
187
+ agentId,
188
+ records: [],
189
+ };
190
+ const startedAt = new Date().toISOString();
191
+ return context.run(root, async () => {
192
+ let output, error;
193
+ try {
194
+ output = await operation();
195
+ return output;
196
+ } catch (failure) {
197
+ error = failureCode(failure, "AGENT_FAILED");
198
+ throw failure;
199
+ } finally {
200
+ await submitObservation(client, [
201
+ {
202
+ traceId,
203
+ id: root.id,
204
+ sessionId: root.sessionId,
205
+ name,
206
+ type: "agent",
207
+ startedAt,
208
+ endedAt: new Date().toISOString(),
209
+ input,
210
+ output: outputValue === undefined ? (root.output ?? output) : outputValue,
211
+ ...((error ?? root.error) ? { error: error ?? root.error } : {}),
212
+ metadata,
213
+ },
214
+ ...root.records.map((record, index) =>
215
+ index < 4
216
+ ? record
217
+ : {
218
+ ...record,
219
+ input: undefined,
220
+ output: record.type === "guardrail" ? record.output : undefined,
221
+ metadata: { ...record.metadata, contentTruncated: true },
222
+ },
223
+ ),
224
+ ]);
225
+ }
226
+ });
227
+ }
228
+
229
+ export async function observeGeneration(
230
+ { name = "model.generate", model, provider, input, metrics },
231
+ operation,
232
+ ) {
233
+ const root = context.getStore();
234
+ if (!root) return operation();
235
+ const startedAt = new Date().toISOString();
236
+ let value, error;
237
+ try {
238
+ value = await operation();
239
+ return value;
240
+ } catch (failure) {
241
+ error = failureCode(failure, "MODEL_FAILED");
242
+ throw failure;
243
+ } finally {
244
+ const measured = value ?? metrics?.();
245
+ if (root.records.length < 40)
246
+ root.records.push({
247
+ traceId: root.traceId,
248
+ id: randomUUID(),
249
+ parentId: root.id,
250
+ sessionId: root.sessionId,
251
+ type: "generation",
252
+ name,
253
+ model,
254
+ startedAt,
255
+ endedAt: new Date().toISOString(),
256
+ input,
257
+ output: value?.output ?? value?.choices?.[0]?.message ?? value?.message ?? value,
258
+ usage: providerUsage(measured),
259
+ cost: providerCost(measured),
260
+ ...(error ? { error } : {}),
261
+ metadata: {
262
+ provider,
263
+ usageSource: Object.keys(providerUsage(measured)).length ? "provider" : "unavailable",
264
+ costSource: providerCost(measured)
265
+ ? (measured?.cost_source ?? "provider")
266
+ : model && model !== "default"
267
+ ? "langfuse-model-definition"
268
+ : "unavailable",
269
+ },
270
+ });
271
+ }
272
+ }
273
+
274
+ const observedInspectors = new WeakSet();
275
+ export function observeInspector(inspector) {
276
+ if (observedInspectors.has(inspector)) return inspector;
277
+ async function inspect(kind, value, operation) {
278
+ const root = context.getStore();
279
+ if (!root) return operation(() => {});
280
+ const startedAt = new Date().toISOString();
281
+ let recorded = false;
282
+ function complete(error) {
283
+ if (recorded) return;
284
+ recorded = true;
285
+ if (root.records.length < 40)
286
+ root.records.push({
287
+ traceId: root.traceId,
288
+ id: randomUUID(),
289
+ parentId: root.id,
290
+ sessionId: root.sessionId,
291
+ name: `inspection.${kind}`,
292
+ type: "guardrail",
293
+ startedAt,
294
+ endedAt: new Date().toISOString(),
295
+ input: value,
296
+ output: {
297
+ decision: error === "INSPECTION_BLOCKED" ? "block" : error ? "unavailable" : "allow",
298
+ code: error ?? "ALLOWED",
299
+ detector: inspector.mode ?? "rules",
300
+ },
301
+ ...(error ? { error } : {}),
302
+ metadata: { detector: inspector.mode ?? "rules" },
303
+ });
304
+ }
305
+ try {
306
+ const result = await operation(() => complete());
307
+ complete();
308
+ return result;
309
+ } catch (failure) {
310
+ complete(failureCode(failure, "INSPECTION_UNAVAILABLE"));
311
+ throw failure;
312
+ }
313
+ }
314
+ const observed = Object.freeze({
315
+ ...inspector,
316
+ assertAllowed: (kind, value, options) =>
317
+ inspect(kind, value, () => inspector.assertAllowed(kind, value, options)),
318
+ runAction: (value, operation, options) =>
319
+ inspect("action", value, (allowed) =>
320
+ inspector.runAction(
321
+ value,
322
+ (...args) => {
323
+ allowed();
324
+ return operation(...args);
325
+ },
326
+ options,
327
+ ),
328
+ ),
329
+ inspectFile: (name, mediaType, bytes, options) =>
330
+ inspect("file", { name, mediaType, bytes: bytes.byteLength }, () =>
331
+ inspector.inspectFile(name, mediaType, bytes, options),
332
+ ),
333
+ });
334
+ observedInspectors.add(observed);
335
+ return observed;
336
+ }
@@ -85,11 +85,14 @@ export function installedClientInvocation(command, args, environment = process.e
85
85
  if (platform !== "win32") {
86
86
  const isExecutable = options.isExecutable ?? executableFile;
87
87
  if (
88
- platform === "darwin" && command === "codex" &&
89
- !(environment.PATH ?? "/usr/bin:/bin").split(delimiter)
88
+ platform === "darwin" &&
89
+ command === "codex" &&
90
+ !(environment.PATH ?? "/usr/bin:/bin")
91
+ .split(delimiter)
90
92
  .some((directory) => isExecutable(join(directory || ".", command)))
91
93
  ) {
92
- const bundled = "/Applications/ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex";
94
+ const bundled =
95
+ "/Applications/ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex";
93
96
  if (isExecutable(bundled)) return { command: bundled, args };
94
97
  }
95
98
  return { command, args };
@@ -39,6 +39,8 @@ export function pluginConnectionOptions(args = [], environment = process.env) {
39
39
  "agent-id": { type: "string" },
40
40
  environment: { type: "string" },
41
41
  "base-url": { type: "string" },
42
+ "device-login": { type: "boolean" },
43
+ "no-browser": { type: "boolean" },
42
44
  help: { type: "boolean", short: "h" },
43
45
  },
44
46
  }).values;
@@ -64,6 +66,8 @@ export function pluginConnectionOptions(args = [], environment = process.env) {
64
66
  agentId,
65
67
  environment: flags.environment,
66
68
  baseUrl,
69
+ ...(flags["device-login"] ? { deviceLogin: true } : {}),
70
+ ...(flags["no-browser"] ? { noBrowser: true } : {}),
67
71
  configDirectory: join(runtimeRoot(environment), "plugins", flags.provider, selection),
68
72
  };
69
73
  }
@@ -74,6 +78,7 @@ export function createPluginConnector(selection, options = {}) {
74
78
  const env = {
75
79
  ...inherited,
76
80
  TOTEM_CONFIG_DIR: selection.configDirectory,
81
+ TOTEM_HOST_LOCK_ROOT: runtimeRoot(inherited),
77
82
  TOTEM_BASE_URL: selection.baseUrl,
78
83
  TOTEM_PROVIDER: selection.provider,
79
84
  };
@@ -355,6 +360,8 @@ export function createPluginConnector(selection, options = {}) {
355
360
  "--base-url",
356
361
  selection.baseUrl,
357
362
  "--json",
363
+ ...(selection.deviceLogin ? ["--device-login"] : []),
364
+ ...(selection.noBrowser ? ["--no-browser"] : []),
358
365
  ...(selection.agentId ? ["--agent-id", selection.agentId] : []),
359
366
  ],
360
367
  {
@@ -396,6 +403,9 @@ export function createPluginConnector(selection, options = {}) {
396
403
  ready: false,
397
404
  persistent: false,
398
405
  signInUrl: url.toString(),
406
+ ...(selection.deviceLogin && /^[A-Z2-9]{12}$/.test(report.code ?? "")
407
+ ? { approvalCode: report.code }
408
+ : {}),
399
409
  };
400
410
  }
401
411
  } catch {
@@ -4,14 +4,24 @@ import { runtimeErrorCode } from "./runtime-diagnostics.mjs";
4
4
 
5
5
  export function providerDiagnostic(error, { environment = process.env, provider, model } = {}) {
6
6
  // A stream truncated at its output bound could contain only part of a secret.
7
- const captured = error.code === "OUTPUT_LIMIT_EXCEEDED" ? {} : error.privateProviderDiagnostic ?? {};
7
+ const captured =
8
+ error.code === "OUTPUT_LIMIT_EXCEEDED" ? {} : (error.privateProviderDiagnostic ?? {});
8
9
  const secrets = Object.entries(environment)
9
- .filter(([name, value]) => /secret|token|password|api.?key|db.?pass/i.test(name) && typeof value === "string" && value.length >= 4)
10
+ .filter(
11
+ ([name, value]) =>
12
+ /secret|token|password|api.?key|db.?pass/i.test(name) &&
13
+ typeof value === "string" &&
14
+ value.length >= 4,
15
+ )
10
16
  .map(([, value]) => value);
11
- const redact = value => {
17
+ const redact = (value) => {
12
18
  let text = String(value ?? "");
13
19
  for (const secret of secrets) text = text.split(secret).join("[REDACTED]");
14
- return text.replace(/\b(?:tn_(?:live|test)_[A-Za-z0-9_-]+|sk-[A-Za-z0-9_-]+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)\b/g, "[REDACTED]")
20
+ return text
21
+ .replace(
22
+ /\b(?:tn_(?:live|test)_[A-Za-z0-9_-]+|sk-[A-Za-z0-9_-]+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)\b/g,
23
+ "[REDACTED]",
24
+ )
15
25
  .replace(/(Bearer\s+)[^\s"']+/gi, "$1[REDACTED]")
16
26
  .replace(/(https?:\/\/)[^\s\/@]+@/gi, "$1[REDACTED]@")
17
27
  .slice(-6000);
@@ -22,25 +32,44 @@ export function providerDiagnostic(error, { environment = process.env, provider,
22
32
  const event = JSON.parse(line);
23
33
  if (["error", "turn.failed"].includes(event.type))
24
34
  errors.push(redact(event.error?.message ?? event.message ?? event.error));
25
- } catch { /* Successful model output and ordinary stdout never become diagnostics. */ }
35
+ } catch {
36
+ /* Successful model output and ordinary stdout never become diagnostics. */
37
+ }
26
38
  }
27
- return { version: 1, checkedAt: new Date().toISOString(), provider, model,
28
- code: runtimeErrorCode(error), command: redact(captured.command),
39
+ return {
40
+ version: 1,
41
+ checkedAt: new Date().toISOString(),
42
+ provider,
43
+ model,
44
+ code: runtimeErrorCode(error),
45
+ command: redact(captured.command),
29
46
  ...(Number.isInteger(captured.exitCode) ? { exitCode: captured.exitCode } : {}),
30
- stderr: redact(captured.stderr), errors: errors.slice(-8) };
47
+ stderr: redact(captured.stderr),
48
+ errors: errors.slice(-8),
49
+ };
31
50
  }
32
51
 
33
52
  export async function saveProviderDiagnostic(directory, error, context) {
34
53
  if (typeof directory !== "string" || !isAbsolute(directory)) return;
35
- await privateWrite(join(directory, "last-provider-failure.json"), providerDiagnostic(error, context));
54
+ await privateWrite(
55
+ join(directory, "last-provider-failure.json"),
56
+ providerDiagnostic(error, context),
57
+ );
36
58
  }
37
59
 
38
60
  export async function readProviderDiagnostic(profile) {
39
61
  try {
40
62
  const value = await privateRead(join(profile.stateDirectory, "last-provider-failure.json"));
41
63
  const model = profile.client && profile.provider === "qwen-local" ? "default" : profile.model;
42
- if (value.version !== 1 || value.provider !== (profile.client ?? profile.provider) || value.model !== model || !Number.isFinite(Date.parse(value.checkedAt)))
64
+ if (
65
+ value.version !== 1 ||
66
+ value.provider !== (profile.client ?? profile.provider) ||
67
+ value.model !== model ||
68
+ !Number.isFinite(Date.parse(value.checkedAt))
69
+ )
43
70
  return undefined;
44
71
  return value;
45
- } catch (error) { if (error.code !== "ENOENT") throw error; }
72
+ } catch (error) {
73
+ if (error.code !== "ENOENT") throw error;
74
+ }
46
75
  }
@@ -128,6 +128,7 @@ async function installRuntimeUnlocked(env) {
128
128
  for (const name of ["bin", "package.json", "LICENSE.md", "README.md"])
129
129
  await cp(join(packageRoot, name), join(stage, name), { recursive: true, dereference: false });
130
130
  const dependencies = [
131
+ { name: "@modelcontextprotocol/client", from: packageRoot },
131
132
  { name: "zod", from: packageRoot },
132
133
  { name: "undici", from: packageRoot },
133
134
  { name: "@modelcontextprotocol/server", from: packageRoot },
@@ -375,7 +376,8 @@ export async function installRuntimeService(runtime, env = process.env, options
375
376
  status.pid > 1 &&
376
377
  owner?.pid === status.pid &&
377
378
  owner.identity &&
378
- (await (options.processIdentity ?? processIdentity)(status.pid)) === owner.identity
379
+ (await (options.processIdentity ?? processIdentity)(status.pid, owner.identity)) ===
380
+ owner.identity
379
381
  ) {
380
382
  const invocation =
381
383
  platform === "win32"
@@ -433,6 +435,7 @@ export async function installRuntimeService(runtime, env = process.env, options
433
435
  "TOTEM_INSPECTOR_MODEL",
434
436
  "TOTEM_INSPECTOR_TIMEOUT_MS",
435
437
  "TOTEM_PROVIDER",
438
+ "TOTEM_HOST_LOCK_ROOT",
436
439
  "TOTEM_RUNTIME_HOST_KIND",
437
440
  "TOTEM_MODEL",
438
441
  "TOTEM_BASE_URL",
@@ -1,13 +1,13 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { lstat, rename, readdir, unlink, rmdir, rm } from "node:fs/promises";
3
+ import { lstat, rename, readdir, unlink, rmdir, rm, readFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
- import { randomUUID } from "node:crypto";
5
+ import { randomUUID, createHash } from "node:crypto";
6
6
  import { powershellInvocation } from "./platform-client.mjs";
7
7
  import { privateDirectory, privateRead, privateWrite } from "./runtime-installation.mjs";
8
8
 
9
9
  const execute = promisify(execFile);
10
- export async function processIdentity(pid) {
10
+ export async function processIdentity(pid, previousIdentity, options = {}) {
11
11
  if (!Number.isSafeInteger(pid) || pid <= 1) return null;
12
12
  try {
13
13
  process.kill(pid, 0);
@@ -17,14 +17,51 @@ export async function processIdentity(pid) {
17
17
  }
18
18
  // PID reuse across reboot must not make an abandoned lock permanent. A failed
19
19
  // process inspection is uncertainty, never evidence that another owner died.
20
+ const platform = options.platform ?? process.platform;
21
+ if (
22
+ platform === "linux" &&
23
+ (!previousIdentity || previousIdentity.startsWith("linux-proc-v1:"))
24
+ ) {
25
+ const read = options.readFile ?? readFile;
26
+ const statPath = `/proc/${pid}/stat`;
27
+ let stat;
28
+ try {
29
+ stat = await read(statPath, "utf8");
30
+ } catch (error) {
31
+ if (error.code === "ENOENT") return null;
32
+ throw error;
33
+ }
34
+ // comm is parenthesized and may contain spaces or closing parentheses.
35
+ const startTime = (value) => {
36
+ if (!value.startsWith(`${pid} (`)) throw new Error("Invalid Linux process identity.");
37
+ const fields = value
38
+ .slice(value.lastIndexOf(")") + 2)
39
+ .trim()
40
+ .split(/\s+/);
41
+ if (!/^[A-Za-z]$/.test(fields[0] ?? "") || !/^\d{1,20}$/.test(fields[19] ?? ""))
42
+ throw new Error("Invalid Linux process identity.");
43
+ return fields[19];
44
+ };
45
+ const started = startTime(stat);
46
+ const boot = (await read("/proc/sys/kernel/random/boot_id", "utf8")).trim();
47
+ if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(boot))
48
+ throw new Error("Cannot verify the Linux boot identity.");
49
+ const command = await read(`/proc/${pid}/cmdline`);
50
+ if (!command.length || command.length > 2 * 1024 * 1024)
51
+ throw new Error("Cannot verify the Linux process command.");
52
+ // Re-read after inspecting the command to reject a PID reused during the read.
53
+ if (startTime(await read(statPath, "utf8")) !== started)
54
+ throw new Error("Linux process identity changed during inspection.");
55
+ return `linux-proc-v1:${boot}:${pid}:${started}:${createHash("sha256").update(command).digest("hex")}`;
56
+ }
20
57
  const invocation =
21
- process.platform === "win32"
58
+ platform === "win32"
22
59
  ? powershellInvocation(
23
60
  `$p=Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}';if(!$p){exit 1};$p|Select-Object ProcessId,CreationDate,CommandLine|ConvertTo-Json -Compress`,
24
61
  process.env,
25
62
  )
26
63
  : { command: "ps", args: ["-p", String(pid), "-o", "lstart=", "-o", "args="] };
27
- const result = await execute(invocation.command, invocation.args, {
64
+ const result = await (options.execute ?? execute)(invocation.command, invocation.args, {
28
65
  encoding: "utf8",
29
66
  timeout: 3000,
30
67
  maxBuffer: 8192,
@@ -95,7 +132,7 @@ export async function acquireSupervisorLock(directory, options = {}) {
95
132
  if (Date.now() - snapshot.mtimeMs < (options.emptyLockGraceMs ?? 30000))
96
133
  throw new Error("Another supervisor is starting. Wait before retrying.");
97
134
  } else {
98
- const actual = await identity(current.owner.pid);
135
+ const actual = await identity(current.owner.pid, current.owner.identity);
99
136
  if (actual && (!current.owner.identity || current.owner.identity === actual))
100
137
  throw Object.assign(new Error("The Totem supervisor is already running."), {
101
138
  code: "SUPERVISOR_ALREADY_RUNNING",
@@ -16,6 +16,7 @@ export declare class Totem {
16
16
  identityAgentId: string;
17
17
  }>;
18
18
  };
19
+ observability: { status: () => Promise<{enabled: boolean}>; record: (observation: Record<string, unknown>) => Promise<{accepted: boolean}> };
19
20
  memory: {
20
21
  search: (query?: AgentMemorySearch, options?: RequestOptions) => Promise<AgentMemoryHistory>;
21
22
  get: (options?: RequestOptions) => Promise<AgentMemory>;