@indigoai-us/hq-cli 5.124.0 → 5.126.0

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 (80) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/assets/bot-workers/setup/skills/joining-a-team.md +125 -0
  3. package/assets/bot-workers/setup/skills/your-business.md +270 -0
  4. package/assets/bot-workers/setup/worker.yaml +189 -58
  5. package/dist/command-catalog.generated.d.ts +414 -5
  6. package/dist/command-catalog.generated.js +531 -5
  7. package/dist/command-registration-plan.d.ts +6 -0
  8. package/dist/command-registration-plan.js +1 -0
  9. package/dist/commands/bot.d.ts +10 -0
  10. package/dist/commands/bot.js +6 -0
  11. package/dist/commands/lanes.d.ts +75 -0
  12. package/dist/commands/lanes.js +2032 -0
  13. package/dist/commands/login.js +49 -6
  14. package/dist/commands/mesh.js +133 -28
  15. package/dist/commands/onboard-create-identity.d.ts +26 -0
  16. package/dist/commands/onboard-create-identity.js +23 -0
  17. package/dist/commands/onboard.js +13 -4
  18. package/dist/lib/bot/progress.d.ts +6 -0
  19. package/dist/lib/bot/progress.js +16 -0
  20. package/dist/lib/bot/runtime/grok.d.ts +30 -7
  21. package/dist/lib/bot/runtime/grok.js +86 -8
  22. package/dist/lib/bot/runtime/index.js +15 -8
  23. package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
  24. package/dist/lib/bot/runtime/messages-stream.js +49 -3
  25. package/dist/lib/doctor/checks/work-context.js +4 -2
  26. package/dist/lib/lanes/accounts.d.ts +43 -0
  27. package/dist/lib/lanes/accounts.js +128 -0
  28. package/dist/lib/lanes/config.d.ts +56 -0
  29. package/dist/lib/lanes/config.js +351 -0
  30. package/dist/lib/lanes/errors.d.ts +39 -0
  31. package/dist/lib/lanes/errors.js +72 -0
  32. package/dist/lib/lanes/index.d.ts +19 -0
  33. package/dist/lib/lanes/index.js +12 -0
  34. package/dist/lib/lanes/liveness.d.ts +89 -0
  35. package/dist/lib/lanes/liveness.js +273 -0
  36. package/dist/lib/lanes/registry.d.ts +103 -0
  37. package/dist/lib/lanes/registry.js +247 -0
  38. package/dist/lib/lanes/store.d.ts +64 -0
  39. package/dist/lib/lanes/store.js +627 -0
  40. package/dist/lib/lanes/throttle.d.ts +89 -0
  41. package/dist/lib/lanes/throttle.js +297 -0
  42. package/dist/lib/lanes/types.d.ts +193 -0
  43. package/dist/lib/lanes/types.js +217 -0
  44. package/dist/lib/mesh/live/backfill-held.d.ts +2 -0
  45. package/dist/lib/mesh/live/backfill-held.js +8 -4
  46. package/dist/lib/mesh/live/daemon/board-refresh.d.ts +3 -0
  47. package/dist/lib/mesh/live/daemon/board-refresh.js +6 -0
  48. package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
  49. package/dist/lib/mesh/live/daemon/config.js +75 -0
  50. package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
  51. package/dist/lib/mesh/live/daemon/doctor.js +21 -1
  52. package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
  53. package/dist/lib/mesh/live/daemon/index.js +3 -3
  54. package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
  55. package/dist/lib/mesh/live/daemon/install.js +444 -2
  56. package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
  57. package/dist/lib/mesh/live/daemon/paths.js +2 -0
  58. package/dist/lib/mesh/live/daemon/run.d.ts +11 -0
  59. package/dist/lib/mesh/live/daemon/run.js +129 -10
  60. package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
  61. package/dist/lib/mesh/live/flush.d.ts +3 -0
  62. package/dist/lib/mesh/live/flush.js +11 -0
  63. package/dist/lib/scan-packages/index.js +17 -12
  64. package/dist/lib/work-context/company.d.ts +8 -0
  65. package/dist/lib/work-context/company.js +62 -18
  66. package/dist/lib/work-context/config.d.ts +55 -0
  67. package/dist/lib/work-context/config.js +333 -67
  68. package/dist/lib/work-context/contract.d.ts +2 -0
  69. package/dist/lib/work-context/default-company.d.ts +43 -0
  70. package/dist/lib/work-context/default-company.js +83 -0
  71. package/dist/lib/work-context/outbox.d.ts +5 -1
  72. package/dist/lib/work-context/outbox.js +5 -0
  73. package/dist/lib/work-context/reconcile.d.ts +4 -0
  74. package/dist/lib/work-context/reconcile.js +86 -9
  75. package/dist/lib/work-context/state.d.ts +2 -0
  76. package/dist/lib/work-context/state.js +14 -0
  77. package/dist/lib/work-context/windows-acl.d.ts +123 -0
  78. package/dist/lib/work-context/windows-acl.js +425 -0
  79. package/dist/utils/pack-contributions.js +5 -4
  80. package/package.json +1 -1
@@ -0,0 +1,425 @@
1
+ /**
2
+ * Windows DACL check for the work-context config (US-018).
3
+ *
4
+ * NTFS has no POSIX mode bits: Node reports `mode` 0666 for every regular
5
+ * file on win32, so the POSIX `mode & 0o022` test in config.ts can never pass
6
+ * there. Instead of skipping the check on Windows, this module reads the real
7
+ * DACL and refuses the config when any principal other than the current user,
8
+ * SYSTEM, Administrators, or the owner is granted write/modify/full access.
9
+ *
10
+ * How the DACL is read (no native dependency):
11
+ * `icacls <path> /save <tmpfile>` writes the DACL as an SDDL string
12
+ * (`D:AI(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-...)`). SDDL carries
13
+ * SIDs or well-known SID abbreviations, never display names, so the check is
14
+ * independent of the OS language (VORDEFINIERT\Administratoren is still BA /
15
+ * S-1-5-32-544). The current user's SID comes from `whoami /user`. Display
16
+ * names are only resolved (via PowerShell) for the error message.
17
+ *
18
+ * Trust is decided by SID: S-1-5-18 (SYSTEM), S-1-5-32-544 (Administrators),
19
+ * S-1-3-0 (CREATOR OWNER), S-1-3-4 (OWNER RIGHTS), the current user's SID and
20
+ * the owner's SID when known.
21
+ *
22
+ * Reduced-guarantee path (explicit, documented): the check does NOT throw and
23
+ * instead emits one stderr diagnostic that the config is unprotected on this
24
+ * platform when, and only when, the DACL cannot be evaluated:
25
+ * - icacls is missing, fails, or its output has no SDDL line (unreadable);
26
+ * - an ACE grants write to a principal whose SID we cannot resolve (an SDDL
27
+ * abbreviation this module does not know), or carries a rights token we
28
+ * cannot decode, or the current user's SID is unknown and the grant is to
29
+ * a non-well-known SID (unresolvable).
30
+ * A DACL that parses and grants write to a resolved SID outside the trusted
31
+ * set always throws UnsafeConfigPathError. Nothing else falls back.
32
+ */
33
+ import * as fs from "node:fs";
34
+ import * as os from "node:os";
35
+ import * as path from "node:path";
36
+ import { spawnSync } from "node:child_process";
37
+ import { UnsafeConfigPathError } from "./errors.js";
38
+ // --- SIDs ---------------------------------------------------------------
39
+ export const SID_SYSTEM = "S-1-5-18";
40
+ export const SID_ADMINISTRATORS = "S-1-5-32-544";
41
+ export const SID_CREATOR_OWNER = "S-1-3-0";
42
+ export const SID_OWNER_RIGHTS = "S-1-3-4";
43
+ /** SDDL well-known account abbreviations (subset relevant to file DACLs). */
44
+ const SDDL_SID_ABBREVIATIONS = {
45
+ SY: SID_SYSTEM,
46
+ BA: SID_ADMINISTRATORS,
47
+ BU: "S-1-5-32-545", // Users
48
+ BG: "S-1-5-32-546", // Guests
49
+ PU: "S-1-5-32-547", // Power Users
50
+ WD: "S-1-1-0", // Everyone
51
+ AU: "S-1-5-11", // Authenticated Users
52
+ IU: "S-1-5-4", // Interactive
53
+ NU: "S-1-5-2", // Network
54
+ SU: "S-1-5-6", // Service
55
+ AN: "S-1-5-7", // Anonymous
56
+ LS: "S-1-5-19", // Local Service
57
+ NS: "S-1-5-20", // Network Service
58
+ CO: SID_CREATOR_OWNER,
59
+ CG: "S-1-3-1", // Creator Group
60
+ OW: SID_OWNER_RIGHTS,
61
+ RC: "S-1-5-12", // Restricted Code
62
+ AC: "S-1-15-2-1", // All application packages
63
+ DA: "S-1-5-21-<domain>-512", // Domain Admins (domain-relative; never trusted)
64
+ DU: "S-1-5-21-<domain>-513", // Domain Users
65
+ LA: "S-1-5-21-<domain>-500", // Local Administrator account
66
+ LG: "S-1-5-21-<domain>-501", // Local Guest account
67
+ };
68
+ /** Resolve an SDDL account field to a SID, or null when unknown. */
69
+ export function sddlAccountToSid(account) {
70
+ const a = account.trim();
71
+ if (/^S-1-\d+(-\d+)+$/i.test(a))
72
+ return a.toUpperCase();
73
+ const abbr = SDDL_SID_ABBREVIATIONS[a.toUpperCase()];
74
+ if (!abbr || abbr.includes("<domain>"))
75
+ return null;
76
+ return abbr;
77
+ }
78
+ // --- rights -------------------------------------------------------------
79
+ const FILE_WRITE_DATA = 0x2;
80
+ const FILE_APPEND_DATA = 0x4;
81
+ const FILE_WRITE_EA = 0x10;
82
+ const FILE_DELETE_CHILD = 0x40;
83
+ const FILE_WRITE_ATTRIBUTES = 0x100;
84
+ const DELETE = 0x10000;
85
+ const WRITE_DAC = 0x40000;
86
+ const WRITE_OWNER = 0x80000;
87
+ const GENERIC_ALL = 0x10000000;
88
+ const GENERIC_WRITE = 0x40000000;
89
+ /** Any of these bits lets the holder change the file or replace it. */
90
+ export const WRITE_MASK = FILE_WRITE_DATA |
91
+ FILE_APPEND_DATA |
92
+ FILE_WRITE_EA |
93
+ FILE_DELETE_CHILD |
94
+ FILE_WRITE_ATTRIBUTES |
95
+ DELETE |
96
+ WRITE_DAC |
97
+ WRITE_OWNER |
98
+ GENERIC_ALL |
99
+ GENERIC_WRITE;
100
+ /** SDDL rights tokens → access mask (generic + file-specific + DS bits). */
101
+ const SDDL_RIGHTS_TOKENS = {
102
+ GA: GENERIC_ALL,
103
+ GR: 0x80000000,
104
+ GW: GENERIC_WRITE,
105
+ GX: 0x20000000,
106
+ FA: 0x1f01ff,
107
+ FR: 0x120089,
108
+ FW: 0x120116,
109
+ FX: 0x1200a0,
110
+ KA: 0xf003f,
111
+ KR: 0x20019,
112
+ KW: 0x20006,
113
+ KX: 0x20019,
114
+ RC: 0x20000,
115
+ SD: DELETE,
116
+ WD: WRITE_DAC,
117
+ WO: WRITE_OWNER,
118
+ RP: 0x10,
119
+ WP: 0x20,
120
+ CC: 0x1,
121
+ DC: 0x2,
122
+ LC: 0x4,
123
+ SW: 0x8,
124
+ LO: 0x80,
125
+ DT: 0x40,
126
+ CR: 0x100,
127
+ };
128
+ /** Decode an SDDL rights field to an access mask, or null when undecodable. */
129
+ export function sddlRightsToMask(rights) {
130
+ const r = rights.trim();
131
+ if (!r)
132
+ return 0;
133
+ if (/^0x[0-9a-f]+$/i.test(r))
134
+ return Number.parseInt(r, 16) >>> 0;
135
+ if (/^\d+$/.test(r))
136
+ return Number.parseInt(r, 10) >>> 0;
137
+ if (r.length % 2 !== 0)
138
+ return null;
139
+ let mask = 0;
140
+ for (let i = 0; i < r.length; i += 2) {
141
+ const bits = SDDL_RIGHTS_TOKENS[r.slice(i, i + 2).toUpperCase()];
142
+ if (bits === undefined)
143
+ return null;
144
+ mask = (mask | bits) >>> 0;
145
+ }
146
+ return mask;
147
+ }
148
+ // --- parsing ------------------------------------------------------------
149
+ /**
150
+ * Parse the DACL portion of an SDDL string into ACEs. Accepts a full security
151
+ * descriptor string (`O:...G:...D:...S:...`) or just the `D:` section.
152
+ * Returns null when no DACL section is present or an ACE is malformed.
153
+ */
154
+ export function parseSddlDacl(sddl) {
155
+ const text = sddl.trim();
156
+ // Colons never occur inside an ACE or a SID, so section markers are unambiguous.
157
+ const dIdx = text.indexOf("D:");
158
+ if (dIdx < 0)
159
+ return null;
160
+ let body = text.slice(dIdx + 2);
161
+ const sIdx = body.indexOf("S:");
162
+ if (sIdx >= 0)
163
+ body = body.slice(0, sIdx);
164
+ // Strip DACL control flags (P, AI, AR, NO_ACCESS_CONTROL) before the first ACE.
165
+ const firstAce = body.indexOf("(");
166
+ const flags = firstAce < 0 ? body : body.slice(0, firstAce);
167
+ if (!/^[A-Z_]*$/i.test(flags.trim()))
168
+ return null;
169
+ if (firstAce < 0)
170
+ return [];
171
+ const aceText = body.slice(firstAce);
172
+ const aces = [];
173
+ const aceRe = /\(([^()]*)\)/g;
174
+ let consumed = 0;
175
+ let m;
176
+ while ((m = aceRe.exec(aceText)) !== null) {
177
+ if (m.index !== consumed)
178
+ return null; // junk between ACEs
179
+ consumed = m.index + m[0].length;
180
+ const fields = m[1].split(";");
181
+ if (fields.length < 6)
182
+ return null;
183
+ const [type, aceFlags, rights, , , account] = fields;
184
+ if (!type || !account)
185
+ return null;
186
+ aces.push({
187
+ type: type.trim().toUpperCase(),
188
+ flags: aceFlags.trim().toUpperCase(),
189
+ rights: rights.trim(),
190
+ account: account.trim(),
191
+ });
192
+ }
193
+ if (consumed !== aceText.trim().length)
194
+ return null;
195
+ return aces;
196
+ }
197
+ /**
198
+ * Parse the file `icacls <path> /save <file>` writes: UTF-16LE (optional BOM),
199
+ * CRLF, alternating `<name>` / `<sddl>` lines. Returns the SDDL of the first
200
+ * entry (the target itself), or an unreadable result.
201
+ */
202
+ export function parseIcaclsSaveOutput(raw) {
203
+ let text;
204
+ if (Buffer.isBuffer(raw)) {
205
+ const hasLeBom = raw.length >= 2 && raw[0] === 0xff && raw[1] === 0xfe;
206
+ text = raw.toString("utf16le", hasLeBom ? 2 : 0);
207
+ }
208
+ else {
209
+ text = raw;
210
+ }
211
+ text = text.replace(/^\uFEFF/, "");
212
+ const lines = text.split(/\r?\n/).map((l) => l.trim());
213
+ const sddl = lines.find((l) => /^(O:|G:|D:|S:)/.test(l));
214
+ if (!sddl) {
215
+ return { readable: false, reason: "no SDDL line in icacls /save output" };
216
+ }
217
+ return { readable: true, sddl };
218
+ }
219
+ // --- platform readers ---------------------------------------------------
220
+ /**
221
+ * Absolute path of a System32 tool. PATH lookup is not safe here: Git Bash and
222
+ * MSYS environments put a coreutils `whoami` ahead of Windows' `whoami.exe`,
223
+ * which does not understand `/user` and would make the current user SID look
224
+ * unknown on every run.
225
+ */
226
+ export function system32Tool(exe) {
227
+ const winDir = process.env.SystemRoot ?? process.env.windir ?? "C:\\Windows";
228
+ return path.join(winDir, "System32", exe);
229
+ }
230
+ export function readAclWithIcaclsSave(targetPath) {
231
+ const stamp = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
232
+ const saveFile = path.join(os.tmpdir(), `hq-acl-${stamp}.txt`);
233
+ try {
234
+ const res = spawnSync(system32Tool("icacls.exe"), [targetPath, "/save", saveFile], {
235
+ encoding: "utf8",
236
+ windowsHide: true,
237
+ timeout: 10_000,
238
+ });
239
+ if (res.error) {
240
+ return { readable: false, reason: `icacls unavailable: ${res.error.message}` };
241
+ }
242
+ if (res.status !== 0) {
243
+ const stderr = String(res.stderr ?? "").trim();
244
+ return {
245
+ readable: false,
246
+ reason: `icacls exited ${String(res.status)}${stderr ? `: ${stderr}` : ""}`,
247
+ };
248
+ }
249
+ let raw;
250
+ try {
251
+ raw = fs.readFileSync(saveFile);
252
+ }
253
+ catch (err) {
254
+ return { readable: false, reason: `icacls /save wrote nothing: ${err.message}` };
255
+ }
256
+ return parseIcaclsSaveOutput(raw);
257
+ }
258
+ catch (err) {
259
+ return { readable: false, reason: `icacls failed: ${err.message}` };
260
+ }
261
+ finally {
262
+ try {
263
+ fs.rmSync(saveFile, { force: true });
264
+ }
265
+ catch {
266
+ // best effort
267
+ }
268
+ }
269
+ }
270
+ let cachedCurrentUserSid;
271
+ /** Current user's SID via `whoami /user /fo csv /nh`; null when unavailable. */
272
+ export function readCurrentUserSid() {
273
+ if (cachedCurrentUserSid !== undefined)
274
+ return cachedCurrentUserSid;
275
+ try {
276
+ const res = spawnSync(system32Tool("whoami.exe"), ["/user", "/fo", "csv", "/nh"], {
277
+ encoding: "utf8",
278
+ windowsHide: true,
279
+ timeout: 10_000,
280
+ });
281
+ const m = /"(S-1-\d+(?:-\d+)+)"/i.exec(String(res.stdout ?? ""));
282
+ cachedCurrentUserSid = res.status === 0 && m ? m[1].toUpperCase() : null;
283
+ }
284
+ catch {
285
+ cachedCurrentUserSid = null;
286
+ }
287
+ return cachedCurrentUserSid;
288
+ }
289
+ /** Best-effort SID → account name (only used to build error messages). */
290
+ export function resolveSidNameWithPowerShell(sid) {
291
+ if (!/^S-1-\d+(-\d+)+$/i.test(sid))
292
+ return null;
293
+ try {
294
+ const script = `(New-Object System.Security.Principal.SecurityIdentifier('${sid}'))` +
295
+ `.Translate([System.Security.Principal.NTAccount]).Value`;
296
+ const res = spawnSync(system32Tool("WindowsPowerShell/v1.0/powershell.exe"), ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { encoding: "utf8", windowsHide: true, timeout: 15_000 });
297
+ const out = String(res.stdout ?? "").trim();
298
+ return res.status === 0 && out ? out : null;
299
+ }
300
+ catch {
301
+ return null;
302
+ }
303
+ }
304
+ // --- evaluation ---------------------------------------------------------
305
+ /**
306
+ * Evaluate parsed ACEs against the trusted SID set.
307
+ * - `unsafe`: an allow ACE grants a write bit to a resolved, untrusted SID.
308
+ * - `unresolvable`: a write-granting ACE whose SID or rights cannot be decoded,
309
+ * or whose SID is a per-machine/domain SID while the current user is unknown.
310
+ * - `safe`: everything else (deny ACEs and read-only grants are ignored).
311
+ */
312
+ export function evaluateDacl(aces, opts) {
313
+ const trusted = new Set([SID_SYSTEM, SID_ADMINISTRATORS, SID_CREATOR_OWNER, SID_OWNER_RIGHTS]);
314
+ if (opts.currentUserSid)
315
+ trusted.add(opts.currentUserSid.toUpperCase());
316
+ if (opts.ownerSid)
317
+ trusted.add(opts.ownerSid.toUpperCase());
318
+ for (const ace of aces) {
319
+ if (ace.type !== "A")
320
+ continue; // deny / audit / object ACEs never grant
321
+ if (ace.flags.includes("IO"))
322
+ continue; // inherit-only: no effect on this object
323
+ const mask = sddlRightsToMask(ace.rights);
324
+ if (mask === null) {
325
+ return {
326
+ kind: "unresolvable",
327
+ reason: `cannot decode rights "${ace.rights}" for ${ace.account}`,
328
+ };
329
+ }
330
+ if ((mask & WRITE_MASK) === 0)
331
+ continue;
332
+ const sid = sddlAccountToSid(ace.account);
333
+ if (!sid) {
334
+ return { kind: "unresolvable", reason: `cannot resolve principal "${ace.account}" to a SID` };
335
+ }
336
+ if (trusted.has(sid))
337
+ continue;
338
+ if (!opts.currentUserSid && /^S-1-5-21-/.test(sid)) {
339
+ return {
340
+ kind: "unresolvable",
341
+ reason: `current user SID unknown; cannot tell whether ${sid} is the current user`,
342
+ };
343
+ }
344
+ return { kind: "unsafe", sid, rights: ace.rights };
345
+ }
346
+ return { kind: "safe" };
347
+ }
348
+ const reducedGuaranteeReported = new Set();
349
+ /**
350
+ * Per-process memo of non-throwing verdicts, keyed by the object's identity
351
+ * and NTFS ChangeTime (Node `ctimeMs`), which advances whenever the DACL is
352
+ * modified. Spawning icacls costs ~100-200ms; hooks may read the config many
353
+ * times per process. Unsafe verdicts are never cached.
354
+ */
355
+ const verdictCache = new Map();
356
+ function cacheKey(targetPath) {
357
+ try {
358
+ const st = fs.lstatSync(targetPath);
359
+ return { ino: st.ino, ctimeMs: st.ctimeMs };
360
+ }
361
+ catch {
362
+ return null;
363
+ }
364
+ }
365
+ /**
366
+ * Assert the Windows DACL of `targetPath` grants write only to trusted
367
+ * principals. Throws UnsafeConfigPathError naming the offending principal.
368
+ * Unreadable or unresolvable DACL → reduced-guarantee diagnostic (once per
369
+ * path), no throw. See the module header for the exact conditions.
370
+ */
371
+ export function assertWindowsOwnerOnlyAcl(targetPath, kind, opts = {}) {
372
+ const readAcl = opts.readAcl ?? readAclWithIcaclsSave;
373
+ const resolveSidName = opts.resolveSidName ?? resolveSidNameWithPowerShell;
374
+ const diagnostic = opts.diagnostic ?? ((line) => process.stderr.write(`${line}\n`));
375
+ const currentUserSid = opts.currentUserSid === undefined ? readCurrentUserSid() : opts.currentUserSid;
376
+ const reducedGuarantee = (reason) => {
377
+ // Reduced-guarantee path: see module header. Deliberately not a throw.
378
+ if (reducedGuaranteeReported.has(targetPath))
379
+ return;
380
+ reducedGuaranteeReported.add(targetPath);
381
+ diagnostic(`work-context: could not evaluate Windows ACL for ${kind} ${targetPath} (${reason}); ` +
382
+ "config is unprotected on this platform (reduced guarantee)");
383
+ };
384
+ const key = cacheKey(targetPath);
385
+ const cached = key ? verdictCache.get(targetPath) : undefined;
386
+ if (key && cached && cached.ino === key.ino && cached.ctimeMs === key.ctimeMs)
387
+ return;
388
+ const acl = readAcl(targetPath);
389
+ if (!acl.readable) {
390
+ reducedGuarantee(acl.reason);
391
+ if (key)
392
+ verdictCache.set(targetPath, key);
393
+ return;
394
+ }
395
+ const aces = parseSddlDacl(acl.sddl);
396
+ if (aces === null) {
397
+ reducedGuarantee(`unparseable SDDL "${acl.sddl}"`);
398
+ if (key)
399
+ verdictCache.set(targetPath, key);
400
+ return;
401
+ }
402
+ const verdict = evaluateDacl(aces, { currentUserSid, ownerSid: opts.ownerSid ?? null });
403
+ if (verdict.kind === "safe") {
404
+ if (key)
405
+ verdictCache.set(targetPath, key);
406
+ return;
407
+ }
408
+ if (verdict.kind === "unresolvable") {
409
+ reducedGuarantee(verdict.reason);
410
+ if (key)
411
+ verdictCache.set(targetPath, key);
412
+ return;
413
+ }
414
+ const name = resolveSidName(verdict.sid);
415
+ const who = name ? `${name} (${verdict.sid})` : verdict.sid;
416
+ throw new UnsafeConfigPathError(`Config ${kind} ${targetPath} is writable by ${who} [rights ${verdict.rights}] ` +
417
+ "(expected owner-only: current user, SYSTEM, Administrators)");
418
+ }
419
+ /** Test hook: forget which paths already emitted the reduced-guarantee line. */
420
+ export function resetWindowsAclDiagnostics() {
421
+ reducedGuaranteeReported.clear();
422
+ verdictCache.clear();
423
+ cachedCurrentUserSid = undefined;
424
+ }
425
+ //# sourceMappingURL=windows-acl.js.map
@@ -199,10 +199,11 @@ export function linkStatus(link, packDir) {
199
199
  catch {
200
200
  return 'foreign';
201
201
  }
202
- // scan-packages.sh writes the symlink target as the absolute `src` path.
203
- // Canonicalize both sides so `/var` vs `/private/var` (macOS) and symlink
204
- // aliases classify as ours; optionally accept stale targets that still resolve
205
- // inside this pack's directory.
202
+ // scan-packages writes the symlink target as a path relative to the host
203
+ // directory (`path.relative(dirname(dst), src)`). Older trees still carry
204
+ // absolute `src` strings; canonicalize both so those, `/var` vs `/private/var`
205
+ // (macOS), and symlink aliases classify as ours; optionally accept stale
206
+ // targets that still resolve inside this pack's directory.
206
207
  const resolvedTarget = path.resolve(path.dirname(link.dst), target);
207
208
  if (pointsAtDeclaredPayload(link, resolvedTarget)) {
208
209
  return fs.existsSync(link.src) ? 'live' : 'broken';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.124.0",
3
+ "version": "5.126.0",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {