@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.
- package/CHANGELOG.md +149 -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/dist/command-catalog.generated.d.ts +414 -5
- package/dist/command-catalog.generated.js +531 -5
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/bot.d.ts +10 -0
- package/dist/commands/bot.js +6 -0
- package/dist/commands/lanes.d.ts +75 -0
- package/dist/commands/lanes.js +2032 -0
- package/dist/commands/login.js +49 -6
- package/dist/commands/mesh.js +133 -28
- 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/doctor/checks/work-context.js +4 -2
- package/dist/lib/lanes/accounts.d.ts +43 -0
- package/dist/lib/lanes/accounts.js +128 -0
- package/dist/lib/lanes/config.d.ts +56 -0
- package/dist/lib/lanes/config.js +351 -0
- package/dist/lib/lanes/errors.d.ts +39 -0
- package/dist/lib/lanes/errors.js +72 -0
- package/dist/lib/lanes/index.d.ts +19 -0
- package/dist/lib/lanes/index.js +12 -0
- package/dist/lib/lanes/liveness.d.ts +89 -0
- package/dist/lib/lanes/liveness.js +273 -0
- package/dist/lib/lanes/registry.d.ts +103 -0
- package/dist/lib/lanes/registry.js +247 -0
- package/dist/lib/lanes/store.d.ts +64 -0
- package/dist/lib/lanes/store.js +627 -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 +193 -0
- package/dist/lib/lanes/types.js +217 -0
- package/dist/lib/mesh/live/backfill-held.d.ts +2 -0
- package/dist/lib/mesh/live/backfill-held.js +8 -4
- package/dist/lib/mesh/live/daemon/board-refresh.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/board-refresh.js +6 -0
- 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/doctor.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/doctor.js +21 -1
- 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 +11 -0
- package/dist/lib/mesh/live/daemon/run.js +129 -10
- package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
- package/dist/lib/mesh/live/flush.d.ts +3 -0
- package/dist/lib/mesh/live/flush.js +11 -0
- package/dist/lib/scan-packages/index.js +17 -12
- package/dist/lib/work-context/company.d.ts +8 -0
- package/dist/lib/work-context/company.js +62 -18
- package/dist/lib/work-context/config.d.ts +55 -0
- package/dist/lib/work-context/config.js +333 -67
- package/dist/lib/work-context/contract.d.ts +2 -0
- package/dist/lib/work-context/default-company.d.ts +43 -0
- package/dist/lib/work-context/default-company.js +83 -0
- package/dist/lib/work-context/outbox.d.ts +5 -1
- package/dist/lib/work-context/outbox.js +5 -0
- package/dist/lib/work-context/reconcile.d.ts +4 -0
- package/dist/lib/work-context/reconcile.js +86 -9
- package/dist/lib/work-context/state.d.ts +2 -0
- package/dist/lib/work-context/state.js +14 -0
- 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 +1 -1
|
@@ -21,6 +21,7 @@ export const OUTBOX_ALLOWLIST = [
|
|
|
21
21
|
"companySlug",
|
|
22
22
|
"projectId",
|
|
23
23
|
"taskId",
|
|
24
|
+
"companyConfidence",
|
|
24
25
|
"kind",
|
|
25
26
|
"digest",
|
|
26
27
|
"delivery",
|
|
@@ -96,6 +97,8 @@ function projectOutbox(op) {
|
|
|
96
97
|
out.projectId = op.projectId;
|
|
97
98
|
if (op.taskId)
|
|
98
99
|
out.taskId = op.taskId;
|
|
100
|
+
if (op.companyConfidence)
|
|
101
|
+
out.companyConfidence = op.companyConfidence;
|
|
99
102
|
if (op.lastErrorCode)
|
|
100
103
|
out.lastErrorCode = op.lastErrorCode;
|
|
101
104
|
if (op.receiptId)
|
|
@@ -143,6 +146,7 @@ export function enqueueOutbox(input, root) {
|
|
|
143
146
|
companySlug: input.companySlug,
|
|
144
147
|
projectId: input.projectId,
|
|
145
148
|
taskId: input.taskId,
|
|
149
|
+
companyConfidence: input.companyConfidence,
|
|
146
150
|
kind,
|
|
147
151
|
destinationCompanyUid: input.destinationCompanyUid,
|
|
148
152
|
destinationCompanySlug: input.destinationCompanySlug,
|
|
@@ -172,6 +176,7 @@ export function enqueueOutbox(input, root) {
|
|
|
172
176
|
companySlug: input.companySlug,
|
|
173
177
|
projectId: input.projectId,
|
|
174
178
|
taskId: input.taskId,
|
|
179
|
+
companyConfidence: input.companyConfidence,
|
|
175
180
|
kind,
|
|
176
181
|
digest,
|
|
177
182
|
delivery: "queued",
|
|
@@ -52,6 +52,10 @@ export interface ReconcileDeps {
|
|
|
52
52
|
root: string;
|
|
53
53
|
env?: NodeJS.ProcessEnv;
|
|
54
54
|
now?: () => Date;
|
|
55
|
+
/** Used only by explicit historical repair; ordinary reconciliation keeps defaults enabled. */
|
|
56
|
+
allowDeviceDefault?: boolean;
|
|
57
|
+
/** Mark explicit historical default repair as lower-confidence local evidence. */
|
|
58
|
+
deviceDefaultConfidence?: boolean;
|
|
55
59
|
/** Network deliverer; only called after outbox write. */
|
|
56
60
|
deliver?: DeliverFn;
|
|
57
61
|
/** Skip network even when company is resolved (local unit tests). */
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import { InvalidSessionIdentityError, normalizeSessionIdentity, } from "../mesh/live/session-identity.js";
|
|
7
7
|
import { companyCorrectionPath, companySlugFromCwd, resolveCompany, } from "./company.js";
|
|
8
|
+
import { invalidateDefaultCompanyMemberships, } from "./config.js";
|
|
8
9
|
import { WORK_CONTEXT_CONTRACT_VERSION, normalizeTaskId, } from "./contract.js";
|
|
9
10
|
import { EXIT_INVALID_IDENTITY, EXIT_NOT_TRACKING, EXIT_OK, InvalidDecisionOriginError, NotTrackingError, } from "./errors.js";
|
|
10
11
|
import { decisionFromCandidates, } from "./organize.js";
|
|
@@ -24,6 +25,8 @@ function resultOf(parts) {
|
|
|
24
25
|
};
|
|
25
26
|
if (parts.companyUid)
|
|
26
27
|
out.companyUid = parts.companyUid;
|
|
28
|
+
if (parts.companySlug)
|
|
29
|
+
out.companySlug = parts.companySlug;
|
|
27
30
|
if (parts.projectId)
|
|
28
31
|
out.projectId = parts.projectId;
|
|
29
32
|
if (parts.taskId)
|
|
@@ -222,6 +225,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
222
225
|
hqRoot,
|
|
223
226
|
remoteOwnerSlug,
|
|
224
227
|
repoIdentityKey,
|
|
228
|
+
allowDeviceDefault: deps.allowDeviceDefault,
|
|
225
229
|
existingState: prior,
|
|
226
230
|
});
|
|
227
231
|
const nowIso = nowFn().toISOString();
|
|
@@ -341,8 +345,26 @@ export async function reconcileObservation(obs, deps) {
|
|
|
341
345
|
};
|
|
342
346
|
}
|
|
343
347
|
// Membership-validated slug→uid when local resolution only yielded a slug.
|
|
348
|
+
// A device default is weak, cached evidence: validate it authoritatively on
|
|
349
|
+
// every tenant-I/O path so a membership revocation takes effect immediately.
|
|
344
350
|
let resolvedCompany = company.company;
|
|
345
|
-
|
|
351
|
+
const deviceDefaultRequiresValidation = resolvedCompany.source === "device_default";
|
|
352
|
+
if (deviceDefaultRequiresValidation && !deps.validateMembership) {
|
|
353
|
+
return {
|
|
354
|
+
result: resultOf({
|
|
355
|
+
kind: "needs_company",
|
|
356
|
+
classification: "needs_company",
|
|
357
|
+
delivery: "clean",
|
|
358
|
+
sessionId,
|
|
359
|
+
clientOperationId: obs.clientOperationId,
|
|
360
|
+
}),
|
|
361
|
+
exitCode: EXIT_OK,
|
|
362
|
+
diagnostics: ["needs_company: device-default membership validator unavailable"],
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (resolvedCompany.slug &&
|
|
366
|
+
deps.validateMembership &&
|
|
367
|
+
(!resolvedCompany.uid || deviceDefaultRequiresValidation)) {
|
|
346
368
|
try {
|
|
347
369
|
const membership = await deps.validateMembership(resolvedCompany.slug);
|
|
348
370
|
if (membership && typeof membership === "object" && membership.uid) {
|
|
@@ -353,6 +375,9 @@ export async function reconcileObservation(obs, deps) {
|
|
|
353
375
|
diagnostics.push(`membership resolved companyUid for slug=${resolvedCompany.slug}`);
|
|
354
376
|
}
|
|
355
377
|
else if (membership === false) {
|
|
378
|
+
if (deviceDefaultRequiresValidation) {
|
|
379
|
+
invalidateDefaultCompanyMemberships({ root: deps.root, now: nowFn });
|
|
380
|
+
}
|
|
356
381
|
diagnostics.push(`membership denied for slug=${resolvedCompany.slug}; treating as needs_company`);
|
|
357
382
|
const statePath = writeSessionState(mergeLocalSessionFields({
|
|
358
383
|
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
@@ -384,6 +409,19 @@ export async function reconcileObservation(obs, deps) {
|
|
|
384
409
|
}
|
|
385
410
|
catch (err) {
|
|
386
411
|
diagnostics.push(`membership lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
412
|
+
if (deviceDefaultRequiresValidation) {
|
|
413
|
+
return {
|
|
414
|
+
result: resultOf({
|
|
415
|
+
kind: "needs_company",
|
|
416
|
+
classification: "needs_company",
|
|
417
|
+
delivery: "clean",
|
|
418
|
+
sessionId,
|
|
419
|
+
clientOperationId: obs.clientOperationId,
|
|
420
|
+
}),
|
|
421
|
+
exitCode: EXIT_OK,
|
|
422
|
+
diagnostics: [...diagnostics, "needs_company: device-default membership refresh unavailable"],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
387
425
|
}
|
|
388
426
|
}
|
|
389
427
|
// Company resolved — resolve project/task (US-007B order).
|
|
@@ -480,6 +518,11 @@ export async function reconcileObservation(obs, deps) {
|
|
|
480
518
|
registerClassification === "unresolved") {
|
|
481
519
|
registerClassification = "needs_project";
|
|
482
520
|
}
|
|
521
|
+
// Offline preflight has authoritative membership but deliberately does not
|
|
522
|
+
// invent a project. Expose that usable, validated state to hook consumers.
|
|
523
|
+
if (resolvedCompany.source === "device_default" && deps.offline && registerClassification === "unresolved") {
|
|
524
|
+
registerClassification = "needs_project";
|
|
525
|
+
}
|
|
483
526
|
// Do not let a weak later observation erase a server-acknowledged company
|
|
484
527
|
// binding. Keep the result unresolved so the normal candidate threshold can
|
|
485
528
|
// still advance the session, while preserving the durable emitter projection.
|
|
@@ -544,6 +587,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
544
587
|
sessionId,
|
|
545
588
|
clientOperationId: obs.clientOperationId,
|
|
546
589
|
companyUid: resolvedCompany.uid,
|
|
590
|
+
companySlug: resolvedCompany.slug,
|
|
547
591
|
projectId,
|
|
548
592
|
taskId,
|
|
549
593
|
bindingEpisodeId: acknowledgedPrior.bindingEpisodeId,
|
|
@@ -553,6 +597,9 @@ export async function reconcileObservation(obs, deps) {
|
|
|
553
597
|
};
|
|
554
598
|
}
|
|
555
599
|
let outboxOp;
|
|
600
|
+
const companyConfidence = company.status === "resolved" && company.company.source === "device_default"
|
|
601
|
+
? "device_default"
|
|
602
|
+
: undefined;
|
|
556
603
|
try {
|
|
557
604
|
outboxOp = enqueueOutbox({
|
|
558
605
|
clientOperationId: obs.clientOperationId,
|
|
@@ -562,6 +609,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
562
609
|
companySlug: resolvedCompany.slug,
|
|
563
610
|
projectId,
|
|
564
611
|
taskId,
|
|
612
|
+
companyConfidence,
|
|
565
613
|
kind: "register",
|
|
566
614
|
now: nowFn,
|
|
567
615
|
}, deps.root);
|
|
@@ -588,8 +636,9 @@ export async function reconcileObservation(obs, deps) {
|
|
|
588
636
|
diagnostics.push(`project source=${projectResolved.source} projectId=${projectId ?? "-"} taskId=${taskId ?? "-"}`);
|
|
589
637
|
}
|
|
590
638
|
let statePath;
|
|
639
|
+
let stateWrite;
|
|
591
640
|
try {
|
|
592
|
-
|
|
641
|
+
stateWrite = mergeLocalSessionFields({
|
|
593
642
|
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
594
643
|
sessionId,
|
|
595
644
|
companyUid: resolvedCompany.uid,
|
|
@@ -602,6 +651,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
602
651
|
...(typeof toolWrites === "number" ? { toolWrites } : {}),
|
|
603
652
|
...(hasTurn ? { hasTurn: true } : {}),
|
|
604
653
|
...(decision ? { decision } : {}),
|
|
654
|
+
...(companyConfidence ? { companyConfidence } : {}),
|
|
605
655
|
...transcriptFields,
|
|
606
656
|
}, prior);
|
|
607
657
|
statePath = writeSessionState(stateWrite, deps.root);
|
|
@@ -616,6 +666,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
616
666
|
sessionId,
|
|
617
667
|
clientOperationId: obs.clientOperationId,
|
|
618
668
|
companyUid: resolvedCompany.uid,
|
|
669
|
+
companySlug: resolvedCompany.slug,
|
|
619
670
|
projectId,
|
|
620
671
|
taskId,
|
|
621
672
|
errorCode: "NOT_TRACKING",
|
|
@@ -636,6 +687,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
636
687
|
sessionId,
|
|
637
688
|
clientOperationId: obs.clientOperationId,
|
|
638
689
|
companyUid: resolvedCompany.uid,
|
|
690
|
+
companySlug: resolvedCompany.slug,
|
|
639
691
|
projectId,
|
|
640
692
|
taskId,
|
|
641
693
|
}),
|
|
@@ -649,23 +701,47 @@ export async function reconcileObservation(obs, deps) {
|
|
|
649
701
|
if (delivered.ok) {
|
|
650
702
|
markOutboxAcked(outboxOp.operationId, deps.root, delivered.receiptId, nowFn);
|
|
651
703
|
writeAckedRegisterState(outboxOp, deps.root, delivered.receiptId, nowFn);
|
|
652
|
-
//
|
|
653
|
-
|
|
704
|
+
// Company attribution write for EVERY acked register that resolved a
|
|
705
|
+
// company — not only bound sessions. A company-only ack (needs_project /
|
|
706
|
+
// needs_task / unresolved) must still project companyUid/companySlug and
|
|
707
|
+
// the true computed classification into durable session state; otherwise
|
|
708
|
+
// the emitter and mesh presence lose the tenant the server already
|
|
709
|
+
// acknowledged. projectId / taskId are written only when genuinely
|
|
710
|
+
// resolved, and a fresh bindingEpisodeId only when the session is bound —
|
|
711
|
+
// never fabricated on a partial path. (writeAckedRegisterState above may
|
|
712
|
+
// have already recorded a receipt-derived episode for a resolved partial
|
|
713
|
+
// binding; the default merge preserves that when the binding still matches.)
|
|
714
|
+
if (resolvedCompany.uid) {
|
|
715
|
+
const bound = registerClassification === "bound";
|
|
716
|
+
// A server ack of the company makes it authoritative: the durable status
|
|
717
|
+
// is at least needs_project (company known, no project), needs_task
|
|
718
|
+
// (project, no task), or bound (project + task) — mirroring
|
|
719
|
+
// acknowledgedStatus() in state.ts. Never downgrade an acked company to a
|
|
720
|
+
// pre-threshold "unresolved" projection; that is what dropped the tenant.
|
|
721
|
+
const ackedClassification = projectId && taskId
|
|
722
|
+
? "bound"
|
|
723
|
+
: projectId
|
|
724
|
+
? "needs_task"
|
|
725
|
+
: "needs_project";
|
|
654
726
|
writeSessionState(mergeLocalSessionFields({
|
|
655
727
|
contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
|
|
656
728
|
sessionId,
|
|
657
729
|
companyUid: resolvedCompany.uid,
|
|
658
730
|
companySlug: resolvedCompany.slug,
|
|
659
|
-
contextStatus: "bound",
|
|
660
|
-
projectId,
|
|
661
|
-
taskId,
|
|
662
|
-
bindingEpisodeId: delivered.receiptId,
|
|
731
|
+
contextStatus: bound ? "bound" : ackedClassification,
|
|
732
|
+
...(projectId ? { projectId } : {}),
|
|
733
|
+
...(taskId ? { taskId } : {}),
|
|
734
|
+
...(bound ? { bindingEpisodeId: delivered.receiptId } : {}),
|
|
663
735
|
updatedAt: nowFn().toISOString(),
|
|
664
736
|
startedAt,
|
|
665
737
|
...(typeof toolWrites === "number" ? { toolWrites } : {}),
|
|
666
738
|
...(hasTurn ? { hasTurn: true } : {}),
|
|
667
739
|
...transcriptFields,
|
|
668
|
-
},
|
|
740
|
+
},
|
|
741
|
+
// Merge against the just-written projection rather than the stale
|
|
742
|
+
// pre-reconcile snapshot, so device confidence and any receipt
|
|
743
|
+
// attribution survive.
|
|
744
|
+
readSessionState(sessionId, deps.root) ?? stateWrite), deps.root);
|
|
669
745
|
}
|
|
670
746
|
const kind = registerClassification === "bound"
|
|
671
747
|
? "bound"
|
|
@@ -721,6 +797,7 @@ export async function reconcileObservation(obs, deps) {
|
|
|
721
797
|
sessionId,
|
|
722
798
|
clientOperationId: obs.clientOperationId,
|
|
723
799
|
companyUid: resolvedCompany.uid,
|
|
800
|
+
companySlug: resolvedCompany.slug,
|
|
724
801
|
projectId,
|
|
725
802
|
taskId,
|
|
726
803
|
errorCode: delivered.code,
|
|
@@ -89,6 +89,8 @@ export interface SessionStateFile {
|
|
|
89
89
|
* before SESSION_STATE sync (kept after AMEND-001).
|
|
90
90
|
*/
|
|
91
91
|
transcriptMarker?: "from transcript";
|
|
92
|
+
/** Historical repair under device-default evidence is intentionally weak. */
|
|
93
|
+
companyConfidence?: "device_default";
|
|
92
94
|
updatedAt: string;
|
|
93
95
|
}
|
|
94
96
|
/**
|
|
@@ -27,6 +27,7 @@ export const SESSION_STATE_ALLOWLIST = new Set([
|
|
|
27
27
|
"source",
|
|
28
28
|
"localHarness",
|
|
29
29
|
"transcriptMarker",
|
|
30
|
+
"companyConfidence",
|
|
30
31
|
"updatedAt",
|
|
31
32
|
]);
|
|
32
33
|
export function readSessionState(sessionId, root) {
|
|
@@ -100,6 +101,8 @@ export function projectSessionState(state) {
|
|
|
100
101
|
out.localHarness = state.localHarness;
|
|
101
102
|
if (state.transcriptMarker)
|
|
102
103
|
out.transcriptMarker = state.transcriptMarker;
|
|
104
|
+
if (state.companyConfidence)
|
|
105
|
+
out.companyConfidence = state.companyConfidence;
|
|
103
106
|
return out;
|
|
104
107
|
}
|
|
105
108
|
/**
|
|
@@ -148,6 +151,13 @@ export function mergeLocalSessionFields(next, prior, options = {}) {
|
|
|
148
151
|
if (!out.transcriptMarker && prior.transcriptMarker) {
|
|
149
152
|
out.transcriptMarker = prior.transcriptMarker;
|
|
150
153
|
}
|
|
154
|
+
const sameCompanyBinding = Boolean(prior.companyUid && out.companyUid && prior.companyUid === out.companyUid) ||
|
|
155
|
+
Boolean(prior.companySlug &&
|
|
156
|
+
out.companySlug &&
|
|
157
|
+
prior.companySlug.toLowerCase() === out.companySlug.toLowerCase());
|
|
158
|
+
if (!out.companyConfidence && prior.companyConfidence && sameCompanyBinding) {
|
|
159
|
+
out.companyConfidence = prior.companyConfidence;
|
|
160
|
+
}
|
|
151
161
|
const sameBinding = prior.companyUid === out.companyUid &&
|
|
152
162
|
prior.companySlug === out.companySlug &&
|
|
153
163
|
prior.projectId === out.projectId &&
|
|
@@ -237,6 +247,10 @@ export function isHookWrittenSessionState(state) {
|
|
|
237
247
|
export function authoritativeCompanyFromState(state) {
|
|
238
248
|
if (!state)
|
|
239
249
|
return null;
|
|
250
|
+
// A binding created only from the device default is deliberately weak. A
|
|
251
|
+
// later /startwork meta designation or remembered repo choice may replace it.
|
|
252
|
+
if (state.companyConfidence === "device_default")
|
|
253
|
+
return null;
|
|
240
254
|
const writable = new Set([
|
|
241
255
|
"unresolved",
|
|
242
256
|
"needs_project",
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
export interface SddlAce {
|
|
34
|
+
/** ACE type token, e.g. `A` (allow) or `D` (deny). */
|
|
35
|
+
type: string;
|
|
36
|
+
/** ACE flag string, e.g. `OICIID`. */
|
|
37
|
+
flags: string;
|
|
38
|
+
/** Rights field as written, e.g. `FA`, `0x1200a9`, `CCDCLCSWRPWPRC`. */
|
|
39
|
+
rights: string;
|
|
40
|
+
/** Account field as written: full SID or two-letter abbreviation. */
|
|
41
|
+
account: string;
|
|
42
|
+
}
|
|
43
|
+
export type AclReadResult = {
|
|
44
|
+
readable: true;
|
|
45
|
+
sddl: string;
|
|
46
|
+
} | {
|
|
47
|
+
readable: false;
|
|
48
|
+
reason: string;
|
|
49
|
+
};
|
|
50
|
+
export interface WindowsAclCheckOptions {
|
|
51
|
+
/** Current user SID (defaults to `whoami /user`). `null` = unknown. */
|
|
52
|
+
currentUserSid?: string | null;
|
|
53
|
+
/** Owner SID when known (icacls /save does not include the owner). */
|
|
54
|
+
ownerSid?: string | null;
|
|
55
|
+
/** DACL reader; injectable for tests. Defaults to `icacls <path> /save`. */
|
|
56
|
+
readAcl?: (targetPath: string) => AclReadResult;
|
|
57
|
+
/** SID → display name for error messages; injectable for tests. */
|
|
58
|
+
resolveSidName?: (sid: string) => string | null;
|
|
59
|
+
/** Diagnostic sink for the reduced-guarantee path (defaults to stderr). */
|
|
60
|
+
diagnostic?: (line: string) => void;
|
|
61
|
+
}
|
|
62
|
+
export type AclVerdict = {
|
|
63
|
+
kind: "safe";
|
|
64
|
+
} | {
|
|
65
|
+
kind: "unsafe";
|
|
66
|
+
sid: string;
|
|
67
|
+
rights: string;
|
|
68
|
+
} | {
|
|
69
|
+
kind: "unresolvable";
|
|
70
|
+
reason: string;
|
|
71
|
+
};
|
|
72
|
+
export declare const SID_SYSTEM = "S-1-5-18";
|
|
73
|
+
export declare const SID_ADMINISTRATORS = "S-1-5-32-544";
|
|
74
|
+
export declare const SID_CREATOR_OWNER = "S-1-3-0";
|
|
75
|
+
export declare const SID_OWNER_RIGHTS = "S-1-3-4";
|
|
76
|
+
/** Resolve an SDDL account field to a SID, or null when unknown. */
|
|
77
|
+
export declare function sddlAccountToSid(account: string): string | null;
|
|
78
|
+
/** Any of these bits lets the holder change the file or replace it. */
|
|
79
|
+
export declare const WRITE_MASK: number;
|
|
80
|
+
/** Decode an SDDL rights field to an access mask, or null when undecodable. */
|
|
81
|
+
export declare function sddlRightsToMask(rights: string): number | null;
|
|
82
|
+
/**
|
|
83
|
+
* Parse the DACL portion of an SDDL string into ACEs. Accepts a full security
|
|
84
|
+
* descriptor string (`O:...G:...D:...S:...`) or just the `D:` section.
|
|
85
|
+
* Returns null when no DACL section is present or an ACE is malformed.
|
|
86
|
+
*/
|
|
87
|
+
export declare function parseSddlDacl(sddl: string): SddlAce[] | null;
|
|
88
|
+
/**
|
|
89
|
+
* Parse the file `icacls <path> /save <file>` writes: UTF-16LE (optional BOM),
|
|
90
|
+
* CRLF, alternating `<name>` / `<sddl>` lines. Returns the SDDL of the first
|
|
91
|
+
* entry (the target itself), or an unreadable result.
|
|
92
|
+
*/
|
|
93
|
+
export declare function parseIcaclsSaveOutput(raw: string | Buffer): AclReadResult;
|
|
94
|
+
/**
|
|
95
|
+
* Absolute path of a System32 tool. PATH lookup is not safe here: Git Bash and
|
|
96
|
+
* MSYS environments put a coreutils `whoami` ahead of Windows' `whoami.exe`,
|
|
97
|
+
* which does not understand `/user` and would make the current user SID look
|
|
98
|
+
* unknown on every run.
|
|
99
|
+
*/
|
|
100
|
+
export declare function system32Tool(exe: string): string;
|
|
101
|
+
export declare function readAclWithIcaclsSave(targetPath: string): AclReadResult;
|
|
102
|
+
/** Current user's SID via `whoami /user /fo csv /nh`; null when unavailable. */
|
|
103
|
+
export declare function readCurrentUserSid(): string | null;
|
|
104
|
+
/** Best-effort SID → account name (only used to build error messages). */
|
|
105
|
+
export declare function resolveSidNameWithPowerShell(sid: string): string | null;
|
|
106
|
+
/**
|
|
107
|
+
* Evaluate parsed ACEs against the trusted SID set.
|
|
108
|
+
* - `unsafe`: an allow ACE grants a write bit to a resolved, untrusted SID.
|
|
109
|
+
* - `unresolvable`: a write-granting ACE whose SID or rights cannot be decoded,
|
|
110
|
+
* or whose SID is a per-machine/domain SID while the current user is unknown.
|
|
111
|
+
* - `safe`: everything else (deny ACEs and read-only grants are ignored).
|
|
112
|
+
*/
|
|
113
|
+
export declare function evaluateDacl(aces: SddlAce[], opts: Pick<WindowsAclCheckOptions, "currentUserSid" | "ownerSid">): AclVerdict;
|
|
114
|
+
/**
|
|
115
|
+
* Assert the Windows DACL of `targetPath` grants write only to trusted
|
|
116
|
+
* principals. Throws UnsafeConfigPathError naming the offending principal.
|
|
117
|
+
* Unreadable or unresolvable DACL → reduced-guarantee diagnostic (once per
|
|
118
|
+
* path), no throw. See the module header for the exact conditions.
|
|
119
|
+
*/
|
|
120
|
+
export declare function assertWindowsOwnerOnlyAcl(targetPath: string, kind: "file" | "directory", opts?: WindowsAclCheckOptions): void;
|
|
121
|
+
/** Test hook: forget which paths already emitted the reduced-guarantee line. */
|
|
122
|
+
export declare function resetWindowsAclDiagnostics(): void;
|
|
123
|
+
//# sourceMappingURL=windows-acl.d.ts.map
|