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.
Files changed (54) hide show
  1. package/README.md +48 -6
  2. package/bin/automatic-runtime.mjs +2 -1
  3. package/bin/cloud-supervisor.mjs +44 -0
  4. package/bin/codex-mission-bridge.mjs +6 -2
  5. package/bin/configured-native-installation.mjs +57 -0
  6. package/bin/connected-agent-service.mjs +5 -1
  7. package/bin/device-login.mjs +3 -1
  8. package/bin/inbox-queue.mjs +8 -5
  9. package/bin/mcp.mjs +18 -5
  10. package/bin/mission-agent-service.mjs +13 -4
  11. package/bin/native/darwin-lifetime +0 -0
  12. package/bin/native/darwin-lifetime.json +1 -1
  13. package/bin/native-claude-login.mjs +131 -0
  14. package/bin/native-claude-receiver.mjs +272 -0
  15. package/bin/native-cli-process.py +105 -0
  16. package/bin/native-harness-environment.mjs +20 -0
  17. package/bin/native-hermes-bridge.mjs +99 -0
  18. package/bin/native-hermes-installation.mjs +9 -0
  19. package/bin/native-hermes-mcp.mjs +107 -0
  20. package/bin/native-hermes-mcp.py +205 -0
  21. package/bin/native-hermes-metadata.py +40 -0
  22. package/bin/native-hermes-process.py +62 -0
  23. package/bin/native-hermes-receiver.mjs +28 -0
  24. package/bin/native-inbox-installation.mjs +144 -0
  25. package/bin/native-inbox-receiver.mjs +133 -0
  26. package/bin/native-kilo-installation.mjs +4 -0
  27. package/bin/native-kilo-receiver.mjs +1 -0
  28. package/bin/native-mcp-source.mjs +244 -0
  29. package/bin/native-opencode-bridge.mjs +108 -0
  30. package/bin/native-opencode-installation.mjs +15 -0
  31. package/bin/native-opencode-mcp.mjs +112 -0
  32. package/bin/native-opencode-receiver.mjs +22 -0
  33. package/bin/native-pi-bridge.mjs +170 -0
  34. package/bin/native-pi-installation.mjs +9 -0
  35. package/bin/native-pi-mcp.mjs +139 -0
  36. package/bin/native-pi-receiver.mjs +28 -0
  37. package/bin/network-retry.mjs +9 -1
  38. package/bin/platform-client.mjs +2 -5
  39. package/bin/runtime-installation.mjs +9 -4
  40. package/bin/sdk/errors.d.ts +1 -1
  41. package/bin/sdk/errors.js +1 -1
  42. package/bin/sdk/listener.d.ts +1 -0
  43. package/bin/sdk/listener.js +1 -1
  44. package/bin/sdk/transport.js +1 -1
  45. package/bin/setup-assets/api-manifest.json +1 -1
  46. package/bin/setup-assets/manifest.json +1 -1
  47. package/bin/setup-assets/setup.backup.md +2 -2
  48. package/bin/setup-assets/setup.md +2 -2
  49. package/bin/startup-diagnostics.mjs +19 -2
  50. package/bin/strict-installation.mjs +29 -3
  51. package/bin/supervisor-watchdog.mjs +44 -0
  52. package/bin/totem.mjs +6 -0
  53. package/bin/windows-private-path.mjs +227 -32
  54. 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((item) => known(item.code) || known(item.causeCode) || [401, 403].includes(item.status));
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
- : "RUNTIME_AUTHORIZATION_REJECTED"
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
- if (values.session && !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(values.session))
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 { execFile } from "node:child_process";
2
- import { promisify } from "node:util";
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
- 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/, "")));
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.96",
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/totemlayer/Totem.git"
37
+ "url": "git+https://github.com/linbrandon4/Totem.git"
38
38
  },
39
39
  "license": "SEE LICENSE IN LICENSE.md",
40
40
  "publishConfig": {