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
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const SETUP_CHECKS = Object.freeze([
|
|
4
|
+
"guide",
|
|
5
|
+
"model",
|
|
6
|
+
"identity",
|
|
7
|
+
"transport",
|
|
8
|
+
"receiver",
|
|
9
|
+
"startup",
|
|
10
|
+
"recovery",
|
|
11
|
+
"nativeWake",
|
|
12
|
+
"peerReply",
|
|
13
|
+
"ownerChat",
|
|
14
|
+
]);
|
|
15
|
+
export const GUIDE_UNAVAILABLE = "Setup incomplete: setup.md could not be read in full.";
|
|
16
|
+
const token = (value) =>
|
|
17
|
+
typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,319}$/.test(value);
|
|
18
|
+
const uuid = (value) =>
|
|
19
|
+
typeof value === "string" && /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value);
|
|
20
|
+
const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
21
|
+
const stop = (code) => {
|
|
22
|
+
throw Object.assign(new Error(code), { code });
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function setupBinding(identity, host, transport, connection) {
|
|
26
|
+
if (
|
|
27
|
+
!uuid(identity?.id) ||
|
|
28
|
+
!token(identity.address) ||
|
|
29
|
+
!uuid(identity.organization?.id) ||
|
|
30
|
+
!["PRODUCTION", "SANDBOX"].includes(identity.environment) ||
|
|
31
|
+
!token(host?.name) ||
|
|
32
|
+
!token(host.model) ||
|
|
33
|
+
host.model === "default" ||
|
|
34
|
+
!token(host.sessionId) ||
|
|
35
|
+
!["mcp", "api"].includes(transport) ||
|
|
36
|
+
!/^[a-f0-9]{64}$/.test(connection?.fingerprint ?? "")
|
|
37
|
+
)
|
|
38
|
+
stop("SETUP_BINDING_UNVERIFIED");
|
|
39
|
+
let origin;
|
|
40
|
+
try {
|
|
41
|
+
const url = new URL(connection.origin);
|
|
42
|
+
if (
|
|
43
|
+
url.username ||
|
|
44
|
+
url.password ||
|
|
45
|
+
url.search ||
|
|
46
|
+
url.hash ||
|
|
47
|
+
url.pathname !== "/" ||
|
|
48
|
+
(url.protocol !== "https:" &&
|
|
49
|
+
!(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
|
|
50
|
+
)
|
|
51
|
+
stop("SETUP_BINDING_UNVERIFIED");
|
|
52
|
+
origin = url.origin;
|
|
53
|
+
} catch {
|
|
54
|
+
stop("SETUP_BINDING_UNVERIFIED");
|
|
55
|
+
}
|
|
56
|
+
return digest([
|
|
57
|
+
identity.id,
|
|
58
|
+
identity.address,
|
|
59
|
+
identity.organization.id,
|
|
60
|
+
identity.environment,
|
|
61
|
+
origin,
|
|
62
|
+
connection.fingerprint,
|
|
63
|
+
host.name,
|
|
64
|
+
host.model,
|
|
65
|
+
host.sessionId,
|
|
66
|
+
transport,
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sameHost(before, after) {
|
|
71
|
+
return (
|
|
72
|
+
after?.modelVerified === true &&
|
|
73
|
+
before.name === after.name &&
|
|
74
|
+
before.model === after.model &&
|
|
75
|
+
before.sessionId === after.sessionId &&
|
|
76
|
+
before.policyDigest === after.policyDigest
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** This controller accepts observations from installed adapters, never model-supplied proofs. */
|
|
81
|
+
export async function runStrictSetup(adapter, options = {}) {
|
|
82
|
+
const checks = Object.fromEntries(SETUP_CHECKS.map((name) => [name, false]));
|
|
83
|
+
let identity,
|
|
84
|
+
host,
|
|
85
|
+
transport,
|
|
86
|
+
binding,
|
|
87
|
+
receiver,
|
|
88
|
+
proofs,
|
|
89
|
+
apiReason,
|
|
90
|
+
operation = "readGuide";
|
|
91
|
+
const progress = async (stage) => options.progress?.({ stage, checks: { ...checks } });
|
|
92
|
+
const report = (code) => ({
|
|
93
|
+
version: 1,
|
|
94
|
+
complete: !code && SETUP_CHECKS.every((name) => checks[name]),
|
|
95
|
+
state:
|
|
96
|
+
code === "RECEIVER_INTENTIONALLY_PAUSED"
|
|
97
|
+
? "paused"
|
|
98
|
+
: !code && SETUP_CHECKS.every((name) => checks[name])
|
|
99
|
+
? "complete"
|
|
100
|
+
: "incomplete",
|
|
101
|
+
...(code ? { code } : {}),
|
|
102
|
+
message:
|
|
103
|
+
code === "GUIDE_UNAVAILABLE"
|
|
104
|
+
? GUIDE_UNAVAILABLE
|
|
105
|
+
: !code && SETUP_CHECKS.every((name) => checks[name])
|
|
106
|
+
? "Totem setup complete; required checks verified."
|
|
107
|
+
: checks.identity
|
|
108
|
+
? "Connection verified; automatic receiving setup incomplete"
|
|
109
|
+
: "Setup incomplete",
|
|
110
|
+
...(host ? { host: host.name, model: host.model, sessionId: host.sessionId } : {}),
|
|
111
|
+
...(identity && checks.identity
|
|
112
|
+
? {
|
|
113
|
+
agentId: identity.id,
|
|
114
|
+
address: identity.address,
|
|
115
|
+
organizationId: identity.organization.id,
|
|
116
|
+
environment: identity.environment,
|
|
117
|
+
}
|
|
118
|
+
: {}),
|
|
119
|
+
...(transport ? { transport } : {}),
|
|
120
|
+
...(apiReason ? { apiReason } : {}),
|
|
121
|
+
...(receiver
|
|
122
|
+
? {
|
|
123
|
+
intervalSeconds: receiver.intervalSeconds,
|
|
124
|
+
measuredCheckSeconds: receiver.measuredCheckSeconds,
|
|
125
|
+
}
|
|
126
|
+
: {}),
|
|
127
|
+
...(checks.peerReply
|
|
128
|
+
? {
|
|
129
|
+
peerVerification: {
|
|
130
|
+
messageId: proofs.peer.messageId,
|
|
131
|
+
replyId: proofs.peer.replyId,
|
|
132
|
+
receivedAt: proofs.peer.receivedAt,
|
|
133
|
+
repliedAt: proofs.peer.repliedAt,
|
|
134
|
+
exactlyOneReply: true,
|
|
135
|
+
acknowledgedAfterReply: true,
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
: {}),
|
|
139
|
+
...(checks.ownerChat
|
|
140
|
+
? {
|
|
141
|
+
ownerChatVerification: {
|
|
142
|
+
commandId: proofs.ownerChat.commandId,
|
|
143
|
+
chatId: proofs.ownerChat.chatId,
|
|
144
|
+
eventId: proofs.ownerChat.eventId,
|
|
145
|
+
leaseRenewed: true,
|
|
146
|
+
completedInOriginalChat: true,
|
|
147
|
+
queuedAt: proofs.ownerChat.queuedAt,
|
|
148
|
+
completedAt: proofs.ownerChat.completedAt,
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
: {}),
|
|
152
|
+
checks: { ...checks },
|
|
153
|
+
remaining: SETUP_CHECKS.filter((name) => !checks[name]),
|
|
154
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
// No host inspection, approval or setup mutation is permitted before a complete guide.
|
|
157
|
+
let guide;
|
|
158
|
+
for (const reader of ["primary", "alternate"]) {
|
|
159
|
+
try {
|
|
160
|
+
const value = await adapter.readGuide(reader);
|
|
161
|
+
if (
|
|
162
|
+
value?.complete === true &&
|
|
163
|
+
value.integrityVerified === true &&
|
|
164
|
+
/^[a-f0-9]{64}$/.test(value.sha256 ?? "") &&
|
|
165
|
+
typeof value.text === "string" &&
|
|
166
|
+
value.text.startsWith("# Set up Totem") &&
|
|
167
|
+
value.text.endsWith("<!-- totem-setup:end -->\n")
|
|
168
|
+
) {
|
|
169
|
+
guide = value;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
/* Try the independent supported reader before stopping. */
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!guide) return report("GUIDE_UNAVAILABLE");
|
|
177
|
+
checks.guide = true;
|
|
178
|
+
await progress("guide_verified");
|
|
179
|
+
operation = "inspectHost";
|
|
180
|
+
const inspected = await adapter.inspect();
|
|
181
|
+
const proposedHost = inspected.host;
|
|
182
|
+
if (
|
|
183
|
+
!token(proposedHost?.name) ||
|
|
184
|
+
!token(proposedHost.model) ||
|
|
185
|
+
proposedHost.model === "default" ||
|
|
186
|
+
/(?:tn_(?:live|test)_|Bearer|secret|token=)/i.test(proposedHost.model) ||
|
|
187
|
+
!token(proposedHost.sessionId) ||
|
|
188
|
+
!/^[a-f0-9]{64}$/.test(proposedHost.policyDigest ?? "") ||
|
|
189
|
+
proposedHost.modelVerified !== true
|
|
190
|
+
)
|
|
191
|
+
stop("CURRENT_MODEL_UNVERIFIED");
|
|
192
|
+
host = proposedHost;
|
|
193
|
+
checks.model = true;
|
|
194
|
+
if (inspected.policyDenied) stop("HOST_POLICY_DENIED");
|
|
195
|
+
if (inspected.identityConflict) stop("AGENT_IDENTITY_MISMATCH");
|
|
196
|
+
// Existing transport, identity, credential generation and intentional pauses win.
|
|
197
|
+
transport = inspected.connection?.transport;
|
|
198
|
+
if (!transport) {
|
|
199
|
+
if (inspected.ownerSelectedTransport === "api" && inspected.apiSupported === true) {
|
|
200
|
+
transport = "api";
|
|
201
|
+
apiReason = "The owner explicitly selected API for this host.";
|
|
202
|
+
} else if (inspected.customMcp === true) transport = "mcp";
|
|
203
|
+
else if (
|
|
204
|
+
inspected.customMcp === false &&
|
|
205
|
+
inspected.apiSupported === true &&
|
|
206
|
+
typeof inspected.missingMcpCapability === "string" &&
|
|
207
|
+
inspected.missingMcpCapability.trim()
|
|
208
|
+
) {
|
|
209
|
+
transport = "api";
|
|
210
|
+
apiReason = inspected.missingMcpCapability.slice(0, 500);
|
|
211
|
+
} else stop("HOST_MCP_CAPABILITY_UNVERIFIED");
|
|
212
|
+
}
|
|
213
|
+
if (!["mcp", "api"].includes(transport)) stop("TRANSPORT_UNSUPPORTED");
|
|
214
|
+
if (transport === "api" && !inspected.connection) {
|
|
215
|
+
let apiGuide;
|
|
216
|
+
for (const reader of ["primary", "alternate"]) {
|
|
217
|
+
try {
|
|
218
|
+
const value = await adapter.readApiGuide?.(reader);
|
|
219
|
+
if (
|
|
220
|
+
value?.complete === true &&
|
|
221
|
+
value.integrityVerified === true &&
|
|
222
|
+
/^[a-f0-9]{64}$/.test(value.sha256 ?? "") &&
|
|
223
|
+
value.text?.startsWith("# Connect an agent to the Totem API") &&
|
|
224
|
+
value.text.endsWith("<!-- totem-api-setup:end -->\n")
|
|
225
|
+
) {
|
|
226
|
+
apiGuide = value;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
if (!apiGuide) {
|
|
232
|
+
checks.guide = false;
|
|
233
|
+
stop("API_GUIDE_UNAVAILABLE");
|
|
234
|
+
}
|
|
235
|
+
const delivery = await adapter.explainApi(apiReason);
|
|
236
|
+
if (delivery?.ownerVisible !== true || delivery.deliveredBeforeRequest !== true)
|
|
237
|
+
stop("API_EXPLANATION_NOT_DELIVERED");
|
|
238
|
+
}
|
|
239
|
+
// A stopped connection is inspected read-only, without login, repair, recovery or tests.
|
|
240
|
+
const connection =
|
|
241
|
+
inspected.connection ?? (inspected.paused ? undefined : await adapter.connect(transport));
|
|
242
|
+
if (!connection) stop("CONNECTION_APPROVAL_REQUIRED");
|
|
243
|
+
identity = await adapter.identity(connection, transport);
|
|
244
|
+
const expected = inspected.connection?.identity ?? connection.identity;
|
|
245
|
+
if (
|
|
246
|
+
expected &&
|
|
247
|
+
(identity?.id !== expected.id ||
|
|
248
|
+
identity.environment !== expected.environment ||
|
|
249
|
+
(expected.organization?.id && identity.organization?.id !== expected.organization.id))
|
|
250
|
+
)
|
|
251
|
+
stop("AGENT_IDENTITY_MISMATCH");
|
|
252
|
+
if (options.expectedAgentId && identity?.id !== options.expectedAgentId)
|
|
253
|
+
stop("AGENT_IDENTITY_MISMATCH");
|
|
254
|
+
if (options.expectedEnvironment && identity?.environment !== options.expectedEnvironment)
|
|
255
|
+
stop("AGENT_IDENTITY_MISMATCH");
|
|
256
|
+
binding = setupBinding(identity, host, transport, connection);
|
|
257
|
+
checks.identity = true;
|
|
258
|
+
checks.transport =
|
|
259
|
+
transport === "mcp"
|
|
260
|
+
? connection.mcpIdentityVerified === true
|
|
261
|
+
: connection.apiIdentityVerified === true &&
|
|
262
|
+
connection.inboxVerified === true &&
|
|
263
|
+
connection.installationAcknowledged === true;
|
|
264
|
+
if (!checks.transport) stop("CONNECTION_VERIFICATION_INCOMPLETE");
|
|
265
|
+
if (inspected.paused) return report("RECEIVER_INTENTIONALLY_PAUSED");
|
|
266
|
+
await progress("identity_verified");
|
|
267
|
+
const context = Object.freeze({
|
|
268
|
+
identity,
|
|
269
|
+
host,
|
|
270
|
+
transport,
|
|
271
|
+
binding,
|
|
272
|
+
connectionFingerprint: connection.fingerprint,
|
|
273
|
+
guideSha256: guide.sha256,
|
|
274
|
+
});
|
|
275
|
+
operation = "configureReceiver";
|
|
276
|
+
await adapter.configureReceiver(context);
|
|
277
|
+
receiver = await adapter.readReceiver(context);
|
|
278
|
+
if (receiver?.binding !== binding || receiver.count !== 1 || receiver.sameConnection !== true)
|
|
279
|
+
stop("EXACT_RECEIVER_UNVERIFIED");
|
|
280
|
+
if (receiver.paused) return report("RECEIVER_INTENTIONALLY_PAUSED");
|
|
281
|
+
if (!Number.isFinite(receiver.intervalSeconds) || receiver.intervalSeconds <= 0)
|
|
282
|
+
stop("RECEIVER_INTERVAL_UNVERIFIED");
|
|
283
|
+
// A preserved slower interval or documented fallback remains installed and is reported honestly.
|
|
284
|
+
const operational =
|
|
285
|
+
receiver.running === true &&
|
|
286
|
+
Number.isFinite(receiver.measuredCheckSeconds) &&
|
|
287
|
+
receiver.measuredCheckSeconds > 0 &&
|
|
288
|
+
receiver.independentOfModel === true &&
|
|
289
|
+
receiver.silentWhenIdle === true &&
|
|
290
|
+
receiver.bothQueues === true &&
|
|
291
|
+
receiver.privateState === true;
|
|
292
|
+
checks.receiver = operational && receiver.intervalSeconds === 5;
|
|
293
|
+
checks.startup =
|
|
294
|
+
receiver.startupEnabled === true &&
|
|
295
|
+
receiver.restartOnFailure === true &&
|
|
296
|
+
receiver.persistent === true;
|
|
297
|
+
if (!sameHost(host, await adapter.readHost(context))) {
|
|
298
|
+
checks.model = false;
|
|
299
|
+
stop("CURRENT_MODEL_CHANGED");
|
|
300
|
+
}
|
|
301
|
+
if (!operational || !checks.startup)
|
|
302
|
+
return {
|
|
303
|
+
...report("RECEIVER_CAPABILITY_INCOMPLETE"),
|
|
304
|
+
intervalSeconds: receiver.intervalSeconds,
|
|
305
|
+
};
|
|
306
|
+
await progress("receiver_verified");
|
|
307
|
+
const recovery = await adapter.verifyRecovery(context);
|
|
308
|
+
checks.recovery =
|
|
309
|
+
recovery?.binding === binding &&
|
|
310
|
+
recovery.newRunVerified === true &&
|
|
311
|
+
recovery.savedStatePreserved === true &&
|
|
312
|
+
recovery.ownerPausePreserved === true &&
|
|
313
|
+
recovery.noDuplicateReceiver === true;
|
|
314
|
+
if (!checks.recovery) return report("RECOVERY_UNVERIFIED");
|
|
315
|
+
await progress("recovery_verified");
|
|
316
|
+
// This requests incoming tests from the owner. It never sends traffic to another agent.
|
|
317
|
+
await adapter.requestTests(context);
|
|
318
|
+
proofs = await adapter.readTests(context);
|
|
319
|
+
const peer = proofs?.peer,
|
|
320
|
+
chat = proofs?.ownerChat;
|
|
321
|
+
checks.nativeWake =
|
|
322
|
+
peer?.binding === binding &&
|
|
323
|
+
peer.nativeSessionId === host.sessionId &&
|
|
324
|
+
peer.newMessageTriggeredWake === true &&
|
|
325
|
+
peer.appearedInNativeChat === true;
|
|
326
|
+
checks.peerReply =
|
|
327
|
+
peer?.binding === binding &&
|
|
328
|
+
peer.nativeSessionId === host.sessionId &&
|
|
329
|
+
peer.appearedInNativeChat === true &&
|
|
330
|
+
uuid(peer.messageId) &&
|
|
331
|
+
uuid(peer.replyId) &&
|
|
332
|
+
peer.correlated === true &&
|
|
333
|
+
peer.exactlyOneReply === true &&
|
|
334
|
+
peer.acknowledgedAfterReply === true &&
|
|
335
|
+
peer.authorized === true &&
|
|
336
|
+
Number.isFinite(peer.receivedAt) &&
|
|
337
|
+
Number.isFinite(peer.repliedAt) &&
|
|
338
|
+
peer.repliedAt >= peer.receivedAt;
|
|
339
|
+
checks.ownerChat =
|
|
340
|
+
chat?.binding === binding &&
|
|
341
|
+
uuid(chat.commandId) &&
|
|
342
|
+
uuid(chat.chatId) &&
|
|
343
|
+
uuid(chat.eventId) &&
|
|
344
|
+
chat.claimed === true &&
|
|
345
|
+
chat.leaseRenewed === true &&
|
|
346
|
+
chat.completedInOriginalChat === true &&
|
|
347
|
+
chat.authorized === true &&
|
|
348
|
+
Number.isFinite(chat.queuedAt) &&
|
|
349
|
+
Number.isFinite(chat.completedAt) &&
|
|
350
|
+
chat.completedAt >= chat.queuedAt;
|
|
351
|
+
if (!sameHost(host, await adapter.readHost(context))) {
|
|
352
|
+
checks.model = false;
|
|
353
|
+
stop("CURRENT_MODEL_CHANGED");
|
|
354
|
+
}
|
|
355
|
+
await progress("tests_checked");
|
|
356
|
+
return report(
|
|
357
|
+
!checks.peerReply || !checks.ownerChat
|
|
358
|
+
? "AUTHORIZED_TESTS_PENDING"
|
|
359
|
+
: !checks.receiver
|
|
360
|
+
? "RECEIVER_INTERVAL_LIMITATION"
|
|
361
|
+
: !checks.nativeWake
|
|
362
|
+
? "NATIVE_WAKE_UNVERIFIED"
|
|
363
|
+
: undefined,
|
|
364
|
+
);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
// Adapter error messages may contain private host diagnostics. Export only bounded codes.
|
|
367
|
+
const code = /^[A-Z][A-Z0-9_]{0,79}$/.test(error?.code ?? "")
|
|
368
|
+
? error.code
|
|
369
|
+
: "SETUP_ADAPTER_FAILED";
|
|
370
|
+
if (code === "HOST_POLICY_DENIED") {
|
|
371
|
+
const reason =
|
|
372
|
+
typeof error.publicReason === "string"
|
|
373
|
+
? error.publicReason
|
|
374
|
+
.slice(0, 1000)
|
|
375
|
+
.replace(/tn_(?:live|test)_[A-Za-z0-9_-]+|Bearer\s+\S+/gi, "[redacted]")
|
|
376
|
+
: "The host did not supply a public reason.";
|
|
377
|
+
return { ...report(code), deniedOperation: operation, statedReason: reason };
|
|
378
|
+
}
|
|
379
|
+
return report(code);
|
|
380
|
+
} finally {
|
|
381
|
+
await Promise.resolve()
|
|
382
|
+
.then(() => adapter.close?.())
|
|
383
|
+
.catch(() => {});
|
|
384
|
+
}
|
|
385
|
+
}
|
package/bin/totem.mjs
CHANGED
|
@@ -1754,6 +1754,11 @@ async function runRfq(options, baseUrl) {
|
|
|
1754
1754
|
}
|
|
1755
1755
|
|
|
1756
1756
|
async function main() {
|
|
1757
|
+
if (process.argv[2] === "install-worker") {
|
|
1758
|
+
const { strictSetupWorkerMain } = await import("./strict-setup-jobs.mjs");
|
|
1759
|
+
process.exitCode = await strictSetupWorkerMain(process.argv[3]);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1757
1762
|
if (process.argv[2] === "update") {
|
|
1758
1763
|
const { runtimeUpdateMain } = await import("./managed-mcp.mjs");
|
|
1759
1764
|
await runtimeUpdateMain(process.argv.slice(3));
|
|
@@ -1769,7 +1774,12 @@ async function main() {
|
|
|
1769
1774
|
process.exitCode = await executorDiagnosticMain(process.argv.slice(3));
|
|
1770
1775
|
return;
|
|
1771
1776
|
}
|
|
1772
|
-
if (
|
|
1777
|
+
if (process.argv[2] === "install") {
|
|
1778
|
+
const { strictInstallationMain } = await import("./strict-installation.mjs");
|
|
1779
|
+
process.exitCode = await strictInstallationMain(process.argv.slice(3));
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
if (!process.argv[2] || process.argv[2] === "login") {
|
|
1773
1783
|
const { installationMain } = await import("./installation.mjs");
|
|
1774
1784
|
process.exitCode = await installationMain(process.argv.slice(3), {});
|
|
1775
1785
|
return;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { lstat } from "node:fs/promises";
|
|
5
|
+
|
|
6
|
+
const execute = promisify(execFile);
|
|
7
|
+
export function validateWindowsPrivateAcl(snapshot) {
|
|
8
|
+
const sid = snapshot?.sid;
|
|
9
|
+
if (typeof sid !== "string" || !/^S-1-(?:\d+-){1,14}\d+$/.test(sid))
|
|
10
|
+
throw new Error("The Windows account SID could not be verified.");
|
|
11
|
+
const trusted = new Set([sid, "S-1-5-18", "S-1-5-32-544"]);
|
|
12
|
+
const trustedAncestors = new Set([
|
|
13
|
+
...trusted,
|
|
14
|
+
"S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464",
|
|
15
|
+
]);
|
|
16
|
+
if (
|
|
17
|
+
snapshot.owner !== sid ||
|
|
18
|
+
snapshot.reparse ||
|
|
19
|
+
!Array.isArray(snapshot.rules) ||
|
|
20
|
+
!Array.isArray(snapshot.ancestors)
|
|
21
|
+
)
|
|
22
|
+
throw new Error("The private Windows path has an unsafe owner or reparse point.");
|
|
23
|
+
let readable = false;
|
|
24
|
+
for (const rule of snapshot.rules) {
|
|
25
|
+
if (
|
|
26
|
+
!Number.isSafeInteger(rule.rights) ||
|
|
27
|
+
rule.rights < 0 ||
|
|
28
|
+
!["Allow", "Deny"].includes(rule.type)
|
|
29
|
+
)
|
|
30
|
+
throw new Error("The Windows access rules could not be verified.");
|
|
31
|
+
if (rule.type === "Allow" && !trusted.has(rule.sid))
|
|
32
|
+
throw new Error("The private Windows path grants access to another principal.");
|
|
33
|
+
if (rule.type === "Allow" && rule.sid === sid && (rule.rights & 131073) === 131073)
|
|
34
|
+
readable = true;
|
|
35
|
+
if (rule.type === "Deny" && rule.sid === sid && rule.rights & 131073)
|
|
36
|
+
throw new Error("The private Windows path denies required owner access.");
|
|
37
|
+
}
|
|
38
|
+
if (
|
|
39
|
+
!readable ||
|
|
40
|
+
snapshot.ancestors.some(
|
|
41
|
+
(parent) =>
|
|
42
|
+
parent.reparse ||
|
|
43
|
+
!trustedAncestors.has(parent.owner) ||
|
|
44
|
+
!Array.isArray(parent.rules) ||
|
|
45
|
+
parent.rules.some(
|
|
46
|
+
(rule) =>
|
|
47
|
+
!rule.inheritOnly &&
|
|
48
|
+
rule.type === "Allow" &&
|
|
49
|
+
!trustedAncestors.has(rule.sid) &&
|
|
50
|
+
(rule.rights & (64 | 262144 | 524288)) !== 0,
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
throw new Error("The private Windows path or its parent can be replaced by another principal.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function assertWindowsPrivatePath(path) {
|
|
58
|
+
if (process.platform !== "win32") return;
|
|
59
|
+
await lstat(path); // Preserve ENOENT for callers that create a new private file.
|
|
60
|
+
const encoded = Buffer.from(resolve(path), "utf8").toString("base64");
|
|
61
|
+
const script = `$ErrorActionPreference='Stop'
|
|
62
|
+
$path=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}'))
|
|
63
|
+
$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
64
|
+
function Rules($acl) { @($acl.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier]) | ForEach-Object { @{sid=$_.IdentityReference.Value;rights=[int64]$_.FileSystemRights;type=$_.AccessControlType.ToString();inheritOnly=[bool]($_.PropagationFlags -band [Security.AccessControl.PropagationFlags]::InheritOnly)} }) }
|
|
65
|
+
$item=Get-Item -LiteralPath $path -Force
|
|
66
|
+
$acl=Get-Acl -LiteralPath $path
|
|
67
|
+
$parents=@(); $parent=Split-Path -LiteralPath $path
|
|
68
|
+
while ($parent) {
|
|
69
|
+
$p=Get-Item -LiteralPath $parent -Force
|
|
70
|
+
$parentAcl=Get-Acl -LiteralPath $parent
|
|
71
|
+
$parents+=@{owner=$parentAcl.GetOwner([Security.Principal.SecurityIdentifier]).Value;reparse=[bool]($p.Attributes -band [IO.FileAttributes]::ReparsePoint);rules=@(Rules $parentAcl)}
|
|
72
|
+
$next=Split-Path -LiteralPath $parent
|
|
73
|
+
if ($next -eq $parent) { break }; $parent=$next
|
|
74
|
+
}
|
|
75
|
+
@{sid=$sid;owner=$acl.GetOwner([Security.Principal.SecurityIdentifier]).Value;reparse=[bool]($item.Attributes -band [IO.FileAttributes]::ReparsePoint);rules=@(Rules $acl);ancestors=$parents} | ConvertTo-Json -Depth 8 -Compress`;
|
|
76
|
+
const systemRoot = process.env.SystemRoot ?? process.env.SYSTEMROOT;
|
|
77
|
+
if (!systemRoot) throw new Error("The Windows system directory could not be verified.");
|
|
78
|
+
const { stdout } = await execute(
|
|
79
|
+
join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
|
|
80
|
+
[
|
|
81
|
+
"-NoProfile",
|
|
82
|
+
"-NonInteractive",
|
|
83
|
+
"-EncodedCommand",
|
|
84
|
+
Buffer.from(script, "utf16le").toString("base64"),
|
|
85
|
+
],
|
|
86
|
+
{ timeout: 10000, maxBuffer: 262144, windowsHide: true },
|
|
87
|
+
);
|
|
88
|
+
validateWindowsPrivateAcl(JSON.parse(stdout.replace(/^\uFEFF/, "")));
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "totemlayer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.75",
|
|
4
4
|
"description": "Cross-platform CLI for TotemLayer agent identity and authorization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,9 +33,10 @@
|
|
|
33
33
|
"registry": "https://registry.npmjs.org/"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@modelcontextprotocol/client": "2.3.0",
|
|
36
37
|
"@modelcontextprotocol/server": "2.3.0",
|
|
37
|
-
"undici": "7.30.
|
|
38
|
-
"zod": "4.
|
|
38
|
+
"undici": "7.30.1",
|
|
39
|
+
"zod": "4.6.6"
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
42
|
"postinstall": "node bin/installation.mjs --postinstall",
|
package/scripts/prepare.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { access, readdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { transform } from "esbuild";
|
|
3
3
|
import "./build-native.mjs";
|
|
4
4
|
import { generateNetworkContract } from "../../../scripts/generate-network-contract.mjs";
|
|
5
|
+
import "../../../scripts/build-strict-installer-guide.mjs";
|
|
5
6
|
await generateNetworkContract();
|
|
6
7
|
// This candidate rebases coordination on the immutable published 0.1.21 bundle.
|
|
7
8
|
// Its bundled SDK is the release source; do not rebuild it from an older sibling checkout.
|
|
@@ -18,6 +19,7 @@ for (const name of [
|
|
|
18
19
|
"runtime-diagnostics.mjs",
|
|
19
20
|
"codex-chat-bridge.mjs",
|
|
20
21
|
"codex-human-chat-bridge.mjs",
|
|
22
|
+
"codex-mission-bridge.mjs",
|
|
21
23
|
"chat-prompt-limit.mjs",
|
|
22
24
|
"worker-core/adapters.mjs",
|
|
23
25
|
"worker-core/errors.mjs",
|