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/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.75.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.75.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.75.tgz totem install
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
Version 0.1.75 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
|
|
|
@@ -92,7 +88,7 @@ totem diagnose --json
|
|
|
92
88
|
|
|
93
89
|
Check the observed receive, wake, and reply results. Stop pauses automatic processing until you start it again. The host must stay awake and online. Upgrades retain saved connections and recovery records. Hosted Context processing is temporarily paused.
|
|
94
90
|
|
|
95
|
-
Local connections register automatic startup for the signed-in user: launchd on macOS, Task Scheduler on Windows, or a current-user Startup shortcut when task registration is unavailable. The supervisor restarts crashed children with backoff. Startup registration is verified separately from current readiness. Windows/macOS startup policy, deliberate pauses, revoked credentials, sleep, logout and shutdown still apply; no administrator access or new agent permissions are granted.
|
|
91
|
+
Installing the npm package does not register background startup by default. Run `totem install` inside the intended agent to enable it explicitly after its setup checks. Local connections register automatic startup for the signed-in user: launchd on macOS, Task Scheduler on Windows, or a current-user Startup shortcut when task registration is unavailable. The supervisor restarts crashed children with backoff. Startup registration is verified separately from current readiness. Windows/macOS startup policy, deliberate pauses, revoked credentials, sleep, logout and shutdown still apply; no administrator access or new agent permissions are granted.
|
|
96
92
|
|
|
97
93
|
Connection acceptance and action execution have separate limits. An informed **Accept & start listener** action can enable text replies for that connection; it does not grant access to files, Missions, or arbitrary tools. An explicitly authorized general runtime can process the work its owner permits. Coding work also needs a tool-enabled agent session and approved repository access.
|
|
98
94
|
|
|
@@ -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-profile.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
|
|
1
2
|
import { constants } from "node:fs";
|
|
2
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
4
|
import { lstat, mkdir, open, realpath, rename, unlink } from "node:fs/promises";
|
|
@@ -90,8 +91,10 @@ async function directories(path, privateRoot, create = false) {
|
|
|
90
91
|
info = await lstat(current);
|
|
91
92
|
}
|
|
92
93
|
if (info.isSymbolicLink() || !info.isDirectory()) fail();
|
|
93
|
-
if (current === privateRoot || current.startsWith(`${privateRoot}${sep}`))
|
|
94
|
+
if (current === privateRoot || current.startsWith(`${privateRoot}${sep}`)) {
|
|
94
95
|
privateDirectory(info);
|
|
96
|
+
await assertWindowsPrivatePath(current);
|
|
97
|
+
}
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
|
|
@@ -108,6 +111,7 @@ function privateFile(info, maximum) {
|
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
async function boundedPrivateRead(path, maximum) {
|
|
114
|
+
await assertWindowsPrivatePath(path);
|
|
111
115
|
const info = await lstat(path);
|
|
112
116
|
privateFile(info, maximum);
|
|
113
117
|
const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
@@ -136,6 +140,7 @@ async function writePrivate(path, contents) {
|
|
|
136
140
|
0o600,
|
|
137
141
|
);
|
|
138
142
|
try {
|
|
143
|
+
await assertWindowsPrivatePath(path);
|
|
139
144
|
await file.writeFile(contents);
|
|
140
145
|
await file.sync();
|
|
141
146
|
} finally {
|
package/bin/agent-runner.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
|
|
1
2
|
import { processIdentity } from "./runtime-lock.mjs";
|
|
2
3
|
import { startupDiagnostic } from "./startup-diagnostics.mjs";
|
|
3
4
|
import { spawn } from "node:child_process";
|
|
@@ -86,8 +87,10 @@ async function directoryTree(path, privateRoot) {
|
|
|
86
87
|
const info = await lstat(current);
|
|
87
88
|
if (!info.isDirectory() || info.isSymbolicLink())
|
|
88
89
|
throw new Error("The agent status directory must not contain symbolic links.");
|
|
89
|
-
if (current === privateRoot || current.startsWith(`${privateRoot}${sep}`))
|
|
90
|
+
if (current === privateRoot || current.startsWith(`${privateRoot}${sep}`)) {
|
|
90
91
|
privateEntry(info, true);
|
|
92
|
+
await assertWindowsPrivatePath(current);
|
|
93
|
+
}
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
|
|
@@ -110,6 +113,7 @@ async function runtimeDirectory(profile, create = false) {
|
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
async function readPrivateJson(path) {
|
|
116
|
+
await assertWindowsPrivatePath(path);
|
|
113
117
|
await verifyRuntimeDirectory(dirname(path));
|
|
114
118
|
for (let attempt = 0; ; attempt += 1) {
|
|
115
119
|
const info = await lstat(path);
|
|
@@ -268,6 +272,13 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
|
|
|
268
272
|
...(typeof record.packageVersion === "string" ? { packageVersion: record.packageVersion } : {}),
|
|
269
273
|
updatedAt: record.updatedAt,
|
|
270
274
|
...(record.listenerHeartbeatAt ? { listenerHeartbeatAt: record.listenerHeartbeatAt } : {}),
|
|
275
|
+
...(Number.isFinite(record.receiveCheckAt) ? { receiveCheckAt: record.receiveCheckAt } : {}),
|
|
276
|
+
...(Number.isFinite(record.measuredCheckSeconds)
|
|
277
|
+
? { measuredCheckSeconds: record.measuredCheckSeconds }
|
|
278
|
+
: {}),
|
|
279
|
+
...(Number.isFinite(record.receiverIntervalSeconds)
|
|
280
|
+
? { receiverIntervalSeconds: record.receiverIntervalSeconds }
|
|
281
|
+
: {}),
|
|
271
282
|
...(/^[A-Z][A-Z0-9_]{0,79}$/.test(record.errorCode ?? "")
|
|
272
283
|
? { errorCode: record.errorCode }
|
|
273
284
|
: {}),
|
|
@@ -509,7 +520,11 @@ export async function runAgentSession(profile, options = {}) {
|
|
|
509
520
|
verifyReadiness: true,
|
|
510
521
|
connectionOnly: Boolean(identity.listenerConnectionId),
|
|
511
522
|
hostKind: (options.env ?? process.env).TOTEM_RUNTIME_HOST_KIND ?? "local",
|
|
512
|
-
sessionLockDirectory: join(
|
|
523
|
+
sessionLockDirectory: join(
|
|
524
|
+
(options.env ?? process.env).TOTEM_HOST_LOCK_ROOT ??
|
|
525
|
+
runtimeRoot(options.env ?? process.env),
|
|
526
|
+
"agent-runtime-locks",
|
|
527
|
+
),
|
|
513
528
|
profileName: profile.profileName,
|
|
514
529
|
profileConfigDirectory: dirname(dirname(dirname(dirname(profile.stateDirectory)))),
|
|
515
530
|
address: profile.address,
|
|
@@ -544,6 +559,17 @@ export async function runAgentSession(profile, options = {}) {
|
|
|
544
559
|
signal: controller.signal,
|
|
545
560
|
emit: (event) => {
|
|
546
561
|
if (!acceptingEvents) return;
|
|
562
|
+
if (event.type === "receive_check") {
|
|
563
|
+
const at = event.data?.checkedAt;
|
|
564
|
+
if (Number.isFinite(at) && at > (status.receiveCheckAt ?? 0)) {
|
|
565
|
+
if (Number.isFinite(status.receiveCheckAt))
|
|
566
|
+
status.measuredCheckSeconds = (at - status.receiveCheckAt) / 1000;
|
|
567
|
+
status.receiveCheckAt = at;
|
|
568
|
+
status.receiverIntervalSeconds = event.data.intervalSeconds;
|
|
569
|
+
void persist();
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
547
573
|
const state = eventState(event);
|
|
548
574
|
if (state) emit(state);
|
|
549
575
|
},
|
package/bin/agent-setup.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
|
|
1
2
|
import { startupDiagnostic } from "./startup-diagnostics.mjs";
|
|
2
3
|
import { execFile } from "node:child_process";
|
|
3
4
|
import { readFile, stat } from "node:fs/promises";
|
|
@@ -156,6 +157,7 @@ function credential(value) {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
async function keyFromFile(path) {
|
|
160
|
+
await assertWindowsPrivatePath(path);
|
|
159
161
|
try {
|
|
160
162
|
const info = await stat(path);
|
|
161
163
|
if (!info.isFile() || !info.size || info.size > KEY_LIMIT)
|
|
@@ -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) {
|