totemlayer 0.1.96 → 0.1.115
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 +48 -6
- package/bin/automatic-runtime.mjs +2 -1
- package/bin/cloud-supervisor.mjs +44 -0
- package/bin/codex-mission-bridge.mjs +6 -2
- package/bin/configured-native-installation.mjs +57 -0
- package/bin/connected-agent-service.mjs +5 -1
- package/bin/device-login.mjs +3 -1
- package/bin/inbox-queue.mjs +8 -5
- package/bin/mcp.mjs +18 -5
- package/bin/mission-agent-service.mjs +13 -4
- package/bin/native/darwin-lifetime +0 -0
- package/bin/native/darwin-lifetime.json +1 -1
- package/bin/native-claude-login.mjs +131 -0
- package/bin/native-claude-receiver.mjs +272 -0
- package/bin/native-cli-process.py +105 -0
- package/bin/native-harness-environment.mjs +20 -0
- package/bin/native-hermes-bridge.mjs +99 -0
- package/bin/native-hermes-installation.mjs +9 -0
- package/bin/native-hermes-mcp.mjs +107 -0
- package/bin/native-hermes-mcp.py +205 -0
- package/bin/native-hermes-metadata.py +40 -0
- package/bin/native-hermes-process.py +62 -0
- package/bin/native-hermes-receiver.mjs +28 -0
- package/bin/native-inbox-installation.mjs +144 -0
- package/bin/native-inbox-receiver.mjs +133 -0
- package/bin/native-kilo-installation.mjs +4 -0
- package/bin/native-kilo-receiver.mjs +1 -0
- package/bin/native-mcp-source.mjs +244 -0
- package/bin/native-opencode-bridge.mjs +108 -0
- package/bin/native-opencode-installation.mjs +15 -0
- package/bin/native-opencode-mcp.mjs +112 -0
- package/bin/native-opencode-receiver.mjs +22 -0
- package/bin/native-pi-bridge.mjs +170 -0
- package/bin/native-pi-installation.mjs +9 -0
- package/bin/native-pi-mcp.mjs +139 -0
- package/bin/native-pi-receiver.mjs +28 -0
- package/bin/network-retry.mjs +9 -1
- package/bin/platform-client.mjs +2 -5
- package/bin/runtime-installation.mjs +9 -4
- package/bin/sdk/errors.d.ts +1 -1
- package/bin/sdk/errors.js +1 -1
- package/bin/sdk/listener.d.ts +1 -0
- package/bin/sdk/listener.js +1 -1
- package/bin/sdk/transport.js +1 -1
- package/bin/setup-assets/api-manifest.json +1 -1
- package/bin/setup-assets/manifest.json +1 -1
- package/bin/setup-assets/setup.backup.md +2 -2
- package/bin/setup-assets/setup.md +2 -2
- package/bin/startup-diagnostics.mjs +19 -2
- package/bin/strict-installation.mjs +29 -3
- package/bin/supervisor-watchdog.mjs +44 -0
- package/bin/totem.mjs +6 -0
- package/bin/windows-private-path.mjs +227 -32
- package/package.json +2 -2
|
@@ -4,6 +4,10 @@ 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
|
+
RUNTIME_RATE_LIMITED: [
|
|
8
|
+
"Totem requests are waiting for the server's cooldown.",
|
|
9
|
+
"Keep this connection and its queued messages. Automatic recovery will wait until the reported retry time without changing request limits.",
|
|
10
|
+
],
|
|
7
11
|
CHAT_PROMPT_RATE_LIMITED: [
|
|
8
12
|
"Automatic chat prompts are waiting for their existing cooldown.",
|
|
9
13
|
"Keep saved messages queued. Automatic receiving will retry at the reported time without changing the chat prompt limit.",
|
|
@@ -184,7 +188,9 @@ export function startupDiagnostic(error) {
|
|
|
184
188
|
);
|
|
185
189
|
const match = chain
|
|
186
190
|
.toReversed()
|
|
187
|
-
.find(
|
|
191
|
+
.find(
|
|
192
|
+
(item) => known(item.code) || known(item.causeCode) || [401, 403, 429].includes(item.status),
|
|
193
|
+
);
|
|
188
194
|
const causeCode = denied
|
|
189
195
|
? "HOST_APPROVAL_DENIED"
|
|
190
196
|
: match
|
|
@@ -192,16 +198,27 @@ export function startupDiagnostic(error) {
|
|
|
192
198
|
? match.code
|
|
193
199
|
: known(match.causeCode)
|
|
194
200
|
? match.causeCode
|
|
195
|
-
:
|
|
201
|
+
: match.status === 429
|
|
202
|
+
? "RUNTIME_RATE_LIMITED"
|
|
203
|
+
: "RUNTIME_AUTHORIZATION_REJECTED"
|
|
196
204
|
: "STARTUP_CAUSE_UNAVAILABLE";
|
|
197
205
|
const [cause, recovery, retryable = true] = causes[causeCode];
|
|
198
206
|
const retryAt = match?.retryAt;
|
|
199
207
|
const now = Date.now();
|
|
208
|
+
const seconds = match?.retryAfterSeconds;
|
|
209
|
+
const serverRetryAt =
|
|
210
|
+
causeCode === "RUNTIME_RATE_LIMITED" &&
|
|
211
|
+
Number.isSafeInteger(seconds) &&
|
|
212
|
+
seconds > 0 &&
|
|
213
|
+
seconds <= 86400
|
|
214
|
+
? now + seconds * 1000
|
|
215
|
+
: undefined;
|
|
200
216
|
return {
|
|
201
217
|
causeCode,
|
|
202
218
|
cause,
|
|
203
219
|
recovery,
|
|
204
220
|
retryable,
|
|
221
|
+
...(serverRetryAt ? { retryAt: serverRetryAt } : {}),
|
|
205
222
|
...(causeCode === "CHAT_PROMPT_RATE_LIMITED" &&
|
|
206
223
|
Number.isFinite(retryAt) &&
|
|
207
224
|
retryAt > now &&
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { GUIDE_UNAVAILABLE, runStrictSetup } from "./strict-setup.mjs";
|
|
3
3
|
import { readReleaseSetupGuide } from "./setup-guide.mjs";
|
|
4
|
+
import { NATIVE_INSTALL_PROVIDERS } from "./configured-native-installation.mjs";
|
|
4
5
|
|
|
5
6
|
export const STRICT_INSTALL_HELP = `Install Totem with every required verification
|
|
6
7
|
|
|
@@ -9,6 +10,15 @@ export const STRICT_INSTALL_HELP = `Install Totem with every required verificati
|
|
|
9
10
|
[--device-login --no-browser]
|
|
10
11
|
totem install --read-guide
|
|
11
12
|
totem install --status
|
|
13
|
+
totem install --provider pi|hermes|opencode|kilo|claude --native-config /private/binding.json
|
|
14
|
+
--session CURRENT_NATIVE_SESSION --model CURRENT_MODEL [--status] [--json]
|
|
15
|
+
totem install --provider claude --native-config /private/binding.json
|
|
16
|
+
--session CURRENT_NATIVE_SESSION --model CURRENT_MODEL --agent-id UUID --environment PRODUCTION|SANDBOX
|
|
17
|
+
--device-login [--no-browser] [--wait-seconds 1200] [--json]
|
|
18
|
+
|
|
19
|
+
Native profiles use their existing sign-in and a private owner-prepared binding.
|
|
20
|
+
This registers/resumes the exact native receiver; it reports incomplete until live
|
|
21
|
+
receiving, restart, peer and website-owner acceptance are separately verified.
|
|
12
22
|
|
|
13
23
|
Run inside the intended agent's command-capable host. The current native session and
|
|
14
24
|
model must be verified; an installed client is never selected as a substitute.
|
|
@@ -37,6 +47,7 @@ export async function strictInstallationMain(args = [], options = {}) {
|
|
|
37
47
|
environment: { type: "string" },
|
|
38
48
|
"base-url": { type: "string" },
|
|
39
49
|
session: { type: "string" },
|
|
50
|
+
"native-config": { type: "string" },
|
|
40
51
|
model: { type: "string" },
|
|
41
52
|
"wait-seconds": { type: "string", default: "1200" },
|
|
42
53
|
"read-guide": { type: "boolean" },
|
|
@@ -94,8 +105,12 @@ export async function strictInstallationMain(args = [], options = {}) {
|
|
|
94
105
|
/tn_(?:live|test)_/.test(values.model))
|
|
95
106
|
)
|
|
96
107
|
throw new Error();
|
|
97
|
-
|
|
108
|
+
const sessionPattern = values.provider === "hermes" ? /^\d{8}_\d{6}_[a-f0-9]{6}$/
|
|
109
|
+
: ["opencode", "kilo"].includes(values.provider) ? /^ses_[A-Za-z0-9]{12,80}$/
|
|
110
|
+
: /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
|
|
111
|
+
if (values.session && !sessionPattern.test(values.session))
|
|
98
112
|
throw new Error();
|
|
113
|
+
if (values["native-config"] && !NATIVE_INSTALL_PROVIDERS.includes(values.provider)) throw new Error();
|
|
99
114
|
const env = options.env ?? process.env;
|
|
100
115
|
flags = {
|
|
101
116
|
provider: values.provider ?? (env.CODEX_THREAD_ID ? "codex" : undefined),
|
|
@@ -106,10 +121,12 @@ export async function strictInstallationMain(args = [], options = {}) {
|
|
|
106
121
|
model: values.model,
|
|
107
122
|
waitSeconds,
|
|
108
123
|
json: values.json === true,
|
|
124
|
+
...(values["native-config"] ? { nativeConfig: values["native-config"] } : {}),
|
|
109
125
|
};
|
|
110
126
|
if (values["device-login"]) flags.deviceLogin = true;
|
|
111
127
|
if (values["no-browser"]) flags.noBrowser = true;
|
|
112
|
-
if (values.status)
|
|
128
|
+
if (values.status && flags.nativeConfig) flags.status = true;
|
|
129
|
+
else if (values.status) {
|
|
113
130
|
flags.sessionId ??= env.CODEX_THREAD_ID;
|
|
114
131
|
const { readStrictSetupJob } = await import("./strict-setup-jobs.mjs");
|
|
115
132
|
const publicStatus = { ...(await readStrictSetupJob(flags, env)) };
|
|
@@ -130,6 +147,15 @@ export async function strictInstallationMain(args = [], options = {}) {
|
|
|
130
147
|
return 2;
|
|
131
148
|
}
|
|
132
149
|
const write = async (value) => stdout.write(JSON.stringify(value) + "\n");
|
|
150
|
+
if (flags.nativeConfig) {
|
|
151
|
+
try {
|
|
152
|
+
const { installConfiguredNativeReceiver } = await import("./configured-native-installation.mjs");
|
|
153
|
+
await write({ ...await installConfiguredNativeReceiver(flags, { ...options, write }), complete: false });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
await write({ complete: false, state: "incomplete", code: /^[A-Z][A-Z0-9_]{0,79}$/.test(error.code ?? "") ? error.code : "NATIVE_INSTALL_FAILED" });
|
|
156
|
+
}
|
|
157
|
+
return 2;
|
|
158
|
+
}
|
|
133
159
|
if (!options.adapter && !options.foreground && flags.provider === "codex") {
|
|
134
160
|
// Return before readiness needs a model turn in this same native chat.
|
|
135
161
|
const adapter = (await import("./strict-codex-adapter.mjs")).createStrictCodexAdapter(flags, {
|
|
@@ -190,7 +216,7 @@ export async function strictInstallationMain(args = [], options = {}) {
|
|
|
190
216
|
readGuide: readReleaseSetupGuide,
|
|
191
217
|
async inspect() {
|
|
192
218
|
throw Object.assign(new Error(), {
|
|
193
|
-
code: flags.provider ? "HOST_ADAPTER_UNSUPPORTED" : "EXACT_NATIVE_HOST_REQUIRED",
|
|
219
|
+
code: ["pi", "hermes", "opencode", "kilo"].includes(flags.provider) ? "NATIVE_CONFIGURATION_REQUIRED" : flags.provider ? "HOST_ADAPTER_UNSUPPORTED" : "EXACT_NATIVE_HOST_REQUIRED",
|
|
194
220
|
});
|
|
195
221
|
},
|
|
196
222
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { privateRead, privateWrite } from "./runtime-installation.mjs";
|
|
3
|
+
|
|
4
|
+
/** The launcher owns this ChildProcess; never discover or signal a saved PID here. */
|
|
5
|
+
export function watchSupervisorChild(child, directory, options = {}) {
|
|
6
|
+
const heartbeatFile = options.heartbeatFile ?? "supervisor.json";
|
|
7
|
+
if (!["supervisor.json", "status.json"].includes(heartbeatFile)) throw new Error("Invalid supervisor heartbeat file.");
|
|
8
|
+
const timeoutMs = options.timeoutMs ?? 300000;
|
|
9
|
+
const started = Date.now();
|
|
10
|
+
let checking = false, stopped = false, terminating = false, force;
|
|
11
|
+
const alive = () => child.exitCode === null && child.signalCode === null;
|
|
12
|
+
const stop = () => {
|
|
13
|
+
stopped = true;
|
|
14
|
+
clearInterval(timer);
|
|
15
|
+
clearTimeout(force);
|
|
16
|
+
child.removeListener("exit", stop);
|
|
17
|
+
child.removeListener("error", stop);
|
|
18
|
+
};
|
|
19
|
+
const timer = setInterval(async () => {
|
|
20
|
+
if (stopped || checking || terminating || !alive()) return;
|
|
21
|
+
checking = true;
|
|
22
|
+
try {
|
|
23
|
+
const state = await privateRead(join(directory, heartbeatFile)).catch(() => null);
|
|
24
|
+
if (stopped || !alive()) return;
|
|
25
|
+
const now = Date.now(), updated = Date.parse(state?.updatedAt);
|
|
26
|
+
// A deliberately paused dedicated inbox is quiet by design. The stored
|
|
27
|
+
// PID must still match the child this launcher owns, never another job.
|
|
28
|
+
if (options.allowPaused === true && state?.pid === child.pid && state.state === "paused") return;
|
|
29
|
+
if (now - started < timeoutMs ||
|
|
30
|
+
(state?.pid === child.pid && Number.isFinite(updated) && updated <= now + 5000 && now - updated < timeoutMs)) return;
|
|
31
|
+
terminating = true;
|
|
32
|
+
void privateWrite(join(directory, "supervisor-watchdog.json"), {
|
|
33
|
+
code: "SUPERVISOR_HEARTBEAT_TIMEOUT", pid: child.pid, updatedAt: new Date(now).toISOString(),
|
|
34
|
+
}).catch(() => {});
|
|
35
|
+
child.kill("SIGTERM");
|
|
36
|
+
force = setTimeout(() => { if (alive()) child.kill("SIGKILL"); }, options.graceMs ?? 5000);
|
|
37
|
+
force.unref();
|
|
38
|
+
} finally { checking = false; }
|
|
39
|
+
}, options.intervalMs ?? 15000);
|
|
40
|
+
timer.unref();
|
|
41
|
+
child.once("exit", stop);
|
|
42
|
+
child.once("error", stop);
|
|
43
|
+
return stop;
|
|
44
|
+
}
|
package/bin/totem.mjs
CHANGED
|
@@ -251,6 +251,7 @@ Connect once and receive messages and approved Mission work
|
|
|
251
251
|
totem diagnose [--profile NAME] [--fresh] [--json]
|
|
252
252
|
totem repair-startup [--expected-task-sha256 HEX] [--json]
|
|
253
253
|
totem start --background
|
|
254
|
+
totem supervise --help
|
|
254
255
|
totem stop
|
|
255
256
|
|
|
256
257
|
Agent messaging and local runtime
|
|
@@ -1755,6 +1756,11 @@ async function runRfq(options, baseUrl) {
|
|
|
1755
1756
|
}
|
|
1756
1757
|
|
|
1757
1758
|
async function main() {
|
|
1759
|
+
if (process.argv[2] === "supervise") {
|
|
1760
|
+
const { cloudSupervisorMain } = await import("./cloud-supervisor.mjs");
|
|
1761
|
+
process.exitCode = await cloudSupervisorMain(process.argv.slice(3));
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1758
1764
|
if (process.argv[2] === "repair-startup") {
|
|
1759
1765
|
const { startupRepairMain } = await import("./startup-repair.mjs");
|
|
1760
1766
|
process.exitCode = await startupRepairMain(process.argv.slice(3));
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createConnection } from "node:net";
|
|
3
|
+
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
5
|
import { join, resolve } from "node:path";
|
|
4
6
|
import { lstat } from "node:fs/promises";
|
|
5
7
|
|
|
6
|
-
const execute = promisify(execFile);
|
|
7
8
|
export function validateWindowsPrivateAcl(snapshot) {
|
|
8
9
|
const sid = snapshot?.sid;
|
|
9
10
|
if (typeof sid !== "string" || !/^S-1-(?:\d+-){1,14}\d+$/.test(sid))
|
|
@@ -54,36 +55,230 @@ export function validateWindowsPrivateAcl(snapshot) {
|
|
|
54
55
|
throw new Error("The private Windows path or its parent can be replaced by another principal.");
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
// Keep the native process, never its results. Every request reads the current
|
|
59
|
+
// file and all ancestor ACLs again, including after a permission change.
|
|
60
|
+
const inspectionScript = (pipeName) => `$ErrorActionPreference='Stop'
|
|
61
|
+
[Console]::Error.WriteLine('TOTEM_ACL_START')
|
|
62
|
+
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)} }) }
|
|
63
|
+
$security=[IO.Pipes.PipeSecurity]::new()
|
|
64
|
+
$owner=[Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
65
|
+
$security.SetOwner($owner)
|
|
66
|
+
$security.SetAccessRuleProtection($true,$false)
|
|
67
|
+
$security.AddAccessRule([IO.Pipes.PipeAccessRule]::new($owner,[IO.Pipes.PipeAccessRights]::FullControl,[Security.AccessControl.AccessControlType]::Allow))
|
|
68
|
+
$pipe=[IO.Pipes.NamedPipeServerStream]::new('${pipeName}',[IO.Pipes.PipeDirection]::InOut,1,[IO.Pipes.PipeTransmissionMode]::Byte,[IO.Pipes.PipeOptions]::None,4096,4096,$security)
|
|
69
|
+
[Console]::Error.WriteLine('TOTEM_ACL_PIPE_READY')
|
|
70
|
+
try {
|
|
71
|
+
$pipe.WaitForConnection()
|
|
72
|
+
[Console]::Error.WriteLine('TOTEM_ACL_CONNECTED')
|
|
73
|
+
$hello=[Text.Encoding]::UTF8.GetBytes('{"id":0,"nonce":"'+$env:TOTEM_ACL_CHALLENGE+'"}'+"\`n")
|
|
74
|
+
$pipe.Write($hello,0,$hello.Length)
|
|
75
|
+
[Console]::Error.WriteLine('TOTEM_ACL_HELLO_SENT')
|
|
76
|
+
[byte[]]$buffer=[Array]::CreateInstance([byte],4096)
|
|
77
|
+
[Console]::Error.WriteLine('TOTEM_ACL_BUFFER_READY')
|
|
78
|
+
$pending=''
|
|
79
|
+
function Send-Frame($frame) {
|
|
80
|
+
$bytes=[Text.Encoding]::UTF8.GetBytes(($frame | ConvertTo-Json -Depth 9 -Compress)+"\`n")
|
|
81
|
+
$pipe.Write($bytes,0,$bytes.Length)
|
|
82
|
+
}
|
|
83
|
+
while ($true) {
|
|
84
|
+
while (($end=$pending.IndexOf("\`n")) -lt 0) {
|
|
85
|
+
[Console]::Error.WriteLine('TOTEM_ACL_READING')
|
|
86
|
+
$length=$pipe.Read($buffer,0,$buffer.Length)
|
|
87
|
+
if ($length -eq 0) { return }
|
|
88
|
+
[Console]::Error.WriteLine('TOTEM_ACL_FRAME_READ')
|
|
89
|
+
$pending += [Text.Encoding]::ASCII.GetString($buffer,0,$length)
|
|
90
|
+
if ($pending.Length -gt 65536) { throw 'ACL request too large' }
|
|
91
|
+
}
|
|
92
|
+
$line=$pending.Substring(0,$end)
|
|
93
|
+
$pending=$pending.Substring($end+1)
|
|
94
|
+
[Console]::Error.WriteLine('TOTEM_ACL_REQUEST_RECEIVED')
|
|
95
|
+
$request=$null
|
|
96
|
+
try {
|
|
97
|
+
$request=$line | ConvertFrom-Json
|
|
98
|
+
$path=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($request.path))
|
|
99
|
+
$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
100
|
+
$item=Get-Item -LiteralPath $path -Force
|
|
101
|
+
$acl=Get-Acl -LiteralPath $path
|
|
102
|
+
$parents=@(); $parent=Split-Path -LiteralPath $path
|
|
103
|
+
while ($parent) {
|
|
104
|
+
$p=Get-Item -LiteralPath $parent -Force
|
|
105
|
+
$parentAcl=Get-Acl -LiteralPath $parent
|
|
106
|
+
$parents+=@{owner=$parentAcl.GetOwner([Security.Principal.SecurityIdentifier]).Value;reparse=[bool]($p.Attributes -band [IO.FileAttributes]::ReparsePoint);rules=@(Rules $parentAcl)}
|
|
107
|
+
$next=Split-Path -LiteralPath $parent
|
|
108
|
+
if ($next -eq $parent) { break }; $parent=$next
|
|
109
|
+
}
|
|
110
|
+
$snapshot=@{sid=$sid;owner=$acl.GetOwner([Security.Principal.SecurityIdentifier]).Value;reparse=[bool]($item.Attributes -band [IO.FileAttributes]::ReparsePoint);rules=@(Rules $acl);ancestors=$parents}
|
|
111
|
+
Send-Frame @{id=$request.id;snapshot=$snapshot}
|
|
112
|
+
[Console]::Error.WriteLine('TOTEM_ACL_RESULT_SENT')
|
|
113
|
+
} catch {
|
|
114
|
+
Send-Frame @{id=$request.id;error='ACL inspection failed'}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} finally { $pipe.Dispose() }
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
export function createWindowsAclReader(options = {}) {
|
|
121
|
+
let worker,
|
|
122
|
+
nextId = 0;
|
|
123
|
+
const unavailable = () => new Error("The Windows access rules could not be verified.");
|
|
124
|
+
function stop(current) {
|
|
125
|
+
if (current.stopped) return;
|
|
126
|
+
current.stopped = true;
|
|
127
|
+
if (worker === current) worker = undefined;
|
|
128
|
+
clearTimeout(current.idle);
|
|
129
|
+
clearTimeout(current.reconnect);
|
|
130
|
+
for (const { timer, reject } of current.pending.values()) {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
reject(unavailable());
|
|
133
|
+
}
|
|
134
|
+
current.pending.clear();
|
|
135
|
+
current.socket?.destroy();
|
|
136
|
+
current.child?.kill();
|
|
137
|
+
}
|
|
138
|
+
function start() {
|
|
139
|
+
const environment = options.environment ?? process.env;
|
|
140
|
+
const systemRoot = options.systemRoot ?? environment.SystemRoot ?? environment.SYSTEMROOT;
|
|
141
|
+
if (!systemRoot) throw new Error("The Windows system directory could not be verified.");
|
|
142
|
+
const name = `totem-acl-${process.pid}-${randomUUID()}`;
|
|
143
|
+
const nonce = randomUUID();
|
|
144
|
+
const current = {
|
|
145
|
+
pending: new Map(),
|
|
146
|
+
buffer: "",
|
|
147
|
+
decoder: new StringDecoder("utf8"),
|
|
148
|
+
nonce,
|
|
149
|
+
stopped: false,
|
|
150
|
+
authenticated: false,
|
|
151
|
+
};
|
|
152
|
+
worker = current;
|
|
153
|
+
const child = (options.spawn ?? spawn)(
|
|
154
|
+
join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
|
|
155
|
+
[
|
|
156
|
+
"-NoProfile",
|
|
157
|
+
"-NonInteractive",
|
|
158
|
+
"-EncodedCommand",
|
|
159
|
+
Buffer.from(inspectionScript(name), "utf16le").toString("base64"),
|
|
160
|
+
],
|
|
161
|
+
{
|
|
162
|
+
// The encoded command uses a protected native pipe independently of stdin;
|
|
163
|
+
// no file paths or secrets are written to stdio.
|
|
164
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
165
|
+
windowsHide: true,
|
|
166
|
+
env: {
|
|
167
|
+
...Object.fromEntries(
|
|
168
|
+
Object.entries(environment).filter(([name]) =>
|
|
169
|
+
/^(PATH|PATHEXT|COMSPEC|TEMP|TMP|USERPROFILE|HOMEDRIVE|HOMEPATH|APPDATA|LOCALAPPDATA|PROGRAMDATA|PROGRAMFILES|PROGRAMFILES\(X86\)|PSMODULEPATH|SYSTEMDRIVE|USERNAME|USERDOMAIN)$/i.test(
|
|
170
|
+
name,
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
),
|
|
174
|
+
SystemRoot: systemRoot,
|
|
175
|
+
windir: systemRoot,
|
|
176
|
+
TOTEM_ACL_CHALLENGE: nonce,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
current.child = child;
|
|
181
|
+
child.unref();
|
|
182
|
+
child.stderr?.unref?.();
|
|
183
|
+
let diagnostics = "";
|
|
184
|
+
child.stderr?.on("data", (chunk) => {
|
|
185
|
+
diagnostics = (diagnostics + chunk.toString()).slice(-4096);
|
|
186
|
+
for (const line of diagnostics.split(/\r?\n/).slice(0, -1))
|
|
187
|
+
if (/^TOTEM_ACL_[A-Z_]+$/.test(line)) options.onDiagnostic?.(line);
|
|
188
|
+
diagnostics = diagnostics.slice(diagnostics.lastIndexOf("\n") + 1);
|
|
189
|
+
});
|
|
190
|
+
child.on("error", () => stop(current));
|
|
191
|
+
child.on("close", () => stop(current));
|
|
192
|
+
const connect = () => {
|
|
193
|
+
if (current.stopped) return;
|
|
194
|
+
const socket = (options.createConnection ?? createConnection)({
|
|
195
|
+
path: "\\\\.\\pipe\\" + name,
|
|
196
|
+
});
|
|
197
|
+
current.socket = socket;
|
|
198
|
+
// Pending request timers keep the owner alive. Owner exit closes the pipe
|
|
199
|
+
// and the native reader exits; idle inspectors cannot prolong a CLI run.
|
|
200
|
+
socket.unref();
|
|
201
|
+
let retry = false;
|
|
202
|
+
socket.on("error", (error) => {
|
|
203
|
+
if (/^[A-Z_]{1,40}$/.test(error.code ?? "")) options.onDiagnostic?.(error.code);
|
|
204
|
+
if (!current.authenticated && error.code === "ENOENT") {
|
|
205
|
+
retry = true;
|
|
206
|
+
current.reconnect = setTimeout(connect, 25);
|
|
207
|
+
current.reconnect.unref();
|
|
208
|
+
} else stop(current);
|
|
209
|
+
});
|
|
210
|
+
socket.on("close", () => {
|
|
211
|
+
if (!retry) stop(current);
|
|
212
|
+
});
|
|
213
|
+
socket.on("data", (chunk) => {
|
|
214
|
+
if (current.stopped) return;
|
|
215
|
+
current.buffer += current.decoder.write(chunk);
|
|
216
|
+
if (Buffer.byteLength(current.buffer) > 262144) return stop(current);
|
|
217
|
+
let end;
|
|
218
|
+
while ((end = current.buffer.indexOf("\n")) >= 0) {
|
|
219
|
+
const line = current.buffer.slice(0, end).replace(/^\uFEFF/, "");
|
|
220
|
+
current.buffer = current.buffer.slice(end + 1);
|
|
221
|
+
let response;
|
|
222
|
+
try {
|
|
223
|
+
response = JSON.parse(line);
|
|
224
|
+
} catch {
|
|
225
|
+
return stop(current);
|
|
226
|
+
}
|
|
227
|
+
if (!current.authenticated) {
|
|
228
|
+
if (
|
|
229
|
+
response.id !== 0 ||
|
|
230
|
+
typeof response.nonce !== "string" ||
|
|
231
|
+
response.nonce.length !== nonce.length ||
|
|
232
|
+
!/^[a-f0-9-]{36}$/.test(response.nonce) ||
|
|
233
|
+
!timingSafeEqual(Buffer.from(response.nonce), Buffer.from(nonce))
|
|
234
|
+
)
|
|
235
|
+
return stop(current);
|
|
236
|
+
current.authenticated = true;
|
|
237
|
+
options.onDiagnostic?.("AUTHENTICATED");
|
|
238
|
+
for (const pending of current.pending.values()) socket.write(pending.request);
|
|
239
|
+
options.onDiagnostic?.("REQUEST_SENT");
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const pending = current.pending.get(response.id);
|
|
243
|
+
if (!pending) return stop(current);
|
|
244
|
+
clearTimeout(pending.timer);
|
|
245
|
+
current.pending.delete(response.id);
|
|
246
|
+
options.onDiagnostic?.("RESULT_RECEIVED");
|
|
247
|
+
if (response.error || !response.snapshot) pending.reject(unavailable());
|
|
248
|
+
else pending.resolve(response.snapshot);
|
|
249
|
+
}
|
|
250
|
+
if (!current.pending.size) {
|
|
251
|
+
current.idle = setTimeout(() => stop(current), options.idleMs ?? 30000);
|
|
252
|
+
current.idle.unref();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
connect();
|
|
257
|
+
return current;
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
read(path) {
|
|
261
|
+
const encodedPath = Buffer.from(resolve(path), "utf8").toString("base64");
|
|
262
|
+
const current = worker ?? start();
|
|
263
|
+
clearTimeout(current.idle);
|
|
264
|
+
if (current.pending.size >= 1024) return Promise.reject(unavailable());
|
|
265
|
+
const id = ++nextId;
|
|
266
|
+
return new Promise((resolveRequest, reject) => {
|
|
267
|
+
const timer = setTimeout(() => stop(current), options.timeoutMs ?? 10000);
|
|
268
|
+
const request = JSON.stringify({ id, path: encodedPath }) + "\n";
|
|
269
|
+
current.pending.set(id, { resolve: resolveRequest, reject, timer, request });
|
|
270
|
+
if (current.authenticated) current.socket.write(request);
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
close() {
|
|
274
|
+
if (worker) stop(worker);
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const reader = createWindowsAclReader();
|
|
57
280
|
export async function assertWindowsPrivatePath(path) {
|
|
58
281
|
if (process.platform !== "win32") return;
|
|
59
282
|
await lstat(path); // Preserve ENOENT for callers that create a new private file.
|
|
60
|
-
|
|
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/, "")));
|
|
283
|
+
validateWindowsPrivateAcl(await reader.read(path));
|
|
89
284
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "totemlayer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.115",
|
|
4
4
|
"description": "Connect AI agents to Totem Network for discovery, messaging, and shared work.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"homepage": "https://totemlayer.com",
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
|
37
|
-
"url": "git+https://github.com/
|
|
37
|
+
"url": "git+https://github.com/linbrandon4/Totem.git"
|
|
38
38
|
},
|
|
39
39
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
40
40
|
"publishConfig": {
|