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.
- package/README.md +9 -13
- package/bin/agent-runner.mjs +23 -1
- 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/connected-agent-service.mjs +12 -2
- package/bin/executor-diagnostic.mjs +169 -61
- package/bin/installation.mjs +8 -16
- package/bin/local-inspector.mjs +6 -0
- package/bin/mcp-install.mjs +12 -1
- package/bin/mcp.mjs +43 -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/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 +4 -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 +456 -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/package.json +4 -3
- package/scripts/prepare.mjs +2 -0
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ To configure execution later, rerun setup with `--provider` and the adapter's se
|
|
|
27
27
|
|
|
28
28
|
## Connect with automatic listener setup
|
|
29
29
|
|
|
30
|
-
Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.
|
|
30
|
+
Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.74.tgz`, `totem`, `connect`, `--provider`, and `codex` (or `claude` for Claude Code). The host runs the connection itself; the owner does not run a separate listener command. Node.js 22.14 or newer and the selected signed-in client must be available.
|
|
31
31
|
|
|
32
32
|
During browser sign-in, `totem_get_connection_status` reports progress. Normal tools appear after the approved identity is pinned privately. Codex verifies identity, startup and the saved chat callback before receiving work; reconnect preserves bindings and pauses. Missing capabilities leave work queued. A remote HTTP connector cannot install local software.
|
|
33
33
|
|
|
@@ -45,7 +45,7 @@ For a hosted connection, add these settings to your client's MCP configuration:
|
|
|
45
45
|
For a local stdio connection, install the CLI with Node.js 22.14 or newer:
|
|
46
46
|
|
|
47
47
|
```sh
|
|
48
|
-
npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.
|
|
48
|
+
npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.74.tgz
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
Save your agent's connection key in a private file. Protect it with mode 0600 on macOS/Linux or your user's filesystem ACL on Windows. Configure these fields in your client's MCP settings:
|
|
@@ -61,23 +61,19 @@ Reload the client if needed, discover the available tools, and call `totem_get_i
|
|
|
61
61
|
|
|
62
62
|
## Automatic listener setup
|
|
63
63
|
|
|
64
|
-
Run the
|
|
64
|
+
Run inside the intended existing native Codex chat, keeping its current model:
|
|
65
65
|
|
|
66
66
|
```sh
|
|
67
|
-
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.
|
|
67
|
+
npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.74.tgz totem install
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
Version 0.1.74 uses the strict installer. Read [setup.md](https://totemlayer.com/setup.md) in full first. The strict controller verifies the bundled full guide, exact native session/model and authenticated MCP identity, preserves pauses and existing permissions, and reuses the same connection. It configures exactly one supervised five-second receiver and requires observed recovery, native wake and separate peer-message and website-owner-chat reply receipts. Saved configuration or a readiness echo cannot mark setup complete. Missing or unsupported capabilities return an incomplete result.
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
The supervised setup job returns promptly so its bound chat can receive checks. Run `totem install --status` to inspect progress. It resumes on startup/failure and reports into that same chat. Do not start a second installer or change models to finish setup. The current built-in strict local adapter supports Codex's durable existing session API; other apps use the native MCP/receiver path in setup.md and must report their actual limitations.
|
|
73
73
|
|
|
74
|
-
For
|
|
74
|
+
For approval from any phone browser, append `--device-login --no-browser`. The owner compares the displayed code and approves; the receiver stays on the agent's actual available host. iOS/Android chat apps use their own supported custom MCP flow. A mobile browser or remote MCP URL cannot install a desktop service or guarantee background wake in a suspended app. API fallback requires an observed missing custom-MCP capability, or the owner's explicit API choice, and the same receiving checks.
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.68.tgz totem login --device-login --no-browser
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Open the printed link on your own device and compare the connection code. Preserve existing MCP entries; inspect conflicts before replacing them. If an executor is unavailable, the saved connection remains available while you complete runtime setup.
|
|
76
|
+
Package lifecycle installation only prints guidance. It never starts a receiver or requests authorization before the strict controller knows the intended model/session and pause state. Explicit `totem login` authenticates only; it is not strict setup completion. Connection-only/no-start is an explicit owner opt-out and remains receiving-incomplete.
|
|
81
77
|
|
|
82
78
|
## Check or pause your listener
|
|
83
79
|
|
|
@@ -106,7 +102,7 @@ Local and hosted MCP tool arguments may contain at most 10,000 combined object m
|
|
|
106
102
|
|
|
107
103
|
Compatible clients show native owner-review notifications for pending requests. Accepting a notification only opens Totem's authenticated review; the owner still approves or denies the exact action. Already-approved Full access requests do not prompt. Other clients receive logs and the same review link.
|
|
108
104
|
|
|
109
|
-
A hosted HTTP connection cannot install a service or wake a closed task. Supported local installers prepare the bound listener; reconnect repairs only enabled listeners and preserves credentials, pauses and explicit chat bindings. Codex requires its public app-server session API, queue command and a callback from the exact bound chat.
|
|
105
|
+
A hosted HTTP connection cannot install a service or wake a closed task. Supported local installers prepare the bound listener; reconnect repairs only enabled listeners and preserves credentials, pauses and explicit chat bindings. Codex requires its public app-server session API, queue command and a callback from the exact bound chat. The strict installer requires the intended existing chat; other explicitly selected setup paths can create a dedicated inbox when supported. A queued message alone does not prove readiness. Claude MCP tools work, but automatic interactive Claude chat creation reports HARNESS_UNSUPPORTED; no text-only responder is substituted. Verify closed-app behavior on the actual awake, online host.
|
|
110
106
|
|
|
111
107
|
Follow the owner's instructions before sending messages, sharing code, or delegating Missions. For collaboration, agree on the repository, base commit, separate branches, file scopes, and acceptance criteria. Report the resulting commit and validation evidence.
|
|
112
108
|
|
package/bin/agent-runner.mjs
CHANGED
|
@@ -268,6 +268,13 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
|
|
|
268
268
|
...(typeof record.packageVersion === "string" ? { packageVersion: record.packageVersion } : {}),
|
|
269
269
|
updatedAt: record.updatedAt,
|
|
270
270
|
...(record.listenerHeartbeatAt ? { listenerHeartbeatAt: record.listenerHeartbeatAt } : {}),
|
|
271
|
+
...(Number.isFinite(record.receiveCheckAt) ? { receiveCheckAt: record.receiveCheckAt } : {}),
|
|
272
|
+
...(Number.isFinite(record.measuredCheckSeconds)
|
|
273
|
+
? { measuredCheckSeconds: record.measuredCheckSeconds }
|
|
274
|
+
: {}),
|
|
275
|
+
...(Number.isFinite(record.receiverIntervalSeconds)
|
|
276
|
+
? { receiverIntervalSeconds: record.receiverIntervalSeconds }
|
|
277
|
+
: {}),
|
|
271
278
|
...(/^[A-Z][A-Z0-9_]{0,79}$/.test(record.errorCode ?? "")
|
|
272
279
|
? { errorCode: record.errorCode }
|
|
273
280
|
: {}),
|
|
@@ -509,7 +516,11 @@ export async function runAgentSession(profile, options = {}) {
|
|
|
509
516
|
verifyReadiness: true,
|
|
510
517
|
connectionOnly: Boolean(identity.listenerConnectionId),
|
|
511
518
|
hostKind: (options.env ?? process.env).TOTEM_RUNTIME_HOST_KIND ?? "local",
|
|
512
|
-
sessionLockDirectory: join(
|
|
519
|
+
sessionLockDirectory: join(
|
|
520
|
+
(options.env ?? process.env).TOTEM_HOST_LOCK_ROOT ??
|
|
521
|
+
runtimeRoot(options.env ?? process.env),
|
|
522
|
+
"agent-runtime-locks",
|
|
523
|
+
),
|
|
513
524
|
profileName: profile.profileName,
|
|
514
525
|
profileConfigDirectory: dirname(dirname(dirname(dirname(profile.stateDirectory)))),
|
|
515
526
|
address: profile.address,
|
|
@@ -544,6 +555,17 @@ export async function runAgentSession(profile, options = {}) {
|
|
|
544
555
|
signal: controller.signal,
|
|
545
556
|
emit: (event) => {
|
|
546
557
|
if (!acceptingEvents) return;
|
|
558
|
+
if (event.type === "receive_check") {
|
|
559
|
+
const at = event.data?.checkedAt;
|
|
560
|
+
if (Number.isFinite(at) && at > (status.receiveCheckAt ?? 0)) {
|
|
561
|
+
if (Number.isFinite(status.receiveCheckAt))
|
|
562
|
+
status.measuredCheckSeconds = (at - status.receiveCheckAt) / 1000;
|
|
563
|
+
status.receiveCheckAt = at;
|
|
564
|
+
status.receiverIntervalSeconds = event.data.intervalSeconds;
|
|
565
|
+
void persist();
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
547
569
|
const state = eventState(event);
|
|
548
570
|
if (state) emit(state);
|
|
549
571
|
},
|
|
@@ -51,6 +51,8 @@ async function reportActivations(client, ids, state, errorCode) {
|
|
|
51
51
|
.catch(() => {});
|
|
52
52
|
}
|
|
53
53
|
export async function superviseOnce(env = process.env, options = {}) {
|
|
54
|
+
const { resumeStrictSetupJobs } = await import("./strict-setup-jobs.mjs");
|
|
55
|
+
await (options.resumeSetupJobs ?? resumeStrictSetupJobs)(env);
|
|
54
56
|
const directory = join(runtimeRoot(env), "runtime");
|
|
55
57
|
await Promise.resolve((options.resumeLogin ?? resumeDeviceLogin)({ directory }, env)).catch(
|
|
56
58
|
async (error) => {
|
|
@@ -94,6 +96,13 @@ export async function superviseOnce(env = process.env, options = {}) {
|
|
|
94
96
|
await reportActivations(activationClient, activationIds, "paused");
|
|
95
97
|
continue;
|
|
96
98
|
}
|
|
99
|
+
const healthPath = join(profile.stateDirectory, "automatic-health.json");
|
|
100
|
+
const health = await privateRead(healthPath).catch((error) => {
|
|
101
|
+
if (error.code !== "ENOENT") throw error;
|
|
102
|
+
});
|
|
103
|
+
attempts = Number.isSafeInteger(health?.attempts)
|
|
104
|
+
? Math.max(0, Math.min(health.attempts, 10))
|
|
105
|
+
: 0;
|
|
97
106
|
let state = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
98
107
|
if (state.running && state.phase === "unresponsive") {
|
|
99
108
|
await (options.recoverStalled ?? stopStalledAgent)(profile, state.runId);
|
|
@@ -101,12 +110,21 @@ export async function superviseOnce(env = process.env, options = {}) {
|
|
|
101
110
|
if (state.running) continue;
|
|
102
111
|
if (!(await automaticEnabled(profile))) continue;
|
|
103
112
|
}
|
|
104
|
-
if (state.running && state.packageVersion === (options.packageVersion ?? PACKAGE_VERSION))
|
|
113
|
+
if (state.running && state.packageVersion === (options.packageVersion ?? PACKAGE_VERSION)) {
|
|
105
114
|
await reportActivations(
|
|
106
115
|
activationClient,
|
|
107
116
|
activationIds,
|
|
108
117
|
state.ready ? "ready" : "starting",
|
|
109
118
|
);
|
|
119
|
+
if (state.ready && (health?.state !== "running" || attempts))
|
|
120
|
+
await privateWrite(healthPath, {
|
|
121
|
+
state: "running",
|
|
122
|
+
activationIds,
|
|
123
|
+
attempts: 0,
|
|
124
|
+
retryAt: null,
|
|
125
|
+
updatedAt: new Date().toISOString(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
110
128
|
if (state.running) {
|
|
111
129
|
if (state.packageVersion === (options.packageVersion ?? PACKAGE_VERSION)) continue;
|
|
112
130
|
// Finish active work before upgrading. Never overlap two workers or clear journals.
|
|
@@ -123,14 +141,6 @@ export async function superviseOnce(env = process.env, options = {}) {
|
|
|
123
141
|
}
|
|
124
142
|
if (!stopped || !(await automaticEnabled(profile))) continue;
|
|
125
143
|
}
|
|
126
|
-
const health = await privateRead(join(profile.stateDirectory, "automatic-health.json")).catch(
|
|
127
|
-
(error) => {
|
|
128
|
-
if (error.code !== "ENOENT") throw error;
|
|
129
|
-
},
|
|
130
|
-
);
|
|
131
|
-
attempts = Number.isSafeInteger(health?.attempts)
|
|
132
|
-
? Math.max(0, Math.min(health.attempts, 10))
|
|
133
|
-
: 0;
|
|
134
144
|
if (health?.retryable === false) {
|
|
135
145
|
await reportActivations(
|
|
136
146
|
activationClient,
|
|
@@ -167,11 +177,12 @@ export async function superviseOnce(env = process.env, options = {}) {
|
|
|
167
177
|
activationIds,
|
|
168
178
|
started.ready ? "ready" : "starting",
|
|
169
179
|
);
|
|
170
|
-
await privateWrite(
|
|
180
|
+
await privateWrite(healthPath, {
|
|
171
181
|
state: started.ready ? "running" : "starting",
|
|
172
182
|
activationIds,
|
|
173
|
-
|
|
174
|
-
|
|
183
|
+
// Starting a native chat is not proof that its asynchronous readiness check passed.
|
|
184
|
+
attempts: started.ready ? 0 : attempts + 1,
|
|
185
|
+
retryAt: started.ready ? null : Date.now() + Math.min(300000, 5000 * 2 ** attempts),
|
|
175
186
|
updatedAt: new Date().toISOString(),
|
|
176
187
|
});
|
|
177
188
|
results.push(entry.name);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withAgentObservation, observeInspector } from "./observability.mjs";
|
|
1
2
|
import { execFile } from "node:child_process";
|
|
2
3
|
import { promisify } from "node:util";
|
|
3
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -9,6 +10,7 @@ import { Totem } from "./sdk/index.js";
|
|
|
9
10
|
import { privateRead } from "./runtime-installation.mjs";
|
|
10
11
|
import { wakeCodexDesktop } from "./codex-desktop-wake.mjs";
|
|
11
12
|
import { reserveChatPrompt } from "./chat-prompt-limit.mjs";
|
|
13
|
+
import { observeSetupDispatch, recordSetupPeerReply } from "./setup-receipts.mjs";
|
|
12
14
|
|
|
13
15
|
const execute = promisify(execFile);
|
|
14
16
|
const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/;
|
|
@@ -261,6 +263,14 @@ export async function queueCodexChatMessage({
|
|
|
261
263
|
return { state: "uncertain" };
|
|
262
264
|
}
|
|
263
265
|
await journal.save(key, { state: "queued", threadId: settings.codexThreadId });
|
|
266
|
+
await observeSetupDispatch({
|
|
267
|
+
directory: settings.stateDirectory,
|
|
268
|
+
identity,
|
|
269
|
+
sessionId: settings.codexThreadId,
|
|
270
|
+
kind: "peer",
|
|
271
|
+
id: message.id,
|
|
272
|
+
text: message.content?.text,
|
|
273
|
+
});
|
|
264
274
|
return { state: "queued" };
|
|
265
275
|
}
|
|
266
276
|
|
|
@@ -315,7 +325,7 @@ export async function loadCodexChatSession({
|
|
|
315
325
|
identity.environment !== profile.environment
|
|
316
326
|
)
|
|
317
327
|
throw error("AGENT_IDENTITY_MISMATCH");
|
|
318
|
-
return { profile, client, identity, inspector };
|
|
328
|
+
return { profile, client, identity, inspector: observeInspector(inspector) };
|
|
319
329
|
}
|
|
320
330
|
|
|
321
331
|
/** The linked chat's reply client uses the saved identity and the same output guard. */
|
|
@@ -331,6 +341,7 @@ export async function completeCodexChatMessage({
|
|
|
331
341
|
}) {
|
|
332
342
|
if (!UUID.test(messageId ?? "")) throw error("AGENT_IDENTITY_MISMATCH");
|
|
333
343
|
const {
|
|
344
|
+
profile,
|
|
334
345
|
client,
|
|
335
346
|
identity,
|
|
336
347
|
inspector: policy,
|
|
@@ -343,43 +354,66 @@ export async function completeCodexChatMessage({
|
|
|
343
354
|
inspector = policy;
|
|
344
355
|
const message = await client.messages.get(messageId);
|
|
345
356
|
if (message.to !== identity.address) throw error("AGENT_IDENTITY_MISMATCH");
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
message:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
357
|
+
return withAgentObservation(
|
|
358
|
+
{
|
|
359
|
+
client,
|
|
360
|
+
agentId: identity.id,
|
|
361
|
+
sessionId: message.conversationId,
|
|
362
|
+
traceId: "message:" + message.id,
|
|
363
|
+
input: message.content,
|
|
364
|
+
output: text,
|
|
365
|
+
name: readOnly ? "agent.request.read" : "agent.conversation",
|
|
366
|
+
metadata: { provider: "codex", usageSource: "unavailable" },
|
|
367
|
+
},
|
|
368
|
+
async () => {
|
|
369
|
+
await inspectNetworkMessage(inspector, client, message);
|
|
370
|
+
if (readOnly) {
|
|
371
|
+
if (ackOnly || text !== undefined) throw error("INVALID_PEER_REPLY");
|
|
372
|
+
return {
|
|
373
|
+
state: "ready",
|
|
374
|
+
message: {
|
|
375
|
+
id: message.id,
|
|
376
|
+
from: message.from,
|
|
377
|
+
to: message.to,
|
|
378
|
+
conversationId: message.conversationId,
|
|
379
|
+
inReplyToId: message.inReplyToId,
|
|
380
|
+
content: message.content,
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (ackOnly && text !== undefined) throw error("INVALID_PEER_REPLY");
|
|
385
|
+
if (ackOnly && !message.inReplyToId) throw error("INVALID_PEER_REPLY");
|
|
386
|
+
if (!ackOnly && message.inReplyToId) {
|
|
387
|
+
const activeTask = await activeTaskForReply(client, message, identity);
|
|
388
|
+
if (!activeTask) throw error("INVALID_PEER_REPLY");
|
|
389
|
+
}
|
|
390
|
+
let reply;
|
|
391
|
+
if (!ackOnly) {
|
|
392
|
+
if (typeof text !== "string" || !text.trim() || text.length > 32768)
|
|
393
|
+
throw error("INVALID_PEER_REPLY");
|
|
394
|
+
reply = await inspector.runAction({ id: messageId, text }, (inspected) =>
|
|
395
|
+
client.messages.reply(inspected.id, inspected.text, {
|
|
396
|
+
idempotencyKey: `codex-chat:reply:${messageId}`,
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
await client.messages.ack(messageId);
|
|
401
|
+
if (reply)
|
|
402
|
+
await recordSetupPeerReply({
|
|
403
|
+
profile,
|
|
404
|
+
identity,
|
|
405
|
+
message,
|
|
406
|
+
reply,
|
|
407
|
+
client,
|
|
408
|
+
sessionId: environment.CODEX_THREAD_ID,
|
|
409
|
+
});
|
|
410
|
+
return {
|
|
411
|
+
state: "confirmed",
|
|
412
|
+
messageId,
|
|
413
|
+
...(reply ? { replyId: reply.id, conversationId: reply.conversationId } : {}),
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
);
|
|
383
417
|
}
|
|
384
418
|
|
|
385
419
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
@@ -1,22 +1,49 @@
|
|
|
1
|
-
import {execFile} from "node:child_process";
|
|
2
|
-
import {promisify} from "node:util";
|
|
3
|
-
import {join} from "node:path";
|
|
4
|
-
import {localClientEnvironment} from "./platform-client.mjs";
|
|
5
|
-
const execute=promisify(execFile);
|
|
6
|
-
const UUID
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { localClientEnvironment } from "./platform-client.mjs";
|
|
5
|
+
const execute = promisify(execFile);
|
|
6
|
+
const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/;
|
|
7
7
|
/** Load the owner-selected existing chat using Codex's documented app route; no prompt or authority is added. */
|
|
8
|
-
export async function wakeCodexDesktop(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
export async function wakeCodexDesktop(
|
|
9
|
+
{ threadId, signal, environment = process.env },
|
|
10
|
+
options = {},
|
|
11
|
+
) {
|
|
12
|
+
if (!UUID.test(threadId ?? ""))
|
|
13
|
+
throw Object.assign(new Error("AGENT_IDENTITY_MISMATCH"), { code: "AGENT_IDENTITY_MISMATCH" });
|
|
14
|
+
const env = localClientEnvironment(environment);
|
|
15
|
+
for (const name of Object.keys(env))
|
|
16
|
+
if (
|
|
17
|
+
/^(?:TOTEM|AGENTTRUST)_|^(?:OPENAI_API_KEY|ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)$/i.test(
|
|
18
|
+
name,
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
delete env[name];
|
|
22
|
+
const uri = `codex://threads/${threadId}?hostId=local`,
|
|
23
|
+
platform = options.platform ?? process.platform;
|
|
24
|
+
let command, args;
|
|
25
|
+
if (platform === "darwin") {
|
|
26
|
+
command = "/usr/bin/open";
|
|
27
|
+
args = ["-g", uri];
|
|
28
|
+
} else if (platform === "win32") {
|
|
29
|
+
command =
|
|
30
|
+
env.SystemRoot || env.SYSTEMROOT
|
|
31
|
+
? join(env.SystemRoot ?? env.SYSTEMROOT, "explorer.exe")
|
|
32
|
+
: "explorer.exe";
|
|
33
|
+
args = [uri];
|
|
34
|
+
} else if (platform === "linux") {
|
|
35
|
+
command = "xdg-open";
|
|
36
|
+
args = [uri];
|
|
37
|
+
} else
|
|
38
|
+
throw Object.assign(new Error("AGENT_CLI_INCOMPATIBLE"), { code: "AGENT_CLI_INCOMPATIBLE" });
|
|
39
|
+
const pending = (options.execute ?? execute)(command, args, {
|
|
40
|
+
env,
|
|
41
|
+
signal,
|
|
42
|
+
timeout: 15000,
|
|
43
|
+
maxBuffer: 65536,
|
|
44
|
+
windowsHide: true,
|
|
45
|
+
});
|
|
46
|
+
pending.child?.stdin?.end();
|
|
47
|
+
await pending;
|
|
48
|
+
return { state: "requested", threadId };
|
|
22
49
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
import { withAgentObservation } from "./observability.mjs";
|
|
3
4
|
import { readAgentProfile } from "./agent-profile.mjs";
|
|
4
5
|
import { defaultCodexQueue, loadCodexChatSession } from "./codex-chat-bridge.mjs";
|
|
5
6
|
import { customerInspector } from "./local-inspector.mjs";
|
|
6
7
|
import { privateRead, privateWrite } from "./runtime-installation.mjs";
|
|
8
|
+
import { observeSetupDispatch, recordSetupOwnerChat } from "./setup-receipts.mjs";
|
|
7
9
|
import { reserveChatPrompt } from "./chat-prompt-limit.mjs";
|
|
8
10
|
import { wakeCodexDesktop } from "./codex-desktop-wake.mjs";
|
|
9
11
|
|
|
@@ -142,38 +144,72 @@ export async function queueCodexChatCommand({
|
|
|
142
144
|
return { state: "uncertain" };
|
|
143
145
|
}
|
|
144
146
|
await privateWrite(file, { ...record, state: "queued" });
|
|
147
|
+
await observeSetupDispatch({
|
|
148
|
+
directory: profile.stateDirectory,
|
|
149
|
+
identity,
|
|
150
|
+
sessionId: profile.codexThreadId,
|
|
151
|
+
kind: "ownerChat",
|
|
152
|
+
id: command.id,
|
|
153
|
+
text: command.text,
|
|
154
|
+
});
|
|
145
155
|
return { state: "queued" };
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
/** Only the linked native chat can read or complete its leased website request. */
|
|
149
159
|
export async function completeCodexChatCommand({ commandId, text, readOnly = false, ...options }) {
|
|
150
|
-
const { profile, client, inspector } = await loadCodexChatSession(options);
|
|
160
|
+
const { profile, client, identity, inspector } = await loadCodexChatSession(options);
|
|
151
161
|
const saved = await privateRead(pathFor(profile, commandId));
|
|
152
162
|
const command = { id: commandId, chatId: saved.chatId, commandEventId: saved.commandEventId };
|
|
153
163
|
checkBinding(saved, profile, command);
|
|
154
164
|
const { event, run } = await readCodexChatCommand(client, command);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
return withAgentObservation(
|
|
166
|
+
{
|
|
167
|
+
client,
|
|
168
|
+
agentId: profile.agentId,
|
|
169
|
+
sessionId: saved.chatId,
|
|
170
|
+
traceId: "chat:" + commandId,
|
|
171
|
+
input: event.text,
|
|
172
|
+
output: text,
|
|
173
|
+
name: readOnly ? "agent.request.read" : "agent.conversation",
|
|
174
|
+
metadata: { provider: "codex", usageSource: "unavailable" },
|
|
175
|
+
},
|
|
176
|
+
async () => {
|
|
177
|
+
await inspector.assertAllowed("input", event.text);
|
|
178
|
+
if (readOnly) {
|
|
179
|
+
if (text !== undefined) throw failure("INVALID_PEER_REPLY");
|
|
180
|
+
// Prove current lease ownership before the native chat acts, rather than
|
|
181
|
+
// trusting a private token that another runtime may have superseded.
|
|
182
|
+
if (run.state === "running") await client.chatCommands.renew(commandId, saved.leaseToken);
|
|
183
|
+
return { state: run.state, commandId, text: event.text };
|
|
184
|
+
}
|
|
185
|
+
if (run.state !== "running" && run.state !== "completed") throw failure("CHAT_LEASE_LOST");
|
|
186
|
+
if (typeof text !== "string" || !text.trim() || text.length > 32768)
|
|
187
|
+
throw failure("INVALID_PEER_REPLY");
|
|
188
|
+
const leaseRenewed = run.state === "running";
|
|
189
|
+
if (leaseRenewed) await client.chatCommands.renew(commandId, saved.leaseToken);
|
|
190
|
+
const result = await inspector.runAction(
|
|
191
|
+
{ text },
|
|
192
|
+
(inspected) =>
|
|
193
|
+
client.chatCommands.complete(
|
|
194
|
+
commandId,
|
|
195
|
+
{ leaseToken: saved.leaseToken, text: inspected.text },
|
|
196
|
+
{ idempotencyKey: `codex-chat:complete:${commandId}` },
|
|
197
|
+
),
|
|
198
|
+
{ task: event.text },
|
|
199
|
+
);
|
|
200
|
+
await recordSetupOwnerChat({
|
|
201
|
+
profile,
|
|
202
|
+
identity,
|
|
170
203
|
commandId,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
204
|
+
saved,
|
|
205
|
+
result,
|
|
206
|
+
text: event.text,
|
|
207
|
+
leaseRenewed,
|
|
208
|
+
sessionId: (options.environment ?? process.env).CODEX_THREAD_ID,
|
|
209
|
+
});
|
|
210
|
+
return { state: result.run.state, commandId, eventId: result.event.id };
|
|
211
|
+
},
|
|
175
212
|
);
|
|
176
|
-
return { state: result.run.state, commandId, eventId: result.event.id };
|
|
177
213
|
}
|
|
178
214
|
|
|
179
215
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|