caveat-cli 0.21.1 → 0.21.3

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.
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __caveat_createRequire } from 'node:module';
3
+ const require = __caveat_createRequire(import.meta.url);
4
+
5
+ // ../../packages/core/src/privateWindowsAclWorker.ts
6
+ import { spawn } from "node:child_process";
7
+ import { parentPort } from "node:worker_threads";
8
+ import { createInterface } from "node:readline";
9
+
10
+ // ../../packages/core/src/privateWindowsAcl.ts
11
+ import { spawnSync } from "node:child_process";
12
+ var WINDOWS_ACL_VERIFY = String.raw`$ErrorActionPreference='Stop';$p=$env:CAVEAT_ACL_PATH;$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value;$acl=Get-Acl -LiteralPath $p;$owner=$acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value;if($owner -ne $sid){exit 41};$rules=@($acl.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier]));if($rules.Count -ne 1){exit 42};$r=$rules[0];if($r.IdentityReference.Value -ne $sid -or $r.AccessControlType -ne 'Allow' -or $r.IsInherited -or ($r.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl){exit 43}`;
13
+ function windowsAclApplyScript() {
14
+ return String.raw`$ErrorActionPreference='Stop';$p=$env:CAVEAT_ACL_PATH;$isDir=$env:CAVEAT_ACL_DIRECTORY -eq '1';$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User;$acl=if($isDir){New-Object System.Security.AccessControl.DirectorySecurity}else{New-Object System.Security.AccessControl.FileSecurity};$acl.SetAccessRuleProtection($true,$false);$flags=if($isDir){[System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'}else{[System.Security.AccessControl.InheritanceFlags]::None};$rule=New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl',$flags,[System.Security.AccessControl.PropagationFlags]::None,[System.Security.AccessControl.AccessControlType]::Allow);$acl.SetOwner($sid);$acl.AddAccessRule($rule);if($isDir){[System.IO.FileSystemAclExtensions]::SetAccessControl([System.IO.DirectoryInfo]::new($p),$acl)}else{[System.IO.FileSystemAclExtensions]::SetAccessControl([System.IO.FileInfo]::new($p),$acl)};` + WINDOWS_ACL_VERIFY;
15
+ }
16
+
17
+ // ../../packages/core/src/privateWindowsAclProtocol.ts
18
+ var ACL_REPLY_BYTES = 64 * 1024;
19
+ function reply(work, value) {
20
+ const header = new Int32Array(work.buffer, 0, 2);
21
+ const data = Buffer.from(JSON.stringify(value));
22
+ if (data.length > ACL_REPLY_BYTES) throw Error("ACL reply exceeds protocol bound");
23
+ new Uint8Array(work.buffer, 8, data.length).set(data);
24
+ Atomics.store(header, 1, data.length);
25
+ Atomics.store(header, 0, 1);
26
+ Atomics.notify(header, 0);
27
+ }
28
+ function windowsAclSessionProgram() {
29
+ const interpreterExit = (script) => script.replace(/\bexit (41|42|43)\b/g, "[Environment]::Exit($1)");
30
+ return `$apply = { ${interpreterExit(windowsAclApplyScript())} }
31
+ $verify = { ${interpreterExit(WINDOWS_ACL_VERIFY)} }
32
+ ` + String.raw`
33
+ $ErrorActionPreference='Stop'
34
+ while($null -ne ($line=[Console]::In.ReadLine())) {
35
+ $r=ConvertFrom-Json -InputObject $line
36
+ if(@($r.PSObject.Properties.Name).Count -ne 4 -or $r.path -isnot [string] -or $r.directory -isnot [bool] -or $r.apply -isnot [bool] -or $r.sequence -lt 1){exit 47}
37
+ $env:CAVEAT_ACL_PATH=$r.path
38
+ $env:CAVEAT_ACL_DIRECTORY=if($r.directory){'1'}else{'0'}
39
+ if($r.apply){& $apply}else{& $verify}
40
+ [Console]::Out.WriteLine(([ordered]@{sequence=$r.sequence;ok=$true}|ConvertTo-Json -Compress))
41
+ }
42
+ `;
43
+ }
44
+
45
+ // ../../packages/core/src/privateWindowsAclWorker.ts
46
+ var BUFFER_LIMIT = 1024 * 1024;
47
+ function startAclWorker(data) {
48
+ if (!parentPort) throw Error("ACL worker requires its parent");
49
+ const port = parentPort;
50
+ let processState;
51
+ let active;
52
+ const errorFor = (state, code, signal) => Error(`store_unsafe: powershell exit=${code} signal=${signal ?? "none"} stderr=${state.stderr.replace(/\s+/g, " ").trim().slice(0, 400) || "(empty)"}`);
53
+ const fail = (state, error) => {
54
+ const pending = state.pending;
55
+ state.pending = void 0;
56
+ pending?.reject(error);
57
+ state.child.kill();
58
+ };
59
+ function ensureProcess() {
60
+ if (processState) return processState;
61
+ const child = spawn(data.options?.command ?? "pwsh.exe", data.options?.args ?? ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", windowsAclSessionProgram()], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
62
+ let finish;
63
+ const state = { child, closed: new Promise((resolve) => {
64
+ finish = resolve;
65
+ }), stderr: "", stdoutBytes: 0 };
66
+ processState = state;
67
+ child.stdout.on("data", (bytes) => {
68
+ state.stdoutBytes += bytes.length;
69
+ if (state.stdoutBytes > BUFFER_LIMIT) fail(state, Error("store_unsafe: powershell spawn ENOBUFS"));
70
+ });
71
+ for (const stream of [child.stdout, child.stderr]) stream.on("error", (error) => fail(state, Error(`store_unsafe: powershell spawn ${error.code ?? error.name}`)));
72
+ const lines = createInterface({ input: child.stdout });
73
+ lines.on("line", (line) => {
74
+ const pending = state.pending;
75
+ try {
76
+ const value = JSON.parse(line);
77
+ if (!pending || Object.keys(value).sort().join(",") !== "ok,sequence" || value.ok !== true || value.sequence !== pending.operation.sequence) throw Error("store_unsafe: powershell invalid response");
78
+ state.pending = void 0;
79
+ pending.resolve();
80
+ } catch {
81
+ fail(state, Error("store_unsafe: powershell invalid response"));
82
+ }
83
+ });
84
+ child.stderr.on("data", (bytes) => {
85
+ if (Buffer.byteLength(state.stderr) + bytes.length > BUFFER_LIMIT) fail(state, Error("store_unsafe: powershell spawn ENOBUFS"));
86
+ else state.stderr += bytes.toString();
87
+ });
88
+ child.stdin.on("error", (error) => fail(state, Error(`store_unsafe: powershell spawn ${error.code ?? error.name}`)));
89
+ child.on("error", (error) => fail(state, Error(`store_unsafe: powershell spawn ${error.code ?? error.name}`)));
90
+ child.on("close", (code, signal) => {
91
+ lines.close();
92
+ if (processState === state) processState = void 0;
93
+ const pending = state.pending;
94
+ state.pending = void 0;
95
+ pending?.reject(errorFor(state, code, signal));
96
+ finish({ code, signal });
97
+ });
98
+ return state;
99
+ }
100
+ async function perform(work) {
101
+ if (active) {
102
+ reply(work, { sequence: work.operation.sequence, ok: false, error: "store_unsafe: ACL worker concurrent request" });
103
+ return;
104
+ }
105
+ active = work;
106
+ let error;
107
+ let helperPid;
108
+ try {
109
+ if ("close" in work.operation) {
110
+ const state = processState;
111
+ helperPid = state?.child.pid;
112
+ if (state) {
113
+ state.child.stdin.end();
114
+ const timer = setTimeout(() => state.child.kill(), 1e3);
115
+ const result = await state.closed;
116
+ clearTimeout(timer);
117
+ if (result.code !== 0) throw errorFor(state, result.code, result.signal);
118
+ }
119
+ } else {
120
+ const previous = processState;
121
+ if (previous && (previous.child.killed || previous.child.exitCode !== null || previous.child.signalCode !== null)) await previous.closed;
122
+ const state = ensureProcess();
123
+ helperPid = state.child.pid;
124
+ state.stderr = "";
125
+ state.stdoutBytes = 0;
126
+ await new Promise((resolve, reject) => {
127
+ state.pending = { operation: work.operation, resolve, reject };
128
+ state.child.stdin.write(JSON.stringify(work.operation) + "\n");
129
+ });
130
+ }
131
+ } catch (failure) {
132
+ error = failure instanceof Error ? failure.message : "store_unsafe: ACL worker failure";
133
+ const state = processState;
134
+ if (state) {
135
+ state.child.kill();
136
+ await state.closed;
137
+ }
138
+ }
139
+ active = void 0;
140
+ reply(work, { sequence: work.operation.sequence, ok: error === void 0, ...error ? { error } : {}, ...helperPid ? { helperPid } : {} });
141
+ if ("close" in work.operation) port.close();
142
+ }
143
+ port.on("message", (message) => {
144
+ if ("cancel" in message) {
145
+ if (active?.operation.sequence === message.cancel) processState?.child.kill();
146
+ return;
147
+ }
148
+ void perform(message);
149
+ });
150
+ void perform(data.initial);
151
+ }
152
+ export {
153
+ startAclWorker
154
+ };
155
+ //# sourceMappingURL=privateWindowsAclWorker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../packages/core/src/privateWindowsAclWorker.ts","../../../packages/core/src/privateWindowsAcl.ts","../../../packages/core/src/privateWindowsAclProtocol.ts"],"sourcesContent":["import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';\nimport { parentPort } from 'node:worker_threads';\nimport { createInterface } from 'node:readline';\nimport { reply, windowsAclSessionProgram, type AclOperation, type AclWork } from './privateWindowsAclProtocol.js';\n\ntype ChildState = {\n child: ChildProcessWithoutNullStreams;\n closed: Promise<{ code: number | null; signal: string | null }>;\n stderr: string;\n stdoutBytes: number;\n pending?: { operation: AclOperation; resolve: () => void; reject: (error: Error) => void };\n};\nconst BUFFER_LIMIT = 1024 * 1024;\nexport function startAclWorker(data: { initial: AclWork; options?: { command?: string; args?: string[] } }): void {\n if (!parentPort) throw Error('ACL worker requires its parent');\n const port = parentPort;\n let processState: ChildState | undefined;\n let active: AclWork | undefined;\n const errorFor = (state: ChildState, code: number | null, signal: string | null) => Error(`store_unsafe: powershell exit=${code} signal=${signal ?? 'none'} stderr=${state.stderr.replace(/\\s+/g, ' ').trim().slice(0, 400) || '(empty)'}`);\n const fail = (state: ChildState, error: Error) => {\n const pending = state.pending; state.pending = undefined;\n pending?.reject(error);\n // Only the ChildProcess handle created by this worker can be terminated.\n state.child.kill();\n };\n function ensureProcess(): ChildState {\n if (processState) return processState;\n const child = spawn(data.options?.command ?? 'pwsh.exe', data.options?.args ?? ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', windowsAclSessionProgram()], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });\n let finish!: (value: { code: number | null; signal: string | null }) => void;\n const state: ChildState = { child, closed: new Promise(resolve => { finish = resolve; }), stderr: '', stdoutBytes: 0 };\n processState = state;\n child.stdout.on('data', bytes => { state.stdoutBytes += bytes.length; if (state.stdoutBytes > BUFFER_LIMIT) fail(state, Error('store_unsafe: powershell spawn ENOBUFS')); });\n for (const stream of [child.stdout, child.stderr]) stream.on('error', error => fail(state, Error(`store_unsafe: powershell spawn ${(error as NodeJS.ErrnoException).code ?? error.name}`)));\n const lines = createInterface({ input: child.stdout });\n lines.on('line', line => {\n const pending = state.pending;\n try {\n const value = JSON.parse(line) as Record<string, unknown>;\n if (!pending || Object.keys(value).sort().join(',') !== 'ok,sequence' || value.ok !== true || value.sequence !== pending.operation.sequence) throw Error('store_unsafe: powershell invalid response');\n state.pending = undefined; pending.resolve();\n } catch { fail(state, Error('store_unsafe: powershell invalid response')); }\n });\n child.stderr.on('data', bytes => {\n if (Buffer.byteLength(state.stderr) + bytes.length > BUFFER_LIMIT) fail(state, Error('store_unsafe: powershell spawn ENOBUFS'));\n else state.stderr += bytes.toString();\n });\n child.stdin.on('error', error => fail(state, Error(`store_unsafe: powershell spawn ${(error as NodeJS.ErrnoException).code ?? error.name}`)));\n child.on('error', error => fail(state, Error(`store_unsafe: powershell spawn ${(error as NodeJS.ErrnoException).code ?? error.name}`)));\n child.on('close', (code, signal) => {\n lines.close();\n if (processState === state) processState = undefined;\n const pending = state.pending; state.pending = undefined;\n pending?.reject(errorFor(state, code, signal));\n finish({ code, signal });\n });\n return state;\n }\n async function perform(work: AclWork): Promise<void> {\n if (active) { reply(work, { sequence: work.operation.sequence, ok: false, error: 'store_unsafe: ACL worker concurrent request' }); return; }\n active = work;\n let error: string | undefined;\n let helperPid: number | undefined;\n try {\n if ('close' in work.operation) {\n const state = processState;\n helperPid = state?.child.pid;\n if (state) {\n state.child.stdin.end();\n const timer = setTimeout(() => state.child.kill(), 1_000);\n const result = await state.closed;\n clearTimeout(timer);\n if (result.code !== 0) throw errorFor(state, result.code, result.signal);\n }\n } else {\n const previous = processState;\n if (previous && (previous.child.killed || previous.child.exitCode !== null || previous.child.signalCode !== null)) await previous.closed;\n const state = ensureProcess(); helperPid = state.child.pid; state.stderr = ''; state.stdoutBytes = 0;\n await new Promise<void>((resolve, reject) => {\n state.pending = { operation: work.operation as AclOperation, resolve, reject };\n state.child.stdin.write(JSON.stringify(work.operation) + '\\n');\n });\n }\n } catch (failure) {\n error = failure instanceof Error ? failure.message : 'store_unsafe: ACL worker failure';\n // A failed interpreter is closed before the next store can start another.\n const state = processState;\n if (state) { state.child.kill(); await state.closed; }\n }\n active = undefined;\n reply(work, { sequence: work.operation.sequence, ok: error === undefined, ...(error ? { error } : {}), ...(helperPid ? { helperPid } : {}) });\n if ('close' in work.operation) port.close();\n }\n port.on('message', (message: AclWork | { cancel: number }) => {\n if ('cancel' in message) {\n if (active?.operation.sequence === message.cancel) processState?.child.kill();\n return;\n }\n void perform(message);\n });\n void perform(data.initial);\n}\n","import { spawnSync } from 'node:child_process';\n\nexport const WINDOWS_ACL_VERIFY = String.raw`$ErrorActionPreference='Stop';$p=$env:CAVEAT_ACL_PATH;$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value;$acl=Get-Acl -LiteralPath $p;$owner=$acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value;if($owner -ne $sid){exit 41};$rules=@($acl.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier]));if($rules.Count -ne 1){exit 42};$r=$rules[0];if($r.IdentityReference.Value -ne $sid -or $r.AccessControlType -ne 'Allow' -or $r.IsInherited -or ($r.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl){exit 43}`;\n// Persist only modified owner/access sections; Set-Acl may copy the audit\n// section and require SeSecurityPrivilege even for a DACL-only change.\nexport function windowsAclApplyScript() { return String.raw`$ErrorActionPreference='Stop';$p=$env:CAVEAT_ACL_PATH;$isDir=$env:CAVEAT_ACL_DIRECTORY -eq '1';$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User;$acl=if($isDir){New-Object System.Security.AccessControl.DirectorySecurity}else{New-Object System.Security.AccessControl.FileSecurity};$acl.SetAccessRuleProtection($true,$false);$flags=if($isDir){[System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'}else{[System.Security.AccessControl.InheritanceFlags]::None};$rule=New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl',$flags,[System.Security.AccessControl.PropagationFlags]::None,[System.Security.AccessControl.AccessControlType]::Allow);$acl.SetOwner($sid);$acl.AddAccessRule($rule);if($isDir){[System.IO.FileSystemAclExtensions]::SetAccessControl([System.IO.DirectoryInfo]::new($p),$acl)}else{[System.IO.FileSystemAclExtensions]::SetAccessControl([System.IO.FileInfo]::new($p),$acl)};` + WINDOWS_ACL_VERIFY; }\n// Hang guard, not a latency budget, and sized against the slowest machine that\n// runs it rather than a developer box.\n//\n// The old 3s bound is what made this seam flake: on a contended Windows CI\n// runner a single apply can cross it, spawnSync kills powershell,\n// and the null exit status read as \"unsafe ACL\". Measured on the actual\n// runners, one apply costs 331-459ms across 160 isolated runs, but the same\n// test costs 1042-1319ms inside the full parallel suite and the file it lives\n// in takes 47-61s overall — so 3s left barely 2.5x headroom against a runner\n// under load. A later windows-2022 run exhausted the former 15s guard while\n// PowerShell was starting, without returning an ACL rejection. 30s remains a\n// bounded hang guard while giving the hosted runner cold start enough room.\n// Re-measure before changing it; a fast idle box will lie to you.\nexport const WINDOWS_ACL_TIMEOUT_MS = 30_000;\nexport function runWindowsAcl(path: string, directory: boolean, apply: boolean) {\n runAclScript(path, directory, apply ? windowsAclApplyScript() : WINDOWS_ACL_VERIFY);\n}\n/** CopyFile can retain source security; seal every staged descendant in one process. */\nexport function runWindowsAclPrivateTree(path: string) {\n const script = String.raw`$ErrorActionPreference='Stop';$items=@(Get-Item -LiteralPath $env:CAVEAT_ACL_PATH)+@(Get-ChildItem -LiteralPath $env:CAVEAT_ACL_PATH -Force -Recurse);foreach($item in $items){if($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint){exit 44};$env:CAVEAT_ACL_PATH=$item.FullName;$env:CAVEAT_ACL_DIRECTORY=if($item.PSIsContainer){'1'}else{'0'};` + windowsAclApplyScript() + '}';\n runAclScript(path, true, script);\n}\n/** Full installer/status recheck, never called by a hook's stable entry. */\nexport function runWindowsAclVerifyPrivateTree(path: string, additionalPaths: string[] = []) {\n // Inherited owner-only rules are equally private below the independently\n // verified root. Reject additional principals, reparse points and foreign owners.\n const verify = WINDOWS_ACL_VERIFY.replace(' -or $r.IsInherited', '');\n const script = String.raw`$ErrorActionPreference='Stop';foreach($root in (ConvertFrom-Json $env:CAVEAT_ACL_VERIFY_PATHS)){$item=Get-Item -LiteralPath $root -Force;$items=@($item);if($item.PSIsContainer){$items+=@(Get-ChildItem -LiteralPath $root -Force -Recurse)};foreach($item in $items){if($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint){exit 44};$env:CAVEAT_ACL_PATH=$item.FullName;` + verify + '}}';\n runAclScript(path, true, script, { CAVEAT_ACL_VERIFY_PATHS: JSON.stringify([path, ...additionalPaths]) });\n}\n/** Ancestors must not let another ordinary user replace an owner-only root. */\nexport function runWindowsManagedRootAcl(path: string, userHome: string, requireCreate = false) {\n const script = String.raw`\n$ErrorActionPreference='Stop'\n$root=[System.IO.Path]::GetFullPath($env:CAVEAT_ACL_PATH)\n$homePath=[System.IO.Path]::GetFullPath($env:CAVEAT_DEPLOY_HOME)\n$identity=[System.Security.Principal.WindowsIdentity]::GetCurrent()\n$sid=$identity.User.Value\n$trusted=@($sid,'S-1-5-18','S-1-5-32-544')\n$tokenSids=@($sid)+@($identity.Groups | ForEach-Object {$_.Value})\n$mask=[System.Security.AccessControl.FileSystemRights]'Write,Delete,DeleteSubdirectoriesAndFiles,ChangePermissions,TakeOwnership'\n$current=$root\n$createChecked=$false\nwhile($true){\n if(Test-Path -LiteralPath $current){\n $item=Get-Item -LiteralPath $current -Force\n if(-not $item.PSIsContainer -or ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)){exit 44}\n $acl=Get-Acl -LiteralPath $current\n if($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -notin $trusted){exit 41}\n $rules=@($acl.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier]))\n foreach($rule in $rules){\n if($rule.AccessControlType -eq 'Allow' -and ($rule.FileSystemRights -band $mask) -ne 0 -and $rule.IdentityReference.Value -notin @($sid,'S-1-5-18','S-1-5-32-544','S-1-3-0')){exit 43}\n }\n if(-not $createChecked -and $env:CAVEAT_REQUIRE_CREATE -eq '1'){\n $createChecked=$true\n $rights=[System.Security.AccessControl.FileSystemRights]0\n foreach($rule in $rules){\n if($rule.AccessControlType -eq 'Deny' -and $rule.IdentityReference.Value -in $tokenSids -and ($rule.FileSystemRights -band $mask) -ne 0){exit 46}\n if($rule.AccessControlType -eq 'Allow' -and $rule.IdentityReference.Value -eq $sid){$rights=$rights -bor $rule.FileSystemRights}\n }\n if(($rights -band [System.Security.AccessControl.FileSystemRights]::CreateDirectories) -eq 0){exit 46}\n }\n }\n if($current.Equals($homePath,[System.StringComparison]::OrdinalIgnoreCase)){break}\n $next=[System.IO.Path]::GetDirectoryName($current)\n if(-not $next -or $next.Length -lt $homePath.Length){exit 45}\n $current=$next\n}\nif(Test-Path -LiteralPath $root){$env:CAVEAT_ACL_PATH=$root;` + WINDOWS_ACL_VERIFY + '}';\n runAclScript(path, true, script, { CAVEAT_DEPLOY_HOME: userHome, CAVEAT_REQUIRE_CREATE: requireCreate ? '1' : '0' });\n}\n/** Caller-verified source may be readable, but no ordinary foreign writer/owner. */\nexport function runWindowsManagedSourceAcl(path: string) {\n const script = String.raw`\n$ErrorActionPreference='Stop'\n$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value\n$trusted=@($sid,'S-1-5-18','S-1-5-32-544')\n$mask=[System.Security.AccessControl.FileSystemRights]'Write,Delete,DeleteSubdirectoriesAndFiles,ChangePermissions,TakeOwnership'\n$items=@(Get-Item -LiteralPath $env:CAVEAT_ACL_PATH -Force)+@(Get-ChildItem -LiteralPath $env:CAVEAT_ACL_PATH -Force -Recurse)\nforeach($item in $items){\n if($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint){exit 44}\n $acl=Get-Acl -LiteralPath $item.FullName\n if($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -notin $trusted){exit 41}\n foreach($rule in $acl.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier])){\n if($rule.AccessControlType -eq 'Allow' -and ($rule.FileSystemRights -band $mask) -ne 0 -and $rule.IdentityReference.Value -notin @($sid,'S-1-5-18','S-1-5-32-544','S-1-3-0')){exit 43}\n }\n}`;\n runAclScript(path, true, script);\n}\nfunction runAclScript(path: string, directory: boolean, script: string, extraEnv: NodeJS.ProcessEnv = {}) {\n const result = spawnSync('pwsh.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], { env: { ...process.env, ...extraEnv, CAVEAT_ACL_PATH: path, CAVEAT_ACL_DIRECTORY: directory ? '1' : '0' }, encoding: 'utf-8', timeout: WINDOWS_ACL_TIMEOUT_MS, windowsHide: true });\n // 起動失敗・timeout・ACL拒否を区別し、外向きのstore_unsafeにはcauseとして保持する。\n // pathとstderrは内部診断だけに残し、収集レコードには保存しない。\n if (result.error !== undefined) throw Error(`store_unsafe: powershell spawn ${(result.error as NodeJS.ErrnoException).code ?? result.error.message}`);\n if (result.status !== 0) throw Error(`store_unsafe: powershell exit=${result.status} signal=${result.signal ?? 'none'} stderr=${(result.stderr ?? '').replace(/\\s+/g, ' ').trim().slice(0, 400) || '(empty)'}`);\n}\n","import { WINDOWS_ACL_VERIFY, windowsAclApplyScript } from './privateWindowsAcl.js';\n\nexport const ACL_REPLY_BYTES = 64 * 1024;\nexport type AclOperation = { sequence: number; path: string; directory: boolean; apply: boolean };\nexport type AclReply = { sequence: number; ok: boolean; error?: string; terminal?: boolean; helperPid?: number };\nexport type AclWork = { operation: AclOperation | { sequence: number; close: true }; buffer: SharedArrayBuffer };\n\nexport function reply(work: AclWork, value: AclReply): void {\n const header = new Int32Array(work.buffer, 0, 2);\n const data = Buffer.from(JSON.stringify(value));\n if (data.length > ACL_REPLY_BYTES) throw Error('ACL reply exceeds protocol bound');\n new Uint8Array(work.buffer, 8, data.length).set(data);\n Atomics.store(header, 1, data.length);\n Atomics.store(header, 0, 1);\n Atomics.notify(header, 0);\n}\n\n/** Same apply + verify scripts as the synchronous runner, evaluated anew for\n * every request. Reuse only the interpreter, never an ACL decision. A rejection\n * exits this interpreter with the original code; the next request starts fresh. */\nexport function windowsAclSessionProgram(): string {\n // Call-operator script blocks have child scope. Force the same process exit\n // that the original one-shot interpreter used for a rejected ACL.\n const interpreterExit = (script: string) => script.replace(/\\bexit (41|42|43)\\b/g, '[Environment]::Exit($1)');\n return `$apply = { ${interpreterExit(windowsAclApplyScript())} }\\n$verify = { ${interpreterExit(WINDOWS_ACL_VERIFY)} }\\n` + String.raw`\n$ErrorActionPreference='Stop'\nwhile($null -ne ($line=[Console]::In.ReadLine())) {\n $r=ConvertFrom-Json -InputObject $line\n if(@($r.PSObject.Properties.Name).Count -ne 4 -or $r.path -isnot [string] -or $r.directory -isnot [bool] -or $r.apply -isnot [bool] -or $r.sequence -lt 1){exit 47}\n $env:CAVEAT_ACL_PATH=$r.path\n $env:CAVEAT_ACL_DIRECTORY=if($r.directory){'1'}else{'0'}\n if($r.apply){& $apply}else{& $verify}\n [Console]::Out.WriteLine(([ordered]@{sequence=$r.sequence;ok=$true}|ConvertTo-Json -Compress))\n}\n`;\n}\n"],"mappings":"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","names":[]}
@@ -2,14 +2,13 @@
2
2
  import { dirname } from 'node:path';
3
3
  import { lstatSync } from 'node:fs';
4
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
- import { readManagedDeployment } from './managed-deployment.js';
5
+ import { readManagedDeploymentAtRoot } from './managed-deployment.js';
6
6
 
7
7
  // This v1 entry is version independent. Installer updates replace active.json,
8
8
  // never this command's definition. Keep argv[1] stable through the product import.
9
9
  const root = dirname(dirname(fileURLToPath(import.meta.url)));
10
- const userHome = dirname(dirname(dirname(dirname(root))));
11
10
  try {
12
- const active = readManagedDeployment({ userHome });
11
+ const active = readManagedDeploymentAtRoot(root);
13
12
  // File identity handles drive-case/8.3/extended Windows spellings without
14
13
  // accepting a symlink or changing the original stable argv registration.
15
14
  const invoked = lstatSync(process.argv[1], { bigint: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caveat-cli",
3
- "version": "0.21.1",
3
+ "version": "0.21.3",
4
4
  "description": "Long-term memory CLI for Claude Code, Codex, and Cursor (markdown + SQLite FTS5 + MCP + hooks)",
5
5
  "keywords": [
6
6
  "caveat",
@@ -37,7 +37,8 @@
37
37
  "files": [
38
38
  "dist",
39
39
  "README.md",
40
- "LICENSE"
40
+ "LICENSE",
41
+ "THIRD_PARTY_NOTICES"
41
42
  ],
42
43
  "publishConfig": {
43
44
  "access": "public"