totemlayer 0.1.68 → 0.1.75
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 +10 -14
- package/bin/agent-profile.mjs +6 -1
- package/bin/agent-runner.mjs +28 -2
- package/bin/agent-setup.mjs +2 -0
- package/bin/automatic-runtime.mjs +23 -12
- package/bin/codex-chat-bridge.mjs +72 -38
- package/bin/codex-desktop-wake.mjs +47 -20
- package/bin/codex-human-chat-bridge.mjs +57 -21
- package/bin/codex-mission-bridge.mjs +167 -0
- package/bin/collaboration-companion.mjs +2 -0
- package/bin/connected-agent-service.mjs +12 -2
- package/bin/executor-diagnostic.mjs +169 -61
- package/bin/installation.mjs +10 -16
- package/bin/local-inspector.mjs +6 -0
- package/bin/mcp-install.mjs +15 -1
- package/bin/mcp.mjs +45 -1
- package/bin/mission-worker.mjs +62 -39
- package/bin/model-adapter.mjs +30 -1
- package/bin/native/darwin-lifetime +0 -0
- package/bin/native/darwin-lifetime.json +1 -1
- package/bin/network-contract.mjs +125 -0
- package/bin/network-runtime.mjs +367 -315
- package/bin/network.mjs +2 -0
- package/bin/observability.mjs +336 -0
- package/bin/platform-client.mjs +6 -3
- package/bin/plugin-connect.mjs +10 -0
- package/bin/provider-diagnostics.mjs +40 -11
- package/bin/runtime-installation.mjs +15 -1
- package/bin/runtime-lock.mjs +43 -6
- package/bin/sdk/index.d.ts +1 -0
- package/bin/sdk/index.js +1 -1
- package/bin/setup-assets/api-connect.backup.md +129 -0
- package/bin/setup-assets/api-connect.md +129 -0
- package/bin/setup-assets/api-manifest.json +1 -0
- package/bin/setup-assets/manifest.json +1 -0
- package/bin/setup-assets/setup.backup.md +178 -0
- package/bin/setup-assets/setup.md +178 -0
- package/bin/setup-guide.mjs +39 -0
- package/bin/setup-receipts.mjs +181 -0
- package/bin/strict-codex-adapter.mjs +454 -0
- package/bin/strict-installation.mjs +206 -0
- package/bin/strict-setup-jobs.mjs +253 -0
- package/bin/strict-setup.mjs +385 -0
- package/bin/totem.mjs +11 -1
- package/bin/windows-private-path.mjs +89 -0
- package/package.json +4 -3
- package/scripts/prepare.mjs +2 -0
package/bin/network.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
|
|
1
2
|
import { DEFAULT_LISTENER_POLL_MS } from "./sdk/listener.js";
|
|
2
3
|
import { parseArgs, stripVTControlCharacters } from "node:util";
|
|
3
4
|
import { readFile, stat } from "node:fs/promises";
|
|
@@ -210,6 +211,7 @@ export async function networkMain(args) {
|
|
|
210
211
|
if (secret)
|
|
211
212
|
throw new Error("Set only one of TOTEM_NETWORK_API_KEY and TOTEM_NETWORK_API_KEY_FILE.");
|
|
212
213
|
const path = process.env.TOTEM_NETWORK_API_KEY_FILE;
|
|
214
|
+
await assertWindowsPrivatePath(path);
|
|
213
215
|
const info = await stat(path);
|
|
214
216
|
if (
|
|
215
217
|
!info.isFile() ||
|
|
@@ -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
|
+
}
|
package/bin/platform-client.mjs
CHANGED
|
@@ -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" &&
|
|
89
|
-
|
|
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 =
|
|
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 };
|
package/bin/plugin-connect.mjs
CHANGED
|
@@ -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 =
|
|
7
|
+
const captured =
|
|
8
|
+
error.code === "OUTPUT_LIMIT_EXCEEDED" ? {} : (error.privateProviderDiagnostic ?? {});
|
|
8
9
|
const secrets = Object.entries(environment)
|
|
9
|
-
.filter(
|
|
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
|
|
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 {
|
|
35
|
+
} catch {
|
|
36
|
+
/* Successful model output and ordinary stdout never become diagnostics. */
|
|
37
|
+
}
|
|
26
38
|
}
|
|
27
|
-
return {
|
|
28
|
-
|
|
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),
|
|
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(
|
|
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 (
|
|
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) {
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.code !== "ENOENT") throw error;
|
|
74
|
+
}
|
|
46
75
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
|
|
1
2
|
import { execFile, spawn } from "node:child_process";
|
|
2
3
|
import { promisify } from "node:util";
|
|
3
4
|
import { constants } from "node:fs";
|
|
@@ -15,6 +16,7 @@ export function runtimeRoot(env = process.env) {
|
|
|
15
16
|
}
|
|
16
17
|
export async function privateDirectory(path) {
|
|
17
18
|
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
19
|
+
await assertWindowsPrivatePath(path);
|
|
18
20
|
const info = await lstat(path);
|
|
19
21
|
if (
|
|
20
22
|
!info.isDirectory() ||
|
|
@@ -25,6 +27,7 @@ export async function privateDirectory(path) {
|
|
|
25
27
|
throw new Error("The runtime directory must be private and owned by this user.");
|
|
26
28
|
}
|
|
27
29
|
export async function privateRead(path) {
|
|
30
|
+
await assertWindowsPrivatePath(path);
|
|
28
31
|
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
29
32
|
try {
|
|
30
33
|
const info = await handle.stat();
|
|
@@ -50,6 +53,7 @@ export async function privateWrite(path, value) {
|
|
|
50
53
|
0o600,
|
|
51
54
|
);
|
|
52
55
|
try {
|
|
56
|
+
await assertWindowsPrivatePath(temporary);
|
|
53
57
|
await handle.writeFile(JSON.stringify(value));
|
|
54
58
|
await handle.sync();
|
|
55
59
|
} finally {
|
|
@@ -128,6 +132,7 @@ async function installRuntimeUnlocked(env) {
|
|
|
128
132
|
for (const name of ["bin", "package.json", "LICENSE.md", "README.md"])
|
|
129
133
|
await cp(join(packageRoot, name), join(stage, name), { recursive: true, dereference: false });
|
|
130
134
|
const dependencies = [
|
|
135
|
+
{ name: "@modelcontextprotocol/client", from: packageRoot },
|
|
131
136
|
{ name: "zod", from: packageRoot },
|
|
132
137
|
{ name: "undici", from: packageRoot },
|
|
133
138
|
{ name: "@modelcontextprotocol/server", from: packageRoot },
|
|
@@ -187,9 +192,11 @@ import { join, dirname } from 'node:path';
|
|
|
187
192
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
188
193
|
import { spawn } from 'node:child_process';
|
|
189
194
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
195
|
+
import { assertWindowsPrivatePath } from './windows-private-path.mjs';
|
|
190
196
|
const entry = fileURLToPath(import.meta.url);
|
|
191
197
|
const root = dirname(entry);
|
|
192
198
|
const file = join(root, 'current.json');
|
|
199
|
+
await assertWindowsPrivatePath(file);
|
|
193
200
|
const info = await lstat(file);
|
|
194
201
|
if (info.isSymbolicLink() || (process.getuid && info.uid !== process.getuid()) || (process.platform !== 'win32' && (info.mode & 511) !== 384)) throw Error('Unsafe runtime installation');
|
|
195
202
|
const { version } = JSON.parse(await readFile(file, 'utf8'));
|
|
@@ -246,6 +253,10 @@ if (process.env.TOTEM_RUNTIME_ENV_LOADED === entry) {
|
|
|
246
253
|
}
|
|
247
254
|
`;
|
|
248
255
|
for (const [path, contents] of [
|
|
256
|
+
[
|
|
257
|
+
join(runtime, "windows-private-path.mjs"),
|
|
258
|
+
await readFile(new URL("./windows-private-path.mjs", import.meta.url), "utf8"),
|
|
259
|
+
],
|
|
249
260
|
[cli, launcher("totem.mjs")],
|
|
250
261
|
[supervisor, launcher("automatic-runtime.mjs")],
|
|
251
262
|
]) {
|
|
@@ -255,6 +266,7 @@ if (process.env.TOTEM_RUNTIME_ENV_LOADED === entry) {
|
|
|
255
266
|
0o600,
|
|
256
267
|
);
|
|
257
268
|
try {
|
|
269
|
+
await assertWindowsPrivatePath(path);
|
|
258
270
|
await handle.writeFile(contents);
|
|
259
271
|
} finally {
|
|
260
272
|
await handle.close();
|
|
@@ -375,7 +387,8 @@ export async function installRuntimeService(runtime, env = process.env, options
|
|
|
375
387
|
status.pid > 1 &&
|
|
376
388
|
owner?.pid === status.pid &&
|
|
377
389
|
owner.identity &&
|
|
378
|
-
(await (options.processIdentity ?? processIdentity)(status.pid)) ===
|
|
390
|
+
(await (options.processIdentity ?? processIdentity)(status.pid, owner.identity)) ===
|
|
391
|
+
owner.identity
|
|
379
392
|
) {
|
|
380
393
|
const invocation =
|
|
381
394
|
platform === "win32"
|
|
@@ -433,6 +446,7 @@ export async function installRuntimeService(runtime, env = process.env, options
|
|
|
433
446
|
"TOTEM_INSPECTOR_MODEL",
|
|
434
447
|
"TOTEM_INSPECTOR_TIMEOUT_MS",
|
|
435
448
|
"TOTEM_PROVIDER",
|
|
449
|
+
"TOTEM_HOST_LOCK_ROOT",
|
|
436
450
|
"TOTEM_RUNTIME_HOST_KIND",
|
|
437
451
|
"TOTEM_MODEL",
|
|
438
452
|
"TOTEM_BASE_URL",
|
package/bin/runtime-lock.mjs
CHANGED
|
@@ -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
|
-
|
|
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",
|
package/bin/sdk/index.d.ts
CHANGED
|
@@ -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>;
|