@hasna/hooks 0.3.9 → 0.3.10
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/hooks/codewith-native-common.ts +30 -29
- package/package.json +1 -1
|
@@ -367,6 +367,12 @@ interface ProtectedPathRule {
|
|
|
367
367
|
mode: "tree" | "root";
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
interface ProtectedPathContext {
|
|
371
|
+
rules: ProtectedPathRule[];
|
|
372
|
+
workspaceRoots: string[];
|
|
373
|
+
currentManagedRepoRoot: string | null;
|
|
374
|
+
}
|
|
375
|
+
|
|
370
376
|
function splitPathList(value: unknown): string[] {
|
|
371
377
|
if (typeof value === "string") return value.split(":").map((v) => v.trim()).filter(Boolean);
|
|
372
378
|
if (!Array.isArray(value)) return [];
|
|
@@ -418,13 +424,14 @@ function hasnaDivisionRuleFor(target: string, workspaceRoot: string): ProtectedP
|
|
|
418
424
|
return null;
|
|
419
425
|
}
|
|
420
426
|
|
|
421
|
-
async function
|
|
427
|
+
async function protectedPathContextFor(input: CodewithHookInput, cwd: string): Promise<ProtectedPathContext> {
|
|
422
428
|
const home = process.env.HOME || homedir();
|
|
423
429
|
const rules: ProtectedPathRule[] = [
|
|
424
430
|
{ root: join(home, ".hasna"), label: "Hasna state root ~/.hasna", mode: "tree" },
|
|
425
431
|
];
|
|
432
|
+
const workspaceRoots = workspaceRootsFor(input, cwd);
|
|
426
433
|
|
|
427
|
-
for (const root of
|
|
434
|
+
for (const root of workspaceRoots) {
|
|
428
435
|
rules.push({ root, label: "workspace root", mode: "root" });
|
|
429
436
|
}
|
|
430
437
|
|
|
@@ -435,7 +442,17 @@ async function protectedRulesFor(input: CodewithHookInput, cwd: string): Promise
|
|
|
435
442
|
rules.push({ root, label: "active repository or worktree root", mode: "root" });
|
|
436
443
|
}
|
|
437
444
|
|
|
438
|
-
|
|
445
|
+
const worktreesRoot = resolve(defaultWorktreesRoot());
|
|
446
|
+
const isCurrentManagedRepo = repoRoot !== null
|
|
447
|
+
&& isInsidePath(cwd, worktreesRoot)
|
|
448
|
+
&& isInsidePath(repoRoot, worktreesRoot);
|
|
449
|
+
const currentManagedRepoRoot = isCurrentManagedRepo ? resolve(repoRoot) : null;
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
rules: [...new Map(rules.map((rule) => [resolve(rule.root), { ...rule, root: resolve(rule.root) }])).values()],
|
|
453
|
+
workspaceRoots,
|
|
454
|
+
currentManagedRepoRoot,
|
|
455
|
+
};
|
|
439
456
|
}
|
|
440
457
|
|
|
441
458
|
function threatensProtectedPath(targetPath: string, rule: ProtectedPathRule): boolean {
|
|
@@ -461,37 +478,22 @@ function broadContentWipeBase(targetPath: string): string | null {
|
|
|
461
478
|
return dirname(target);
|
|
462
479
|
}
|
|
463
480
|
|
|
464
|
-
function
|
|
465
|
-
const root = defaultWorktreesRoot();
|
|
466
|
-
if (!isInsidePath(path, root)) return null;
|
|
467
|
-
const rel = relative(resolve(root), resolve(path));
|
|
468
|
-
const parts = rel.split(sep).filter(Boolean);
|
|
469
|
-
if (parts.length < 3) return null;
|
|
470
|
-
const [machine, repoSlugHash, lease] = parts;
|
|
471
|
-
if (!machine || !repoSlugHash || !lease) return null;
|
|
472
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,80}$/.test(machine)) return null;
|
|
473
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*-[0-9a-fA-F]{7,16}$/.test(repoSlugHash)) return null;
|
|
474
|
-
if (!/^wt_[0-9a-fA-F]{16,64}$/.test(lease)) return null;
|
|
475
|
-
return resolve(root, machine, repoSlugHash, lease);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function shouldSkipHasnaTreeRule(targetPath: string, rule: ProtectedPathRule): boolean {
|
|
481
|
+
function shouldSkipHasnaTreeRule(targetPath: string, rule: ProtectedPathRule, currentManagedRepoRoot: string | null): boolean {
|
|
479
482
|
if (rule.label !== "Hasna state root ~/.hasna") return false;
|
|
480
|
-
|
|
481
|
-
if (!leaseRoot) return false;
|
|
483
|
+
if (!currentManagedRepoRoot) return false;
|
|
482
484
|
const target = resolve(targetPath);
|
|
483
|
-
return
|
|
485
|
+
return isInsidePath(target, currentManagedRepoRoot);
|
|
484
486
|
}
|
|
485
487
|
|
|
486
|
-
function threatensRule(targetPath: string, rule: ProtectedPathRule): boolean {
|
|
487
|
-
if (shouldSkipHasnaTreeRule(targetPath, rule)) return false;
|
|
488
|
+
function threatensRule(targetPath: string, rule: ProtectedPathRule, currentManagedRepoRoot: string | null): boolean {
|
|
489
|
+
if (shouldSkipHasnaTreeRule(targetPath, rule, currentManagedRepoRoot)) return false;
|
|
488
490
|
const contentBase = broadContentWipeBase(targetPath);
|
|
489
491
|
if (contentBase && mutatesProtectedPath(contentBase, rule)) return true;
|
|
490
492
|
return threatensProtectedPath(targetPath, rule);
|
|
491
493
|
}
|
|
492
494
|
|
|
493
|
-
function mutatesRule(targetPath: string, rule: ProtectedPathRule): boolean {
|
|
494
|
-
if (shouldSkipHasnaTreeRule(targetPath, rule)) return false;
|
|
495
|
+
function mutatesRule(targetPath: string, rule: ProtectedPathRule, currentManagedRepoRoot: string | null): boolean {
|
|
496
|
+
if (shouldSkipHasnaTreeRule(targetPath, rule, currentManagedRepoRoot)) return false;
|
|
495
497
|
return mutatesProtectedPath(targetPath, rule);
|
|
496
498
|
}
|
|
497
499
|
|
|
@@ -825,8 +827,7 @@ function scopedBlockReason(operation: string, targetPath: string, rule: Protecte
|
|
|
825
827
|
export async function classifyDangerousOperation(input: CodewithHookInput): Promise<DangerousOperationMatch> {
|
|
826
828
|
if (input.hook_event_name !== "PreToolUse") return { block: false };
|
|
827
829
|
const cwd = input.cwd || process.cwd();
|
|
828
|
-
const rules = await
|
|
829
|
-
const workspaceRoots = workspaceRootsFor(input, cwd);
|
|
830
|
+
const { rules, workspaceRoots, currentManagedRepoRoot } = await protectedPathContextFor(input, cwd);
|
|
830
831
|
|
|
831
832
|
if (input.tool_name === "Bash") {
|
|
832
833
|
for (const target of destructiveShellTargets(getCommand(input), cwd)) {
|
|
@@ -834,7 +835,7 @@ export async function classifyDangerousOperation(input: CodewithHookInput): Prom
|
|
|
834
835
|
const extraRule = workspaceRoots.map((root) => hasnaDivisionRuleFor(targetPath, root)).find((rule): rule is ProtectedPathRule => Boolean(rule));
|
|
835
836
|
const allRules = extraRule ? [...rules, extraRule] : rules;
|
|
836
837
|
for (const rule of allRules) {
|
|
837
|
-
if (threatensRule(targetPath, rule)) {
|
|
838
|
+
if (threatensRule(targetPath, rule, currentManagedRepoRoot)) {
|
|
838
839
|
return {
|
|
839
840
|
block: true,
|
|
840
841
|
targetPath,
|
|
@@ -853,7 +854,7 @@ export async function classifyDangerousOperation(input: CodewithHookInput): Prom
|
|
|
853
854
|
const extraRule = workspaceRoots.map((root) => hasnaDivisionRuleFor(targetPath, root)).find((rule): rule is ProtectedPathRule => Boolean(rule));
|
|
854
855
|
const allRules = extraRule ? [...rules, extraRule] : rules;
|
|
855
856
|
for (const rule of allRules) {
|
|
856
|
-
if (mutatesRule(targetPath, rule)) {
|
|
857
|
+
if (mutatesRule(targetPath, rule, currentManagedRepoRoot)) {
|
|
857
858
|
return {
|
|
858
859
|
block: true,
|
|
859
860
|
targetPath,
|
package/package.json
CHANGED