totemlayer 0.1.74 → 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 CHANGED
@@ -27,7 +27,7 @@ To configure execution later, rerun setup with `--provider` and the adapter's se
27
27
 
28
28
  ## Connect with automatic listener setup
29
29
 
30
- Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.74.tgz`, `totem`, `connect`, `--provider`, and `codex` (or `claude` for Claude Code). The host runs the connection itself; the owner does not run a separate listener command. Node.js 22.14 or newer and the selected signed-in client must be available.
30
+ Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.75.tgz`, `totem`, `connect`, `--provider`, and `codex` (or `claude` for Claude Code). The host runs the connection itself; the owner does not run a separate listener command. Node.js 22.14 or newer and the selected signed-in client must be available.
31
31
 
32
32
  During browser sign-in, `totem_get_connection_status` reports progress. Normal tools appear after the approved identity is pinned privately. Codex verifies identity, startup and the saved chat callback before receiving work; reconnect preserves bindings and pauses. Missing capabilities leave work queued. A remote HTTP connector cannot install local software.
33
33
 
@@ -45,7 +45,7 @@ For a hosted connection, add these settings to your client's MCP configuration:
45
45
  For a local stdio connection, install the CLI with Node.js 22.14 or newer:
46
46
 
47
47
  ```sh
48
- npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.74.tgz
48
+ npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.75.tgz
49
49
  ```
50
50
 
51
51
  Save your agent's connection key in a private file. Protect it with mode 0600 on macOS/Linux or your user's filesystem ACL on Windows. Configure these fields in your client's MCP settings:
@@ -64,10 +64,10 @@ Reload the client if needed, discover the available tools, and call `totem_get_i
64
64
  Run inside the intended existing native Codex chat, keeping its current model:
65
65
 
66
66
  ```sh
67
- npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.74.tgz totem install
67
+ npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.75.tgz totem install
68
68
  ```
69
69
 
70
- Version 0.1.74 uses the strict installer. Read [setup.md](https://totemlayer.com/setup.md) in full first. The strict controller verifies the bundled full guide, exact native session/model and authenticated MCP identity, preserves pauses and existing permissions, and reuses the same connection. It configures exactly one supervised five-second receiver and requires observed recovery, native wake and separate peer-message and website-owner-chat reply receipts. Saved configuration or a readiness echo cannot mark setup complete. Missing or unsupported capabilities return an incomplete result.
70
+ Version 0.1.75 uses the strict installer. Read [setup.md](https://totemlayer.com/setup.md) in full first. The strict controller verifies the bundled full guide, exact native session/model and authenticated MCP identity, preserves pauses and existing permissions, and reuses the same connection. It configures exactly one supervised five-second receiver and requires observed recovery, native wake and separate peer-message and website-owner-chat reply receipts. Saved configuration or a readiness echo cannot mark setup complete. Missing or unsupported capabilities return an incomplete result.
71
71
 
72
72
  The supervised setup job returns promptly so its bound chat can receive checks. Run `totem install --status` to inspect progress. It resumes on startup/failure and reports into that same chat. Do not start a second installer or change models to finish setup. The current built-in strict local adapter supports Codex's durable existing session API; other apps use the native MCP/receiver path in setup.md and must report their actual limitations.
73
73
 
@@ -88,7 +88,7 @@ totem diagnose --json
88
88
 
89
89
  Check the observed receive, wake, and reply results. Stop pauses automatic processing until you start it again. The host must stay awake and online. Upgrades retain saved connections and recovery records. Hosted Context processing is temporarily paused.
90
90
 
91
- Local connections register automatic startup for the signed-in user: launchd on macOS, Task Scheduler on Windows, or a current-user Startup shortcut when task registration is unavailable. The supervisor restarts crashed children with backoff. Startup registration is verified separately from current readiness. Windows/macOS startup policy, deliberate pauses, revoked credentials, sleep, logout and shutdown still apply; no administrator access or new agent permissions are granted.
91
+ Installing the npm package does not register background startup by default. Run `totem install` inside the intended agent to enable it explicitly after its setup checks. Local connections register automatic startup for the signed-in user: launchd on macOS, Task Scheduler on Windows, or a current-user Startup shortcut when task registration is unavailable. The supervisor restarts crashed children with backoff. Startup registration is verified separately from current readiness. Windows/macOS startup policy, deliberate pauses, revoked credentials, sleep, logout and shutdown still apply; no administrator access or new agent permissions are granted.
92
92
 
93
93
  Connection acceptance and action execution have separate limits. An informed **Accept & start listener** action can enable text replies for that connection; it does not grant access to files, Missions, or arbitrary tools. An explicitly authorized general runtime can process the work its owner permits. Coding work also needs a tool-enabled agent session and approved repository access.
94
94
 
@@ -1,3 +1,4 @@
1
+ import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
1
2
  import { constants } from "node:fs";
2
3
  import { createHash, randomUUID } from "node:crypto";
3
4
  import { lstat, mkdir, open, realpath, rename, unlink } from "node:fs/promises";
@@ -90,8 +91,10 @@ async function directories(path, privateRoot, create = false) {
90
91
  info = await lstat(current);
91
92
  }
92
93
  if (info.isSymbolicLink() || !info.isDirectory()) fail();
93
- if (current === privateRoot || current.startsWith(`${privateRoot}${sep}`))
94
+ if (current === privateRoot || current.startsWith(`${privateRoot}${sep}`)) {
94
95
  privateDirectory(info);
96
+ await assertWindowsPrivatePath(current);
97
+ }
95
98
  }
96
99
  }
97
100
 
@@ -108,6 +111,7 @@ function privateFile(info, maximum) {
108
111
  }
109
112
 
110
113
  async function boundedPrivateRead(path, maximum) {
114
+ await assertWindowsPrivatePath(path);
111
115
  const info = await lstat(path);
112
116
  privateFile(info, maximum);
113
117
  const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
@@ -136,6 +140,7 @@ async function writePrivate(path, contents) {
136
140
  0o600,
137
141
  );
138
142
  try {
143
+ await assertWindowsPrivatePath(path);
139
144
  await file.writeFile(contents);
140
145
  await file.sync();
141
146
  } finally {
@@ -1,3 +1,4 @@
1
+ import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
1
2
  import { processIdentity } from "./runtime-lock.mjs";
2
3
  import { startupDiagnostic } from "./startup-diagnostics.mjs";
3
4
  import { spawn } from "node:child_process";
@@ -86,8 +87,10 @@ async function directoryTree(path, privateRoot) {
86
87
  const info = await lstat(current);
87
88
  if (!info.isDirectory() || info.isSymbolicLink())
88
89
  throw new Error("The agent status directory must not contain symbolic links.");
89
- if (current === privateRoot || current.startsWith(`${privateRoot}${sep}`))
90
+ if (current === privateRoot || current.startsWith(`${privateRoot}${sep}`)) {
90
91
  privateEntry(info, true);
92
+ await assertWindowsPrivatePath(current);
93
+ }
91
94
  }
92
95
  }
93
96
 
@@ -110,6 +113,7 @@ async function runtimeDirectory(profile, create = false) {
110
113
  }
111
114
 
112
115
  async function readPrivateJson(path) {
116
+ await assertWindowsPrivatePath(path);
113
117
  await verifyRuntimeDirectory(dirname(path));
114
118
  for (let attempt = 0; ; attempt += 1) {
115
119
  const info = await lstat(path);
@@ -1,3 +1,4 @@
1
+ import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
1
2
  import { startupDiagnostic } from "./startup-diagnostics.mjs";
2
3
  import { execFile } from "node:child_process";
3
4
  import { readFile, stat } from "node:fs/promises";
@@ -156,6 +157,7 @@ function credential(value) {
156
157
  }
157
158
 
158
159
  async function keyFromFile(path) {
160
+ await assertWindowsPrivatePath(path);
159
161
  try {
160
162
  const info = await stat(path);
161
163
  if (!info.isFile() || !info.size || info.size > KEY_LIMIT)
@@ -1,3 +1,4 @@
1
+ import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
1
2
  import { execFile, spawn } from "node:child_process";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import { access, mkdir, open, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
@@ -25,6 +26,7 @@ function validateInput({ peerAddress, provider, keyFile }) {
25
26
  }
26
27
 
27
28
  async function privateKeyFile(path) {
29
+ await assertWindowsPrivatePath(path);
28
30
  const info = await stat(path);
29
31
  if (!info.isFile() || info.size === 0 || info.size > 8192)
30
32
  throw new Error("The Totem key file must be a nonempty regular file under 8 KiB.");
@@ -1,3 +1,4 @@
1
+ import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
1
2
  import { startupDiagnostic } from "./startup-diagnostics.mjs";
2
3
  import { parseArgs } from "node:util";
3
4
  import { readFile, stat } from "node:fs/promises";
@@ -183,6 +184,7 @@ export async function installationMain(args = [], options = {}) {
183
184
  if (!provider) {
184
185
  let key = environment.TOTEM_NETWORK_API_KEY;
185
186
  if (!key && environment.TOTEM_NETWORK_API_KEY_FILE) {
187
+ await assertWindowsPrivatePath(environment.TOTEM_NETWORK_API_KEY_FILE);
186
188
  const info = await stat(environment.TOTEM_NETWORK_API_KEY_FILE);
187
189
  if (
188
190
  !info.isFile() ||
@@ -1,3 +1,4 @@
1
+ import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
1
2
  import { parseArgs } from "node:util";
2
3
  import { buildMcpConfig } from "./mcp-config.mjs";
3
4
  import { spawnSync } from "node:child_process";
@@ -182,6 +183,7 @@ function managedIdentityBinding(profile) {
182
183
  }
183
184
 
184
185
  async function validateKeyFile(path, platform) {
186
+ await assertWindowsPrivatePath(path);
185
187
  const info = await stat(path);
186
188
  if (!info.isFile() || info.size === 0 || info.size > 8 * 1024)
187
189
  fail("The Totem agent key must be a nonempty regular file no larger than 8 KiB.");
@@ -551,6 +553,7 @@ async function installedServerEntry(profile) {
551
553
  if (!profile.stateDirectory) return SERVER_ENTRY;
552
554
  const path = join(resolve(profile.stateDirectory, "../../../.."), "runtime", "totem.mjs");
553
555
  try {
556
+ await assertWindowsPrivatePath(path);
554
557
  const info = await lstat(path);
555
558
  if (
556
559
  info.isFile() &&
package/bin/mcp.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
1
2
  import { DEFAULT_LISTENER_POLL_MS } from "./sdk/listener.js";
2
3
  import { readTotemActivity } from "./mcp-listener.mjs";
3
4
  import { createHash, randomUUID } from "node:crypto";
@@ -139,6 +140,7 @@ async function readApiKey(environment = process.env) {
139
140
  throw new Error(
140
141
  "Set TOTEM_NETWORK_API_KEY_FILE to a private agent key file, or set TOTEM_NETWORK_API_KEY through a secret manager.",
141
142
  );
143
+ await assertWindowsPrivatePath(keyFile);
142
144
  const info = await stat(keyFile);
143
145
  if (!info.isFile() || info.size > MAX_KEY_BYTES)
144
146
  throw new Error("The agent key file must be a regular file no larger than 8 KiB.");
Binary file
@@ -1 +1 @@
1
- {"sourceSha256":"e15dc6cfd6f828dbcb829fc6fa3f5f6ef51062f45462ec6532b8c52f532c7951","binarySha256":"356be0a0c15189c84a576b2cbcda0565bbef220e626819f44923c9776ec40821","architectures":["arm64","x86_64"],"minimumMacOS":"13.0"}
1
+ {"sourceSha256":"e15dc6cfd6f828dbcb829fc6fa3f5f6ef51062f45462ec6532b8c52f532c7951","binarySha256":"eb32f32457c22af44178ab7a327a5639ae3254bfc17b403d39245b4eb83c1780","architectures":["arm64","x86_64"],"minimumMacOS":"13.0"}
package/bin/network.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
1
2
  import { DEFAULT_LISTENER_POLL_MS } from "./sdk/listener.js";
2
3
  import { parseArgs, stripVTControlCharacters } from "node:util";
3
4
  import { readFile, stat } from "node:fs/promises";
@@ -210,6 +211,7 @@ export async function networkMain(args) {
210
211
  if (secret)
211
212
  throw new Error("Set only one of TOTEM_NETWORK_API_KEY and TOTEM_NETWORK_API_KEY_FILE.");
212
213
  const path = process.env.TOTEM_NETWORK_API_KEY_FILE;
214
+ await assertWindowsPrivatePath(path);
213
215
  const info = await stat(path);
214
216
  if (
215
217
  !info.isFile() ||
@@ -1,3 +1,4 @@
1
+ import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
1
2
  import { execFile, spawn } from "node:child_process";
2
3
  import { promisify } from "node:util";
3
4
  import { constants } from "node:fs";
@@ -15,6 +16,7 @@ export function runtimeRoot(env = process.env) {
15
16
  }
16
17
  export async function privateDirectory(path) {
17
18
  await mkdir(path, { recursive: true, mode: 0o700 });
19
+ await assertWindowsPrivatePath(path);
18
20
  const info = await lstat(path);
19
21
  if (
20
22
  !info.isDirectory() ||
@@ -25,6 +27,7 @@ export async function privateDirectory(path) {
25
27
  throw new Error("The runtime directory must be private and owned by this user.");
26
28
  }
27
29
  export async function privateRead(path) {
30
+ await assertWindowsPrivatePath(path);
28
31
  const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
29
32
  try {
30
33
  const info = await handle.stat();
@@ -50,6 +53,7 @@ export async function privateWrite(path, value) {
50
53
  0o600,
51
54
  );
52
55
  try {
56
+ await assertWindowsPrivatePath(temporary);
53
57
  await handle.writeFile(JSON.stringify(value));
54
58
  await handle.sync();
55
59
  } finally {
@@ -188,9 +192,11 @@ import { join, dirname } from 'node:path';
188
192
  import { fileURLToPath, pathToFileURL } from 'node:url';
189
193
  import { spawn } from 'node:child_process';
190
194
  import { setTimeout as delay } from 'node:timers/promises';
195
+ import { assertWindowsPrivatePath } from './windows-private-path.mjs';
191
196
  const entry = fileURLToPath(import.meta.url);
192
197
  const root = dirname(entry);
193
198
  const file = join(root, 'current.json');
199
+ await assertWindowsPrivatePath(file);
194
200
  const info = await lstat(file);
195
201
  if (info.isSymbolicLink() || (process.getuid && info.uid !== process.getuid()) || (process.platform !== 'win32' && (info.mode & 511) !== 384)) throw Error('Unsafe runtime installation');
196
202
  const { version } = JSON.parse(await readFile(file, 'utf8'));
@@ -247,6 +253,10 @@ if (process.env.TOTEM_RUNTIME_ENV_LOADED === entry) {
247
253
  }
248
254
  `;
249
255
  for (const [path, contents] of [
256
+ [
257
+ join(runtime, "windows-private-path.mjs"),
258
+ await readFile(new URL("./windows-private-path.mjs", import.meta.url), "utf8"),
259
+ ],
250
260
  [cli, launcher("totem.mjs")],
251
261
  [supervisor, launcher("automatic-runtime.mjs")],
252
262
  ]) {
@@ -256,6 +266,7 @@ if (process.env.TOTEM_RUNTIME_ENV_LOADED === entry) {
256
266
  0o600,
257
267
  );
258
268
  try {
269
+ await assertWindowsPrivatePath(path);
259
270
  await handle.writeFile(contents);
260
271
  } finally {
261
272
  await handle.close();
@@ -1 +1 @@
1
- {"version":1,"packageVersion":"0.1.74","bytes":27189,"sha256":"c62a0116c76177d905051b40ddcbb0503e042b1fc8f5d024c1ebf41c7f173983"}
1
+ {"version":1,"packageVersion":"0.1.75","bytes":27189,"sha256":"c62a0116c76177d905051b40ddcbb0503e042b1fc8f5d024c1ebf41c7f173983"}
@@ -1 +1 @@
1
- {"version":1,"packageVersion":"0.1.74","bytes":36144,"sha256":"a5115bdc86d08737eba0510aab110dd28f583e3ec24aa84f389c9f017a7c5cae"}
1
+ {"version":1,"packageVersion":"0.1.75","bytes":36144,"sha256":"b9e6518f5e1d15b83e013469753220a26ab61fb9a4bc45db74cc007ea2178279"}
@@ -39,10 +39,10 @@ Use an actual native user-message tool if available and wait for delivery. Other
39
39
  Run on the actual command-capable agent host, from the intended native session:
40
40
 
41
41
  ~~~sh
42
- npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.74.tgz totem install
42
+ npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.75.tgz totem install
43
43
  ~~~
44
44
 
45
- This command pins release **0.1.74**. Verify the published package and full guide before making setup changes. Node.js 22.14 or newer is required for this package. Pin the tested release. `--ignore-scripts` prevents package lifecycle setup before the strict controller. Installing the package alone grants no connection or background authority.
45
+ This command pins release **0.1.75**. Verify the published package and full guide before making setup changes. Node.js 22.14 or newer is required for this package. Pin the tested release. `--ignore-scripts` prevents package lifecycle setup before the strict controller. Installing the package alone grants no connection or background authority.
46
46
 
47
47
  The command verifies the full bundled guide against its byte length/SHA-256 manifest, with a complete alternate copy. `totem install --read-guide` prints it without setup changes. Corrupt/incomplete required guides are a hard stop. The native adapter must verify the current model and unchanged host permissions, exact session, intended identity, transport and current credential generation. “default” or a guessed model is not evidence. There is no force-success, skip-tests, model-supplied proof or arbitrary adapter-script URL.
48
48
 
@@ -39,10 +39,10 @@ Use an actual native user-message tool if available and wait for delivery. Other
39
39
  Run on the actual command-capable agent host, from the intended native session:
40
40
 
41
41
  ~~~sh
42
- npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.74.tgz totem install
42
+ npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.75.tgz totem install
43
43
  ~~~
44
44
 
45
- This command pins release **0.1.74**. Verify the published package and full guide before making setup changes. Node.js 22.14 or newer is required for this package. Pin the tested release. `--ignore-scripts` prevents package lifecycle setup before the strict controller. Installing the package alone grants no connection or background authority.
45
+ This command pins release **0.1.75**. Verify the published package and full guide before making setup changes. Node.js 22.14 or newer is required for this package. Pin the tested release. `--ignore-scripts` prevents package lifecycle setup before the strict controller. Installing the package alone grants no connection or background authority.
46
46
 
47
47
  The command verifies the full bundled guide against its byte length/SHA-256 manifest, with a complete alternate copy. `totem install --read-guide` prints it without setup changes. Corrupt/incomplete required guides are a hard stop. The native adapter must verify the current model and unchanged host permissions, exact session, intended identity, transport and current credential generation. “default” or a guessed model is not evidence. There is no force-success, skip-tests, model-supplied proof or arbitrary adapter-script URL.
48
48
 
@@ -15,6 +15,7 @@ import { runtimeRoot, privateRead, privateWrite } from "./runtime-installation.m
15
15
  import { readReleaseSetupGuide } from "./setup-guide.mjs";
16
16
  import { prepareSetupTests, readSetupTests } from "./setup-receipts.mjs";
17
17
  import { defaultCodexQueue } from "./codex-chat-bridge.mjs";
18
+ import { installedClientInvocation, localClientEnvironment } from "./platform-client.mjs";
18
19
 
19
20
  const execute = promisify(execFile);
20
21
  const fail = (code) => {
@@ -190,18 +191,15 @@ export function createStrictCodexAdapter(flags, options = {}) {
190
191
  readGuide: options.readGuide ?? readReleaseSetupGuide,
191
192
  async inspect(readOnly = false) {
192
193
  host = await snapshot();
193
- const result = await (options.execute ?? execute)("codex", ["mcp", "list", "--json"], {
194
- env: inherited,
194
+ const invocation = installedClientInvocation("codex", ["mcp", "list", "--json"], inherited);
195
+ const pending = (options.execute ?? execute)(invocation.command, invocation.args, {
196
+ env: localClientEnvironment(inherited),
195
197
  encoding: "utf8",
196
198
  timeout: 5000,
197
199
  maxBuffer: 65536,
198
- }).catch((error) => {
199
- if (
200
- /no mcp server|not found|does not exist/i.test(
201
- `${error.stdout ?? ""}\n${error.stderr ?? ""}`,
202
- )
203
- )
204
- return null;
200
+ });
201
+ pending.child?.stdin?.end();
202
+ const result = await pending.catch(() => {
205
203
  fail("MCP_SETTINGS_UNVERIFIED");
206
204
  });
207
205
  if (result) {
@@ -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.74",
3
+ "version": "0.1.75",
4
4
  "description": "Cross-platform CLI for TotemLayer agent identity and authorization",
5
5
  "type": "module",
6
6
  "bin": {