totemlayer 0.1.77 → 0.1.92
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 +1 -1
- package/bin/agent-profile.mjs +7 -0
- package/bin/agent-runner.mjs +20 -3
- package/bin/automatic-runtime.mjs +3 -1
- package/bin/claude-directory-profile.mjs +222 -0
- package/bin/codex-chat-bridge.mjs +83 -33
- package/bin/codex-chat-readiness.mjs +15 -1
- package/bin/codex-desktop-wake.mjs +7 -45
- package/bin/codex-human-chat-bridge.mjs +6 -1
- package/bin/codex-mission-bridge.mjs +9 -2
- package/bin/codex-native-callbacks.mjs +273 -0
- package/bin/harness-bootstrap.mjs +77 -7
- package/bin/harness-catalog.mjs +142 -0
- package/bin/installation.mjs +2 -0
- package/bin/mcp-config.d.mts +16 -1
- package/bin/mcp-config.mjs +127 -7
- package/bin/mcp-install.mjs +2 -1
- package/bin/mcp.mjs +77 -22
- package/bin/native/darwin-lifetime +0 -0
- package/bin/native/darwin-lifetime.json +1 -1
- package/bin/network-runtime.mjs +55 -10
- package/bin/runtime-diagnostics.mjs +2 -0
- package/bin/runtime-installation.mjs +5 -1
- package/bin/runtime-lock.mjs +18 -9
- package/bin/setup-assets/api-manifest.json +1 -1
- package/bin/setup-assets/manifest.json +1 -1
- package/bin/setup-receipts.mjs +54 -9
- package/bin/startup-diagnostics.mjs +14 -0
- package/bin/startup-repair.mjs +177 -0
- package/bin/strict-codex-adapter.mjs +72 -22
- package/bin/strict-setup-jobs.mjs +11 -3
- package/bin/totem.mjs +6 -0
- package/package.json +5 -1
package/bin/setup-receipts.mjs
CHANGED
|
@@ -64,15 +64,18 @@ export async function observeSetupDispatch({ directory, identity, sessionId, kin
|
|
|
64
64
|
!text.includes(`Totem setup test ${request.nonce}`)
|
|
65
65
|
)
|
|
66
66
|
return;
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
67
|
+
const name = `${kind}-dispatch-${id}`;
|
|
68
|
+
if (await read(directory, name)) return;
|
|
69
|
+
const dispatch = {
|
|
70
70
|
version: 1,
|
|
71
71
|
binding: request.binding,
|
|
72
72
|
id,
|
|
73
73
|
sessionId,
|
|
74
74
|
dispatchedAt: Date.now(),
|
|
75
|
-
}
|
|
75
|
+
};
|
|
76
|
+
await privateWrite(path(directory, name), dispatch);
|
|
77
|
+
if (!(await read(directory, `${kind}-dispatch`)))
|
|
78
|
+
await privateWrite(path(directory, `${kind}-dispatch`), dispatch);
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
export async function recordSetupPeerReply({
|
|
@@ -97,7 +100,9 @@ export async function recordSetupPeerReply({
|
|
|
97
100
|
!message.content?.text?.includes(`Totem setup test ${request.nonce}`)
|
|
98
101
|
)
|
|
99
102
|
return;
|
|
100
|
-
const dispatch =
|
|
103
|
+
const dispatch =
|
|
104
|
+
(await read(profile.stateDirectory, `peer-dispatch-${message.id}`)) ??
|
|
105
|
+
(await read(profile.stateDirectory, "peer-dispatch"));
|
|
101
106
|
if (dispatch?.id !== message.id || dispatch.binding !== request.binding) return;
|
|
102
107
|
const receivedAt = Date.parse(message.createdAt);
|
|
103
108
|
if (!Number.isFinite(receivedAt) || receivedAt < request.issuedAt) return;
|
|
@@ -117,14 +122,24 @@ export async function recordSetupPeerReply({
|
|
|
117
122
|
}
|
|
118
123
|
if (cursor || replies.length !== 1 || replies[0].id !== reply.id) return;
|
|
119
124
|
const acknowledged = await client.messages.get(message.id);
|
|
120
|
-
|
|
125
|
+
const repliedAt = Date.parse(reply.createdAt);
|
|
126
|
+
const acknowledgedAt = Date.parse(acknowledged.readAt);
|
|
127
|
+
if (
|
|
128
|
+
acknowledged.id !== message.id ||
|
|
129
|
+
!["acknowledged", "responded"].includes(acknowledged.state) ||
|
|
130
|
+
!Number.isFinite(repliedAt) ||
|
|
131
|
+
!Number.isFinite(acknowledgedAt) ||
|
|
132
|
+
acknowledgedAt < repliedAt
|
|
133
|
+
)
|
|
134
|
+
return;
|
|
121
135
|
await privateWrite(path(profile.stateDirectory, "peer"), {
|
|
122
136
|
binding: request.binding,
|
|
123
137
|
nativeSessionId: sessionId,
|
|
124
138
|
messageId: message.id,
|
|
125
139
|
replyId: reply.id,
|
|
126
140
|
receivedAt,
|
|
127
|
-
repliedAt
|
|
141
|
+
repliedAt,
|
|
142
|
+
acknowledgedAt,
|
|
128
143
|
correlated: true,
|
|
129
144
|
exactlyOneReply: true,
|
|
130
145
|
acknowledgedAfterReply: true,
|
|
@@ -143,6 +158,7 @@ export async function recordSetupOwnerChat({
|
|
|
143
158
|
text,
|
|
144
159
|
sessionId,
|
|
145
160
|
leaseRenewed,
|
|
161
|
+
client,
|
|
146
162
|
}) {
|
|
147
163
|
const request = await read(profile.stateDirectory, "request");
|
|
148
164
|
if (!request || request.credentialFingerprint !== (await credentialFingerprint(profile))) return;
|
|
@@ -153,12 +169,41 @@ export async function recordSetupOwnerChat({
|
|
|
153
169
|
!uuid(saved.chatId) ||
|
|
154
170
|
!uuid(result?.event?.id) ||
|
|
155
171
|
result.run?.state !== "completed" ||
|
|
156
|
-
result.
|
|
172
|
+
result.run.id !== commandId ||
|
|
173
|
+
result.run.commandEventId !== saved.commandEventId ||
|
|
174
|
+
result.event.runId !== commandId ||
|
|
175
|
+
result.event.actor !== "agent" ||
|
|
157
176
|
leaseRenewed !== true
|
|
158
177
|
)
|
|
159
178
|
return;
|
|
160
|
-
const dispatch =
|
|
179
|
+
const dispatch =
|
|
180
|
+
(await read(profile.stateDirectory, `ownerChat-dispatch-${commandId}`)) ??
|
|
181
|
+
(await read(profile.stateDirectory, "ownerChat-dispatch"));
|
|
161
182
|
if (dispatch?.id !== commandId || dispatch.binding !== request.binding) return;
|
|
183
|
+
let cursor = "0",
|
|
184
|
+
completedInOriginalChat = false;
|
|
185
|
+
for (let page = 0; page < 100; page++) {
|
|
186
|
+
const detail = await client.chats.get(saved.chatId, {
|
|
187
|
+
limit: 100,
|
|
188
|
+
...(cursor ? { cursor } : {}),
|
|
189
|
+
});
|
|
190
|
+
if (detail.chat.id !== saved.chatId) return;
|
|
191
|
+
const event = detail.events.find((item) => item.id === result.event.id);
|
|
192
|
+
const run = detail.runs.find((item) => item.id === commandId);
|
|
193
|
+
if (
|
|
194
|
+
event?.actor === "agent" &&
|
|
195
|
+
event.runId === commandId &&
|
|
196
|
+
event.text === result.event.text &&
|
|
197
|
+
run?.state === "completed" &&
|
|
198
|
+
run.commandEventId === saved.commandEventId
|
|
199
|
+
) {
|
|
200
|
+
completedInOriginalChat = true;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
if (!detail.hasMore || !detail.nextCursor || detail.nextCursor === cursor) return;
|
|
204
|
+
cursor = detail.nextCursor;
|
|
205
|
+
}
|
|
206
|
+
if (!completedInOriginalChat) return;
|
|
162
207
|
await privateWrite(path(profile.stateDirectory, "ownerChat"), {
|
|
163
208
|
binding: request.binding,
|
|
164
209
|
commandId,
|
|
@@ -4,6 +4,20 @@ export const HOST_APPROVAL_RECOVERY =
|
|
|
4
4
|
"Stop retrying. Ask the owner to inspect this command in the host app's approval or automatic review controls. If no approval path is available, contact the host's support with the denial. Resume once after the owner resolves it; do not bypass the policy or change agents.";
|
|
5
5
|
|
|
6
6
|
const causes = {
|
|
7
|
+
STARTUP_CONFLICT: [
|
|
8
|
+
"The existing startup task or shortcut uses a different command.",
|
|
9
|
+
"Inspect the current-user Windows startup task with totem repair-startup. Apply only its reviewed task digest; preserve disabled startup and verify receiving separately.",
|
|
10
|
+
false,
|
|
11
|
+
],
|
|
12
|
+
STARTUP_DISABLED: [
|
|
13
|
+
"The owner disabled automatic startup on this host.",
|
|
14
|
+
"Keep receiving paused until the owner explicitly enables startup through this host's controls.",
|
|
15
|
+
false,
|
|
16
|
+
],
|
|
17
|
+
STARTUP_REGISTRATION_FAILED: [
|
|
18
|
+
"The host could not verify registration of its startup entry.",
|
|
19
|
+
"Inspect this host's current-user startup permissions and service manager, then retry once after repair.",
|
|
20
|
+
],
|
|
7
21
|
LISTENER_STARTUP_UNAVAILABLE: [
|
|
8
22
|
"The host has no verified enabled persistent listener service.",
|
|
9
23
|
"Restore the host's supported startup service, then rerun setup. A temporary background process is not a complete installation.",
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify, parseArgs } from "node:util";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
runtimeRoot,
|
|
8
|
+
privateDirectory,
|
|
9
|
+
privateRead,
|
|
10
|
+
ensureInstalledRuntime,
|
|
11
|
+
} from "./runtime-installation.mjs";
|
|
12
|
+
import { powershellInvocation } from "./platform-client.mjs";
|
|
13
|
+
|
|
14
|
+
const execute = promisify(execFile);
|
|
15
|
+
const literal = (value) => `'${String(value).replaceAll("'", "''")}'`;
|
|
16
|
+
|
|
17
|
+
/** Explicit repair of one owner-reviewed task. Passive MCP startup never invokes this. */
|
|
18
|
+
export async function repairWindowsStartup(environment = process.env, options = {}) {
|
|
19
|
+
if ((options.platform ?? process.platform) !== "win32")
|
|
20
|
+
throw Object.assign(new Error("Run this repair on the Windows receiver host."), {
|
|
21
|
+
code: "STARTUP_REPAIR_UNSUPPORTED",
|
|
22
|
+
});
|
|
23
|
+
const digest = options.expectedTaskSha256 ?? "";
|
|
24
|
+
if (digest && !/^[a-f0-9]{64}$/.test(digest)) throw new Error("Invalid task digest.");
|
|
25
|
+
const root = runtimeRoot(environment),
|
|
26
|
+
directory = join(root, "runtime");
|
|
27
|
+
const current = await privateRead(join(directory, "current.json"));
|
|
28
|
+
if (!/^\d+\.\d+\.\d+$/.test(current.version)) throw new Error("Invalid installed runtime.");
|
|
29
|
+
const suffix =
|
|
30
|
+
root === resolve(join(homedir(), ".totem"))
|
|
31
|
+
? ""
|
|
32
|
+
: `.${createHash("sha256").update(root).digest("hex").slice(0, 12)}`;
|
|
33
|
+
const task = `TotemRuntime${suffix}`;
|
|
34
|
+
const backups = join(directory, "startup-backups");
|
|
35
|
+
if (digest) await privateDirectory(backups);
|
|
36
|
+
const script = `
|
|
37
|
+
$ErrorActionPreference='Stop'
|
|
38
|
+
$task=${literal(task)};$expected=${literal(digest)};$node=${literal(process.execPath)}
|
|
39
|
+
$arguments=${literal('"' + join(directory, "supervisor.mjs") + '"')}
|
|
40
|
+
$backupDirectory=${literal(backups)}
|
|
41
|
+
function Hash($text) {
|
|
42
|
+
$sha=[Security.Cryptography.SHA256]::Create()
|
|
43
|
+
try { -join ($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($text))|ForEach-Object {$_.ToString('x2')}) } finally { $sha.Dispose() }
|
|
44
|
+
}
|
|
45
|
+
function Owned($entry) {
|
|
46
|
+
$user=$entry.Principal.UserId
|
|
47
|
+
try {
|
|
48
|
+
$sid=if($user -like 'S-1-*'){[Security.Principal.SecurityIdentifier]::new($user)}else{[Security.Principal.NTAccount]::new($user).Translate([Security.Principal.SecurityIdentifier])}
|
|
49
|
+
$sid.Value -eq [Security.Principal.WindowsIdentity]::GetCurrent().User.Value -and [string]$entry.Principal.RunLevel -in @('Limited','0') -and [string]$entry.Principal.LogonType -in @('Interactive','InteractiveToken','3')
|
|
50
|
+
} catch { $false }
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
$old=Get-ScheduledTask -TaskName $task -TaskPath '\\'
|
|
54
|
+
if(!(Owned $old)){throw 'STARTUP_OWNER_MISMATCH'}
|
|
55
|
+
if($old.State -eq 'Disabled' -or !$old.Settings.Enabled){throw 'STARTUP_DISABLED'}
|
|
56
|
+
if($old.Actions.Count -ne 1){throw 'STARTUP_CONFLICT'}
|
|
57
|
+
foreach($hive in @('HKCU:','HKLM:')) {
|
|
58
|
+
$key=$hive+'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\StartupFolder'
|
|
59
|
+
if(Test-Path -LiteralPath $key) {
|
|
60
|
+
$approval=(Get-Item -LiteralPath $key).GetValue($task+'.lnk',$null)
|
|
61
|
+
if($null -ne $approval -and ($approval.Length -lt 1 -or $approval[0] -notin @(2,6))){throw 'STARTUP_DISABLED'}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
$xml=Export-ScheduledTask -TaskName $task -TaskPath '\\'
|
|
65
|
+
$hash=Hash $xml
|
|
66
|
+
$same=$old.Actions[0].Execute -eq $node -and $old.Actions[0].Arguments -eq $arguments
|
|
67
|
+
if(!$expected){@{state=if($same){'current'}else{'plan'};task=$task;taskSha256=$hash;version=${literal(current.version)};currentCommandType=[IO.Path]::GetFileName($old.Actions[0].Execute);proposedCommandType='node';changesProfiles=$false;changesPermissions=$false;requiresRestart=$true;receivingVerified=$false}|ConvertTo-Json -Compress;exit 0}
|
|
68
|
+
if($hash -ne $expected){throw 'STARTUP_PLAN_STALE'}
|
|
69
|
+
if($same){@{state='current';task=$task;version=${literal(current.version)};receivingVerified=$false}|ConvertTo-Json -Compress;exit 0}
|
|
70
|
+
$backup=Join-Path $backupDirectory ($hash+'.xml')
|
|
71
|
+
if(Test-Path -LiteralPath $backup) {
|
|
72
|
+
$file=Get-Item -LiteralPath $backup
|
|
73
|
+
if($file.PSIsContainer -or ($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -or (Hash ([IO.File]::ReadAllText($backup))) -ne $hash){throw 'STARTUP_BACKUP_CONFLICT'}
|
|
74
|
+
} else {
|
|
75
|
+
$stream=[IO.File]::Open($backup,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None)
|
|
76
|
+
try {$bytes=[Text.Encoding]::UTF8.GetBytes($xml);$stream.Write($bytes,0,$bytes.Length);$stream.Flush()}finally{$stream.Dispose()}
|
|
77
|
+
}
|
|
78
|
+
if((Hash (Export-ScheduledTask -TaskName $task -TaskPath '\\')) -ne $hash){throw 'STARTUP_PLAN_STALE'}
|
|
79
|
+
$wasRunning=$old.State -eq 'Running'
|
|
80
|
+
$changed=$false
|
|
81
|
+
try {
|
|
82
|
+
Stop-ScheduledTask -TaskName $task -TaskPath '\\'
|
|
83
|
+
$stopped=Get-ScheduledTask -TaskName $task -TaskPath '\\'
|
|
84
|
+
if($stopped.State -eq 'Running' -or $stopped.State -eq 'Disabled'){throw 'STARTUP_STOP_UNVERIFIED'}
|
|
85
|
+
if((Hash (Export-ScheduledTask -TaskName $task -TaskPath '\\')) -ne $hash){throw 'STARTUP_PLAN_STALE'}
|
|
86
|
+
$action=New-ScheduledTaskAction -Execute $node -Argument $arguments
|
|
87
|
+
$changed=$true
|
|
88
|
+
Set-ScheduledTask -TaskName $task -TaskPath '\\' -Action $action|Out-Null
|
|
89
|
+
$saved=Get-ScheduledTask -TaskName $task -TaskPath '\\'
|
|
90
|
+
if(!(Owned $saved) -or $saved.State -eq 'Disabled' -or $saved.Actions.Count -ne 1 -or $saved.Actions[0].Execute -ne $node -or $saved.Actions[0].Arguments -ne $arguments){throw 'STARTUP_REPAIR_UNVERIFIED'}
|
|
91
|
+
Start-ScheduledTask -TaskName $task -TaskPath '\\'
|
|
92
|
+
@{state='repaired';task=$task;version=${literal(current.version)};backupSha256=$hash;receivingVerified=$false}|ConvertTo-Json -Compress
|
|
93
|
+
} catch {
|
|
94
|
+
if(!$changed){
|
|
95
|
+
if($wasRunning -and (Hash (Export-ScheduledTask -TaskName $task -TaskPath '\\')) -eq $hash){Start-ScheduledTask -TaskName $task -TaskPath '\\'}
|
|
96
|
+
throw 'STARTUP_REPAIR_FAILED'
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
Stop-ScheduledTask -TaskName $task -TaskPath '\\' -ErrorAction SilentlyContinue
|
|
100
|
+
Register-ScheduledTask -TaskName $task -TaskPath '\\' -Xml $xml -Force|Out-Null
|
|
101
|
+
if((Hash (Export-ScheduledTask -TaskName $task -TaskPath '\\')) -ne $hash){throw 'Rollback did not match'}
|
|
102
|
+
if($wasRunning){Start-ScheduledTask -TaskName $task -TaskPath '\\'}
|
|
103
|
+
} catch {throw 'STARTUP_ROLLBACK_FAILED'}
|
|
104
|
+
throw 'STARTUP_REPAIR_ROLLED_BACK'
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
$code=[string]$_.Exception.Message
|
|
108
|
+
if($code -notmatch '^STARTUP_[A-Z_]+$'){$code='STARTUP_REPAIR_FAILED'}
|
|
109
|
+
@{state='blocked';code=$code;receivingVerified=$false}|ConvertTo-Json -Compress
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
const invocation = powershellInvocation(script, environment);
|
|
113
|
+
const result = await (options.execute ?? execute)(invocation.command, invocation.args, {
|
|
114
|
+
timeout: 30000,
|
|
115
|
+
maxBuffer: 8192,
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
});
|
|
118
|
+
const receipt = JSON.parse(result.stdout.trim());
|
|
119
|
+
if (
|
|
120
|
+
!["plan", "current", "repaired", "blocked"].includes(receipt.state) ||
|
|
121
|
+
(receipt.state === "plan" && !/^[a-f0-9]{64}$/.test(receipt.taskSha256 ?? "")) ||
|
|
122
|
+
(receipt.state === "repaired" && receipt.backupSha256 !== digest)
|
|
123
|
+
)
|
|
124
|
+
throw new Error("Invalid startup repair receipt.");
|
|
125
|
+
return receipt;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function startupRepairMain(args, options = {}) {
|
|
129
|
+
const stdout = options.stdout ?? process.stdout;
|
|
130
|
+
try {
|
|
131
|
+
const { values } = parseArgs({
|
|
132
|
+
args,
|
|
133
|
+
allowPositionals: false,
|
|
134
|
+
options: {
|
|
135
|
+
help: { type: "boolean", short: "h" },
|
|
136
|
+
"expected-task-sha256": { type: "string" },
|
|
137
|
+
json: { type: "boolean" },
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
if (values.help) {
|
|
141
|
+
stdout.write(
|
|
142
|
+
"totem repair-startup [--expected-task-sha256 HEX] [--json]\nInspect the current-user Windows startup task. Supply its inspected SHA-256 to migrate only that task with a local backup and rollback. Profiles and pauses are preserved; receiving must be verified separately.\n",
|
|
143
|
+
);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
if (values["expected-task-sha256"]) {
|
|
147
|
+
if (!/^[a-f0-9]{64}$/.test(values["expected-task-sha256"]))
|
|
148
|
+
throw new Error("Invalid task digest.");
|
|
149
|
+
const plan = await repairWindowsStartup(options.env ?? process.env, options);
|
|
150
|
+
if (
|
|
151
|
+
!["plan", "current"].includes(plan.state) ||
|
|
152
|
+
plan.taskSha256 !== values["expected-task-sha256"]
|
|
153
|
+
) {
|
|
154
|
+
stdout.write(
|
|
155
|
+
JSON.stringify({
|
|
156
|
+
state: "blocked",
|
|
157
|
+
code: plan.code ?? "STARTUP_PLAN_STALE",
|
|
158
|
+
receivingVerified: false,
|
|
159
|
+
}) + "\n",
|
|
160
|
+
);
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
await (options.ensureRuntime ?? ensureInstalledRuntime)(options.env ?? process.env);
|
|
164
|
+
}
|
|
165
|
+
const result = await repairWindowsStartup(options.env ?? process.env, {
|
|
166
|
+
...options,
|
|
167
|
+
expectedTaskSha256: values["expected-task-sha256"],
|
|
168
|
+
});
|
|
169
|
+
stdout.write(JSON.stringify(result) + "\n");
|
|
170
|
+
return result.state === "blocked" ? 1 : 0;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
stdout.write(
|
|
173
|
+
JSON.stringify({ state: "blocked", code: error.code ?? "STARTUP_REPAIR_FAILED" }) + "\n",
|
|
174
|
+
);
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
2
3
|
import { opendir, readFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
+
import { isAbsolute, join } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
4
6
|
import { execFile } from "node:child_process";
|
|
5
7
|
import { promisify } from "node:util";
|
|
6
8
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
@@ -90,28 +92,71 @@ export function createStrictCodexAdapter(flags, options = {}) {
|
|
|
90
92
|
rpc = (options.connectHarness ?? connectCodexHarness)(inherited);
|
|
91
93
|
await rpc.initialize();
|
|
92
94
|
}
|
|
93
|
-
//
|
|
94
|
-
const current = await rpc.request("thread/
|
|
95
|
+
// Reading avoids acquiring the active chat's writer or changing its settings.
|
|
96
|
+
const current = await rpc.request("thread/read", { threadId });
|
|
97
|
+
const thread = current?.thread;
|
|
98
|
+
if (thread?.id !== threadId || thread.ephemeral !== false || thread.archived)
|
|
99
|
+
fail("EXACT_NATIVE_SESSION_REQUIRED");
|
|
95
100
|
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
!thread.model ||
|
|
102
|
+
thread.model === "default" ||
|
|
103
|
+
(flags.model && thread.model !== flags.model)
|
|
99
104
|
)
|
|
100
|
-
fail("
|
|
101
|
-
|
|
105
|
+
fail("CURRENT_MODEL_UNVERIFIED");
|
|
106
|
+
// Codex returns the native rollout path; permission settings are not in thread/read.
|
|
107
|
+
if (!thread.path || !isAbsolute(thread.path)) fail("CURRENT_SESSION_SETTINGS_UNVERIFIED");
|
|
108
|
+
let metadata,
|
|
109
|
+
settings,
|
|
110
|
+
partialRecord = false;
|
|
111
|
+
const input = createReadStream(thread.path, { encoding: "utf8" });
|
|
112
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
113
|
+
try {
|
|
114
|
+
for await (const line of lines) {
|
|
115
|
+
if (partialRecord) fail("CURRENT_SESSION_SETTINGS_UNVERIFIED");
|
|
116
|
+
let record;
|
|
117
|
+
try {
|
|
118
|
+
record = JSON.parse(line);
|
|
119
|
+
} catch {
|
|
120
|
+
// The native writer may still be appending its final JSONL record.
|
|
121
|
+
if (line.endsWith("}")) fail("CURRENT_SESSION_SETTINGS_UNVERIFIED");
|
|
122
|
+
partialRecord = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (record.type === "session_meta") metadata = record.payload;
|
|
126
|
+
if (record.type === "turn_context") settings = record.payload;
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
fail("CURRENT_SESSION_SETTINGS_UNVERIFIED");
|
|
130
|
+
} finally {
|
|
131
|
+
lines.close();
|
|
132
|
+
input.destroy();
|
|
133
|
+
}
|
|
134
|
+
if (
|
|
135
|
+
metadata?.id !== threadId ||
|
|
136
|
+
metadata.model_provider !== thread.modelProvider ||
|
|
137
|
+
!settings?.turn_id ||
|
|
138
|
+
settings.cwd !== thread.cwd ||
|
|
139
|
+
!settings.approval_policy ||
|
|
140
|
+
!settings.sandbox_policy?.type
|
|
141
|
+
)
|
|
142
|
+
fail("CURRENT_SESSION_SETTINGS_UNVERIFIED");
|
|
143
|
+
if (settings.model !== thread.model || settings.effort !== thread.reasoningEffort)
|
|
102
144
|
fail("CURRENT_MODEL_UNVERIFIED");
|
|
103
145
|
return {
|
|
104
146
|
name: "codex",
|
|
105
|
-
model:
|
|
147
|
+
model: thread.model,
|
|
106
148
|
sessionId: threadId,
|
|
107
149
|
modelVerified: true,
|
|
108
150
|
policyDigest: sha([
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
151
|
+
thread.modelProvider,
|
|
152
|
+
settings.approval_policy,
|
|
153
|
+
settings.approvals_reviewer,
|
|
154
|
+
settings.sandbox_policy,
|
|
155
|
+
settings.permission_profile,
|
|
156
|
+
settings.active_permission_profile,
|
|
157
|
+
settings.disabled_plugin_ids,
|
|
158
|
+
settings.effort,
|
|
159
|
+
settings.service_tier,
|
|
115
160
|
]),
|
|
116
161
|
};
|
|
117
162
|
}
|
|
@@ -354,11 +399,14 @@ export function createStrictCodexAdapter(flags, options = {}) {
|
|
|
354
399
|
},
|
|
355
400
|
async readReceiver(context) {
|
|
356
401
|
let current;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
402
|
+
const deadline = Date.now() + (flags.waitSeconds ?? 1200) * 1000;
|
|
403
|
+
do {
|
|
404
|
+
current = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
405
|
+
if (Number.isFinite(current.measuredCheckSeconds) && current.listenerReady) break;
|
|
406
|
+
if (!(await automaticEnabled(profile))) fail("RECEIVER_INTENTIONALLY_PAUSED");
|
|
407
|
+
if (Date.now() >= deadline) break;
|
|
360
408
|
await pause(1000);
|
|
361
|
-
}
|
|
409
|
+
} while (true);
|
|
362
410
|
return {
|
|
363
411
|
binding: context.binding,
|
|
364
412
|
count: current.running ? 1 : 0,
|
|
@@ -368,7 +416,7 @@ export function createStrictCodexAdapter(flags, options = {}) {
|
|
|
368
416
|
measuredCheckSeconds: current.measuredCheckSeconds,
|
|
369
417
|
independentOfModel: true,
|
|
370
418
|
silentWhenIdle: true,
|
|
371
|
-
bothQueues:
|
|
419
|
+
bothQueues: !identity.listenerConnectionId,
|
|
372
420
|
privateState: true,
|
|
373
421
|
startupEnabled: activation?.persistent === true,
|
|
374
422
|
restartOnFailure: activation?.persistent === true,
|
|
@@ -396,7 +444,8 @@ export function createStrictCodexAdapter(flags, options = {}) {
|
|
|
396
444
|
await privateWrite(join(profile.stateDirectory, "runner", `${current.runId}.stop`), {
|
|
397
445
|
runId: current.runId,
|
|
398
446
|
});
|
|
399
|
-
|
|
447
|
+
const deadline = Date.now() + (flags.waitSeconds ?? 1200) * 1000;
|
|
448
|
+
do {
|
|
400
449
|
if (!(await automaticEnabled(profile))) fail("RECEIVER_INTENTIONALLY_PAUSED");
|
|
401
450
|
const after = await readAgentRuntimeStatus(profile);
|
|
402
451
|
if (after.runId !== current.runId && after.ready && after.listenerReady) {
|
|
@@ -414,8 +463,9 @@ export function createStrictCodexAdapter(flags, options = {}) {
|
|
|
414
463
|
await privateWrite(file, receipt);
|
|
415
464
|
return receipt;
|
|
416
465
|
}
|
|
466
|
+
if (Date.now() >= deadline) break;
|
|
417
467
|
await pause(1000);
|
|
418
|
-
}
|
|
468
|
+
} while (true);
|
|
419
469
|
return { binding: context.binding, newRunVerified: false };
|
|
420
470
|
},
|
|
421
471
|
async requestTests(context) {
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
installRuntimeService,
|
|
10
10
|
runtimeRoot,
|
|
11
11
|
} from "./runtime-installation.mjs";
|
|
12
|
-
import { acquireSupervisorLock, processIdentity } from "./
|
|
12
|
+
import { acquireSupervisorLock, processIdentity } from "./runtime-lock.mjs";
|
|
13
13
|
|
|
14
14
|
const valid = (id) => typeof id === "string" && /^[a-f0-9]{64}$/.test(id);
|
|
15
15
|
const missing = async (promise) =>
|
|
@@ -93,10 +93,18 @@ export async function scheduleStrictSetup(flags, host, env = process.env, option
|
|
|
93
93
|
throw Object.assign(new Error(), { code: "SETUP_BINDING_CHANGED" });
|
|
94
94
|
const job = previous ?? { version: 1, id, flags: selected };
|
|
95
95
|
if (!validJob(job)) throw Object.assign(new Error(), { code: "SETUP_BINDING_UNVERIFIED" });
|
|
96
|
+
const status = await missing(privateRead(join(path, "status.json")));
|
|
97
|
+
const recheck =
|
|
98
|
+
status?.state === "blocked" &&
|
|
99
|
+
[
|
|
100
|
+
"RECEIVER_INTERVAL_UNVERIFIED",
|
|
101
|
+
"RECEIVER_CAPABILITY_INCOMPLETE",
|
|
102
|
+
"RECOVERY_UNVERIFIED",
|
|
103
|
+
].includes(status.code);
|
|
104
|
+
if (status?.state === "blocked" && !recheck) return status;
|
|
96
105
|
const runtime = await (options.ensureRuntime ?? ensureInstalledRuntime)(env);
|
|
97
106
|
if (!previous) await privateWrite(join(path, "job.json"), job);
|
|
98
|
-
|
|
99
|
-
if (status?.state === "complete")
|
|
107
|
+
if (status?.state === "complete" || recheck)
|
|
100
108
|
await privateWrite(join(path, "status.json"), {
|
|
101
109
|
state: "checking",
|
|
102
110
|
complete: false,
|
package/bin/totem.mjs
CHANGED
|
@@ -249,6 +249,7 @@ Connect once and receive messages and approved Mission work
|
|
|
249
249
|
totem setup
|
|
250
250
|
totem status
|
|
251
251
|
totem diagnose [--profile NAME] [--fresh] [--json]
|
|
252
|
+
totem repair-startup [--expected-task-sha256 HEX] [--json]
|
|
252
253
|
totem start --background
|
|
253
254
|
totem stop
|
|
254
255
|
|
|
@@ -1754,6 +1755,11 @@ async function runRfq(options, baseUrl) {
|
|
|
1754
1755
|
}
|
|
1755
1756
|
|
|
1756
1757
|
async function main() {
|
|
1758
|
+
if (process.argv[2] === "repair-startup") {
|
|
1759
|
+
const { startupRepairMain } = await import("./startup-repair.mjs");
|
|
1760
|
+
process.exitCode = await startupRepairMain(process.argv.slice(3));
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1757
1763
|
if (process.argv[2] === "install-worker") {
|
|
1758
1764
|
const { strictSetupWorkerMain } = await import("./strict-setup-jobs.mjs");
|
|
1759
1765
|
process.exitCode = await strictSetupWorkerMain(process.argv[3]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "totemlayer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.92",
|
|
4
4
|
"description": "Connect AI agents to Totem Network for discovery, messaging, and shared work.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"authorization"
|
|
33
33
|
],
|
|
34
34
|
"homepage": "https://totemlayer.com",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/totemlayer/Totem.git"
|
|
38
|
+
},
|
|
35
39
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
36
40
|
"publishConfig": {
|
|
37
41
|
"access": "public",
|