@indigoai-us/hq-cli 5.125.0 → 5.127.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.
- package/CHANGELOG.md +329 -0
- package/assets/bot-workers/setup/skills/joining-a-team.md +125 -0
- package/assets/bot-workers/setup/skills/your-business.md +270 -0
- package/assets/bot-workers/setup/worker.yaml +189 -58
- package/assets/lanes-hooks/hq-lanes-drain.json +15 -0
- package/assets/lanes-hooks/hq-lanes-drain.mjs +254 -0
- package/assets/lanes-hooks/hq-lanes-drain.sh +21 -0
- package/assets/lanes-reconcile/hq-lanes-reconcile.service +11 -0
- package/assets/lanes-reconcile/hq-lanes-reconcile.timer +12 -0
- package/dist/command-catalog.generated.d.ts +643 -6
- package/dist/command-catalog.generated.js +836 -6
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +11 -1
- package/dist/commands/agents.d.ts +12 -0
- package/dist/commands/agents.js +46 -2
- package/dist/commands/bot.d.ts +10 -0
- package/dist/commands/bot.js +6 -0
- package/dist/commands/lanes.d.ts +114 -0
- package/dist/commands/lanes.js +2969 -0
- package/dist/commands/mesh.js +287 -24
- package/dist/commands/onboard-create-identity.d.ts +26 -0
- package/dist/commands/onboard-create-identity.js +23 -0
- package/dist/commands/onboard.js +13 -4
- package/dist/lib/bot/progress.d.ts +6 -0
- package/dist/lib/bot/progress.js +16 -0
- package/dist/lib/bot/runtime/grok.d.ts +30 -7
- package/dist/lib/bot/runtime/grok.js +86 -8
- package/dist/lib/bot/runtime/index.js +15 -8
- package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
- package/dist/lib/bot/runtime/messages-stream.js +49 -3
- package/dist/lib/lanes/accounts.d.ts +103 -0
- package/dist/lib/lanes/accounts.js +318 -0
- package/dist/lib/lanes/compaction.d.ts +96 -0
- package/dist/lib/lanes/compaction.js +197 -0
- package/dist/lib/lanes/config.d.ts +71 -0
- package/dist/lib/lanes/config.js +408 -0
- package/dist/lib/lanes/delivery.d.ts +151 -0
- package/dist/lib/lanes/delivery.js +354 -0
- package/dist/lib/lanes/dropbox.d.ts +96 -0
- package/dist/lib/lanes/dropbox.js +408 -0
- package/dist/lib/lanes/effort.d.ts +50 -0
- package/dist/lib/lanes/effort.js +109 -0
- package/dist/lib/lanes/errors.d.ts +39 -0
- package/dist/lib/lanes/errors.js +72 -0
- package/dist/lib/lanes/hooks-install.d.ts +81 -0
- package/dist/lib/lanes/hooks-install.js +326 -0
- package/dist/lib/lanes/hq-root.d.ts +19 -0
- package/dist/lib/lanes/hq-root.js +28 -0
- package/dist/lib/lanes/index.d.ts +49 -0
- package/dist/lib/lanes/index.js +27 -0
- package/dist/lib/lanes/liveness.d.ts +95 -0
- package/dist/lib/lanes/liveness.js +332 -0
- package/dist/lib/lanes/machine-identity.d.ts +34 -0
- package/dist/lib/lanes/machine-identity.js +92 -0
- package/dist/lib/lanes/mesh-events.d.ts +109 -0
- package/dist/lib/lanes/mesh-events.js +275 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +65 -0
- package/dist/lib/lanes/outpost-dispatch.js +90 -0
- package/dist/lib/lanes/questions.d.ts +75 -0
- package/dist/lib/lanes/questions.js +243 -0
- package/dist/lib/lanes/quota.d.ts +95 -0
- package/dist/lib/lanes/quota.js +320 -0
- package/dist/lib/lanes/reconcile-install.d.ts +69 -0
- package/dist/lib/lanes/reconcile-install.js +221 -0
- package/dist/lib/lanes/reconcile.d.ts +102 -0
- package/dist/lib/lanes/reconcile.js +985 -0
- package/dist/lib/lanes/registry.d.ts +116 -0
- package/dist/lib/lanes/registry.js +521 -0
- package/dist/lib/lanes/spawn.d.ts +274 -0
- package/dist/lib/lanes/spawn.js +1363 -0
- package/dist/lib/lanes/store.d.ts +68 -0
- package/dist/lib/lanes/store.js +745 -0
- package/dist/lib/lanes/throttle.d.ts +89 -0
- package/dist/lib/lanes/throttle.js +297 -0
- package/dist/lib/lanes/types.d.ts +535 -0
- package/dist/lib/lanes/types.js +350 -0
- package/dist/lib/lanes/watcher-main.d.ts +9 -0
- package/dist/lib/lanes/watcher-main.js +22 -0
- package/dist/lib/lanes/watcher.d.ts +216 -0
- package/dist/lib/lanes/watcher.js +859 -0
- package/dist/lib/mesh/api.d.ts +71 -7
- package/dist/lib/mesh/api.js +167 -28
- package/dist/lib/mesh/live/backfill-held.d.ts +18 -7
- package/dist/lib/mesh/live/backfill-held.js +66 -21
- package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
- package/dist/lib/mesh/live/daemon/config.js +75 -0
- package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
- package/dist/lib/mesh/live/daemon/index.js +3 -3
- package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
- package/dist/lib/mesh/live/daemon/install.js +444 -2
- package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/paths.js +2 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/run.js +111 -54
- package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.js +31 -8
- package/dist/lib/scan-packages/index.d.ts +2 -0
- package/dist/lib/scan-packages/index.js +18 -13
- package/dist/lib/work-context/config.d.ts +20 -0
- package/dist/lib/work-context/config.js +26 -5
- package/dist/lib/work-context/reconcile.js +28 -7
- package/dist/lib/work-context/state.d.ts +12 -0
- package/dist/lib/work-context/state.js +131 -3
- package/dist/lib/work-context/windows-acl.d.ts +123 -0
- package/dist/lib/work-context/windows-acl.js +425 -0
- package/dist/utils/pack-contributions.js +5 -4
- package/package.json +3 -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
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
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.
|
|
3
|
+
"version": "5.127.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"assets/scaffold",
|
|
14
14
|
"assets/bot-workers",
|
|
15
15
|
"assets/mesh-daemon",
|
|
16
|
+
"assets/lanes-reconcile",
|
|
17
|
+
"assets/lanes-hooks",
|
|
16
18
|
"CHANGELOG.md",
|
|
17
19
|
"README.md"
|
|
18
20
|
],
|