@hasna/hooks 0.10.2 → 0.10.4

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/dist/index.js CHANGED
@@ -581,7 +581,7 @@ var init_redact = __esm(() => {
581
581
  ];
582
582
  });
583
583
 
584
- // ../hooks0102-standalone-lock/node_modules/zod/v3/helpers/util.js
584
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
585
585
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
586
586
  const t = typeof data;
587
587
  switch (t) {
@@ -712,7 +712,7 @@ var init_util = __esm(() => {
712
712
  ]);
713
713
  });
714
714
 
715
- // ../hooks0102-standalone-lock/node_modules/zod/v3/ZodError.js
715
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
716
716
  var ZodIssueCode, quotelessJson = (obj) => {
717
717
  const json = JSON.stringify(obj, null, 2);
718
718
  return json.replace(/"([^"]+)":/g, "$1:");
@@ -833,7 +833,7 @@ var init_ZodError = __esm(() => {
833
833
  };
834
834
  });
835
835
 
836
- // ../hooks0102-standalone-lock/node_modules/zod/v3/locales/en.js
836
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
837
837
  var errorMap = (issue, _ctx) => {
838
838
  let message;
839
839
  switch (issue.code) {
@@ -940,7 +940,7 @@ var init_en = __esm(() => {
940
940
  en_default = errorMap;
941
941
  });
942
942
 
943
- // ../hooks0102-standalone-lock/node_modules/zod/v3/errors.js
943
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
944
944
  function setErrorMap(map) {
945
945
  overrideErrorMap = map;
946
946
  }
@@ -953,7 +953,7 @@ var init_errors = __esm(() => {
953
953
  overrideErrorMap = en_default;
954
954
  });
955
955
 
956
- // ../hooks0102-standalone-lock/node_modules/zod/v3/helpers/parseUtil.js
956
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
957
957
  function addIssueToContext(ctx, issueData) {
958
958
  const overrideMap = getErrorMap();
959
959
  const issue = makeIssue({
@@ -1058,10 +1058,10 @@ var init_parseUtil = __esm(() => {
1058
1058
  });
1059
1059
  });
1060
1060
 
1061
- // ../hooks0102-standalone-lock/node_modules/zod/v3/helpers/typeAliases.js
1061
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/typeAliases.js
1062
1062
  var init_typeAliases = () => {};
1063
1063
 
1064
- // ../hooks0102-standalone-lock/node_modules/zod/v3/helpers/errorUtil.js
1064
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
1065
1065
  var errorUtil;
1066
1066
  var init_errorUtil = __esm(() => {
1067
1067
  (function(errorUtil2) {
@@ -1070,7 +1070,7 @@ var init_errorUtil = __esm(() => {
1070
1070
  })(errorUtil || (errorUtil = {}));
1071
1071
  });
1072
1072
 
1073
- // ../hooks0102-standalone-lock/node_modules/zod/v3/types.js
1073
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
1074
1074
  class ParseInputLazyPath {
1075
1075
  constructor(parent, value, path, key) {
1076
1076
  this._cachedPath = [];
@@ -4421,7 +4421,7 @@ var init_types = __esm(() => {
4421
4421
  NEVER = INVALID;
4422
4422
  });
4423
4423
 
4424
- // ../hooks0102-standalone-lock/node_modules/zod/v3/external.js
4424
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
4425
4425
  var exports_external = {};
4426
4426
  __export(exports_external, {
4427
4427
  void: () => voidType,
@@ -4541,7 +4541,7 @@ var init_external = __esm(() => {
4541
4541
  init_ZodError();
4542
4542
  });
4543
4543
 
4544
- // ../hooks0102-standalone-lock/node_modules/zod/index.js
4544
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/index.js
4545
4545
  var init_zod = __esm(() => {
4546
4546
  init_external();
4547
4547
  init_external();
@@ -5101,7 +5101,7 @@ var init_profiles = __esm(() => {
5101
5101
  init_app_home();
5102
5102
  });
5103
5103
 
5104
- // ../hooks0102-standalone-lock/node_modules/@hasna/contracts/dist/client/transport.js
5104
+ // ../../node_modules/.bun/@hasna+contracts@1.2.2+944b86d8024e79fa/node_modules/@hasna/contracts/dist/client/transport.js
5105
5105
  import { isIP } from "net";
5106
5106
  import { spawnSync } from "child_process";
5107
5107
  import { closeSync as closeSync22, fstatSync as fstatSync22, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
@@ -7719,6 +7719,9 @@ function buildNativeSafetyCommand(binding) {
7719
7719
  return prefix + [encoded, binding.runtime, binding.home, binding.path].map(quote).join(" ");
7720
7720
  }
7721
7721
  function nativeSafetyRegistration(command) {
7722
+ return nativeSafetyCommandBinding(command)?.name;
7723
+ }
7724
+ function nativeSafetyCommandBinding(command) {
7722
7725
  if (!command.startsWith(prefix))
7723
7726
  return;
7724
7727
  const encoded = command.slice(prefix.length).match(/^'([A-Za-z0-9_-]+)' /)?.[1];
@@ -7726,7 +7729,7 @@ function nativeSafetyRegistration(command) {
7726
7729
  return;
7727
7730
  try {
7728
7731
  const binding = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
7729
- return buildNativeSafetyCommand(binding) === command ? binding.name : undefined;
7732
+ return buildNativeSafetyCommand(binding) === command ? binding : undefined;
7730
7733
  } catch {
7731
7734
  return;
7732
7735
  }
@@ -7920,7 +7923,7 @@ function writeCodexSettings(path, settings) {
7920
7923
  // src/lib/claude-skills-coordination.ts
7921
7924
  import { createHash as createHash5 } from "crypto";
7922
7925
 
7923
- // ../hooks0102-standalone-lock/node_modules/@hasna/skills/dist/index.js
7926
+ // ../../node_modules/.bun/@hasna+skills@0.9.14+a00d3064559565bb/node_modules/@hasna/skills/dist/index.js
7924
7927
  import { existsSync as existsSync9 } from "fs";
7925
7928
  import { homedir as homedir4 } from "os";
7926
7929
  import { join as join8, resolve as resolve3 } from "path";
@@ -11,5 +11,6 @@ export interface NativeSafetyBinding {
11
11
  export declare function isNativeSafetyName(name: string): name is NativeSafetyName;
12
12
  export declare function buildNativeSafetyCommand(binding: NativeSafetyBinding): string;
13
13
  export declare function nativeSafetyRegistration(command: string): NativeSafetyName | undefined;
14
+ export declare function nativeSafetyCommandBinding(command: string): NativeSafetyBinding | undefined;
14
15
  /** Built-in capability only: no registry, custom source, profile or app store. */
15
16
  export declare function installedNativeSafetyCommand(name: NativeSafetyName): string;
@@ -0,0 +1,58 @@
1
+ export declare class NativeSafetyError extends Error {
2
+ readonly code: "binding_missing" | "binding_invalid" | "binding_changed" | "binding_busy" | "guard_failed" | "input_invalid";
3
+ readonly reason?: string | undefined;
4
+ constructor(code: "binding_missing" | "binding_invalid" | "binding_changed" | "binding_busy" | "guard_failed" | "input_invalid", reason?: string | undefined);
5
+ }
6
+ export type NativeSafetyEvent = {
7
+ hook_event_name: "PreToolUse";
8
+ tool_name: "Bash" | "apply_patch";
9
+ cwd: string;
10
+ session_id?: string;
11
+ tool_input: Record<string, unknown>;
12
+ };
13
+ export type NativeSafetyDecision = {
14
+ decision: "continue";
15
+ } | {
16
+ decision: "deny";
17
+ reason: string;
18
+ } | {
19
+ decision: "rewrite";
20
+ command: string;
21
+ };
22
+ export type NativeSafetyRecord = {
23
+ schema: "hasna.hooks.native-safety.v1";
24
+ harness: "sumi";
25
+ command: string;
26
+ };
27
+ export declare function readNativeSafetyRegistration(home?: string): {
28
+ record: NativeSafetyRecord;
29
+ sha256: string;
30
+ path: string;
31
+ };
32
+ /** Register only the bundled composite guard. Updates preserve the exact prior
33
+ * record and require its digest. A lock coordinates package-owned writers; an
34
+ * unexpected preimage or a retained interrupted lock requires reconciliation. */
35
+ export declare function registerNativeSafety(options?: {
36
+ home?: string;
37
+ expectedSHA256?: string;
38
+ command?: string;
39
+ }): {
40
+ ok: true;
41
+ changed: boolean;
42
+ path: string;
43
+ sha256: string;
44
+ };
45
+ /** A bounded supervisor subprocess evaluates data only; it never runs the
46
+ * requested shell command. AbortSignal targets this newly created child. */
47
+ export declare function evaluateNativeSafety(event: NativeSafetyEvent, options?: {
48
+ home?: string;
49
+ signal?: AbortSignal;
50
+ }): Promise<NativeSafetyDecision>;
51
+ /** Classify a synthetic removal through this exact supervisor without executing
52
+ * the returned command or creating a file. A harmless ':' would not test whether
53
+ * the saved PATH can actually resolve Trash. */
54
+ export declare function verifyNativeSafetyCommand(command: string, home?: string): Promise<void>;
55
+ export declare function verifyNativeSafetyRegistration(home?: string): Promise<{
56
+ path: string;
57
+ sha256: string;
58
+ }>;
@@ -0,0 +1,363 @@
1
+ // @bun
2
+ // src/native-safety.ts
3
+ import { execFile } from "child_process";
4
+ import { createHash as createHash2, randomUUID } from "crypto";
5
+ import { closeSync as closeSync2, constants as constants2, existsSync as existsSync2, fstatSync as fstatSync2, fsyncSync, linkSync, lstatSync as lstatSync2, mkdirSync, openSync as openSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, renameSync, unlinkSync, writeFileSync } from "fs";
6
+ import { homedir as homedir2 } from "os";
7
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve } from "path";
8
+
9
+ // src/lib/native-safety.ts
10
+ import { createHash } from "crypto";
11
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readFileSync, realpathSync } from "fs";
12
+ import { dirname, isAbsolute, join } from "path";
13
+ import { homedir } from "os";
14
+ function isNativeSafetyName(name) {
15
+ return name === "trash-guard" || name === "workspace-repos-guard";
16
+ }
17
+ var supervisorSource = String.raw`async function supervise(encoded) {
18
+ const fs = require("node:fs");
19
+ const path = require("node:path");
20
+ const crypto = require("node:crypto");
21
+ const cp = require("node:child_process");
22
+ const fail = () => { process.stderr.write("Native safety guard refused: verification or execution failed.\n"); process.exit(2); };
23
+ try {
24
+ const b = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
25
+ const check = (file, hash, maximum) => {
26
+ if (!path.isAbsolute(file) || fs.realpathSync(file) !== file) throw Error();
27
+ for (let parent = path.dirname(file);;) {
28
+ const s = fs.lstatSync(parent);
29
+ if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022)) throw Error();
30
+ if (parent === path.dirname(parent)) break;
31
+ parent = path.dirname(parent);
32
+ }
33
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
34
+ try {
35
+ const s = fs.fstatSync(fd);
36
+ if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022) || s.size > maximum) throw Error();
37
+ const bytes = fs.readFileSync(fd);
38
+ if (crypto.createHash("sha256").update(bytes).digest("hex") !== hash) throw Error();
39
+ return bytes;
40
+ } finally { fs.closeSync(fd); }
41
+ };
42
+ check(b.runtime, b.runtimeSHA256, 200_000_000);
43
+ const bytes = check(b.worker, b.workerSHA256, 256_000);
44
+ const deadline = setTimeout(fail, 1500);
45
+ const chunks = []; let size = 0;
46
+ for await (const chunk of process.stdin) {
47
+ size += chunk.length;
48
+ if (size > 1_048_576) throw Error();
49
+ chunks.push(chunk);
50
+ }
51
+ clearTimeout(deadline);
52
+ const input = Buffer.concat(chunks);
53
+ const child = cp.spawnSync(b.runtime, ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", "--eval", bytes.toString("utf8"), b.name], {
54
+ input, env: { HOME: b.home, PATH: b.path }, cwd: "/", timeout: 2500, killSignal: "SIGKILL", maxBuffer: 1_048_576,
55
+ });
56
+ if (child.error || child.status !== 0 || child.signal || child.stderr.length) throw Error();
57
+ const result = JSON.parse(child.stdout.toString("utf8"));
58
+ if (!result || Array.isArray(result) || Object.keys(result).length !== 1) throw Error();
59
+ if (result.verdict === null) return;
60
+ const output = result.verdict?.hookSpecificOutput;
61
+ if (!output || Object.keys(result.verdict).length !== 1 || output.hookEventName !== "PreToolUse") throw Error();
62
+ if (output.permissionDecision === "deny") {
63
+ if (Object.keys(output).sort().join() !== "hookEventName,permissionDecision,permissionDecisionReason" || typeof output.permissionDecisionReason !== "string" || !output.permissionDecisionReason.trim()) throw Error();
64
+ } else if (output.permissionDecision === "allow") {
65
+ if (b.name !== "trash-guard" || Object.keys(output).sort().join() !== "hookEventName,permissionDecision,updatedInput" || !output.updatedInput || typeof output.updatedInput.command !== "string" || !output.updatedInput.command.trim()) throw Error();
66
+ } else throw Error();
67
+ process.stdout.write(JSON.stringify(result.verdict));
68
+ } catch { fail(); }
69
+ }`;
70
+ var quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
71
+ var program = `(${supervisorSource})(process.argv[1]);`;
72
+ var shell = `cd / || exit 2
73
+ exec 3<&0 || exit 2
74
+ /usr/bin/env -i HOME="$3" PATH="$4" "$2" --config=/dev/null --no-env-file --no-macros --no-install --eval ${quote(program)} "$1" <&3 3<&- 2>/dev/null &
75
+ runner=$!
76
+ exec 3<&-
77
+ (trap 'kill "$sleeper" 2>/dev/null; exit 0' TERM; /bin/sleep 6 & sleeper=$!; wait "$sleeper"; kill -KILL "$runner" 2>/dev/null) &
78
+ watchdog=$!
79
+ wait "$runner"; result=$?
80
+ kill "$watchdog" 2>/dev/null; wait "$watchdog" 2>/dev/null
81
+ if [ "$result" -ne 0 ]; then printf '%s\\n' 'Native safety guard refused: verification or execution failed.' >&2; exit 2; fi`;
82
+ var prefix = `/usr/bin/env -i /bin/sh -c ${quote(shell)} hasna-native-safety-v1 `;
83
+ function validateBinding(b) {
84
+ if (!b || !isNativeSafetyName(b.name) || ![b.runtime, b.worker, b.home].every((p) => typeof p === "string" && isAbsolute(p) && !/[\0\r\n]/.test(p)) || ![b.runtimeSHA256, b.workerSHA256].every((h) => /^[0-9a-f]{64}$/.test(h)) || typeof b.path !== "string" || b.path.split(":").some((p) => !isAbsolute(p) || /[\0\r\n]/.test(p)))
85
+ throw new Error("Invalid native safety binding");
86
+ }
87
+ function buildNativeSafetyCommand(binding) {
88
+ validateBinding(binding);
89
+ const encoded = Buffer.from(JSON.stringify(binding)).toString("base64url");
90
+ return prefix + [encoded, binding.runtime, binding.home, binding.path].map(quote).join(" ");
91
+ }
92
+ function nativeSafetyCommandBinding(command) {
93
+ if (!command.startsWith(prefix))
94
+ return;
95
+ const encoded = command.slice(prefix.length).match(/^'([A-Za-z0-9_-]+)' /)?.[1];
96
+ if (!encoded || encoded.length > 16384)
97
+ return;
98
+ try {
99
+ const binding = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
100
+ return buildNativeSafetyCommand(binding) === command ? binding : undefined;
101
+ } catch {
102
+ return;
103
+ }
104
+ }
105
+ function pinEligibleFile(file, maximum, executable = false) {
106
+ if (!isAbsolute(file) || realpathSync(file) !== file)
107
+ throw new Error("Native safety requires canonical files");
108
+ for (let parent = dirname(file);; ) {
109
+ const s = lstatSync(parent);
110
+ if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || s.mode & 18)
111
+ throw new Error("Native safety requires trusted, non-writable file ancestry");
112
+ if (parent === dirname(parent))
113
+ break;
114
+ parent = dirname(parent);
115
+ }
116
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
117
+ try {
118
+ const s = fstatSync(fd);
119
+ if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || s.mode & 18 || s.size > maximum || executable && !(s.mode & 64))
120
+ throw new Error("Native safety requires an eligible installed file");
121
+ const bytes = readFileSync(fd);
122
+ if (bytes.length > maximum)
123
+ throw new Error("Native safety file exceeds its bound");
124
+ return createHash("sha256").update(bytes).digest("hex");
125
+ } finally {
126
+ closeSync(fd);
127
+ }
128
+ }
129
+ function installedNativeSafetyCommand(name) {
130
+ const home = realpathSync(homedir());
131
+ const runtime = realpathSync(process.execPath);
132
+ const candidates = [join(import.meta.dir, "../../bin/native-safety-entry.js"), join(import.meta.dir, "../bin/native-safety-entry.js"), join(import.meta.dir, "native-safety-entry.js")];
133
+ const worker = candidates.find(existsSync);
134
+ if (!worker)
135
+ throw new Error("Bundled native safety runner is missing; build or reinstall @hasna/hooks before installing its guards.");
136
+ const canonicalWorker = realpathSync(worker);
137
+ return buildNativeSafetyCommand({ name, runtime, runtimeSHA256: pinEligibleFile(runtime, 200000000, true), worker: canonicalWorker, workerSHA256: pinEligibleFile(canonicalWorker, 256000), home, path: `${dirname(runtime)}:${join(home, ".bun/bin")}:/usr/local/bin:/usr/bin:/bin` });
138
+ }
139
+
140
+ // src/native-safety.ts
141
+ class NativeSafetyError extends Error {
142
+ code;
143
+ reason;
144
+ constructor(code, reason) {
145
+ super(`Native deletion safety refused (${code}). Run the reviewed Hooks safety setup before using shell or patch tools.`);
146
+ this.code = code;
147
+ this.reason = reason;
148
+ this.name = "NativeSafetyError";
149
+ }
150
+ }
151
+ var hash = (value) => createHash2("sha256").update(value).digest("hex");
152
+ var filename = "sumi-trash-guard.json";
153
+ var owner = (uid) => uid === 0 || uid === process.getuid?.();
154
+ function safeDirectory(path) {
155
+ for (let cursor = path;; cursor = dirname2(cursor)) {
156
+ const stat = lstatSync2(cursor);
157
+ if (!stat.isDirectory() || !owner(stat.uid) || stat.mode & 18)
158
+ throw new NativeSafetyError("binding_invalid");
159
+ if (dirname2(cursor) === cursor)
160
+ return;
161
+ }
162
+ }
163
+ function canonicalHome(home) {
164
+ if (!isAbsolute2(home) || home.includes("\x00") || realpathSync2(home) !== resolve(home))
165
+ throw new NativeSafetyError("binding_invalid");
166
+ safeDirectory(home);
167
+ return resolve(home);
168
+ }
169
+ function readOwned(path) {
170
+ safeDirectory(dirname2(path));
171
+ const fd = openSync2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
172
+ try {
173
+ const stat = fstatSync2(fd);
174
+ if (!stat.isFile() || !owner(stat.uid) || stat.mode & 63 || stat.size > 32768)
175
+ throw new NativeSafetyError("binding_invalid");
176
+ const value = readFileSync2(fd, "utf8");
177
+ if (Buffer.byteLength(value) > 32768)
178
+ throw new NativeSafetyError("binding_invalid");
179
+ return value;
180
+ } finally {
181
+ closeSync2(fd);
182
+ }
183
+ }
184
+ function parseRecord(text, home) {
185
+ const record = JSON.parse(text);
186
+ if (!record || Array.isArray(record) || Object.keys(record).sort().join() !== "command,harness,schema" || record.schema !== "hasna.hooks.native-safety.v1" || record.harness !== "sumi" || typeof record.command !== "string")
187
+ throw new NativeSafetyError("binding_invalid");
188
+ const binding = nativeSafetyCommandBinding(record.command);
189
+ if (binding?.name !== "trash-guard" || binding.home !== home)
190
+ throw new NativeSafetyError("binding_invalid");
191
+ return record;
192
+ }
193
+ function syncDirectory(path) {
194
+ const fd = openSync2(path, constants2.O_RDONLY | constants2.O_DIRECTORY | constants2.O_NOFOLLOW);
195
+ try {
196
+ fsyncSync(fd);
197
+ } finally {
198
+ closeSync2(fd);
199
+ }
200
+ }
201
+ function readNativeSafetyRegistration(home = homedir2()) {
202
+ try {
203
+ home = canonicalHome(home);
204
+ const path = join2(home, ".hasna/hooks/native", filename), text = readOwned(path);
205
+ return { record: parseRecord(text, home), sha256: hash(text), path };
206
+ } catch (error) {
207
+ if (error instanceof NativeSafetyError)
208
+ throw error;
209
+ throw new NativeSafetyError(error?.code === "ENOENT" ? "binding_missing" : "binding_invalid");
210
+ }
211
+ }
212
+ function registerNativeSafety(options = {}) {
213
+ const home = canonicalHome(options.home ?? homedir2());
214
+ const record = { schema: "hasna.hooks.native-safety.v1", harness: "sumi", command: options.command ?? installedNativeSafetyCommand("trash-guard") };
215
+ const text = JSON.stringify(record) + `
216
+ `;
217
+ parseRecord(text, home);
218
+ for (const dir of [join2(home, ".hasna"), join2(home, ".hasna/hooks"), join2(home, ".hasna/hooks/native")]) {
219
+ try {
220
+ mkdirSync(dir, { mode: 448 });
221
+ syncDirectory(dirname2(dir));
222
+ } catch (error) {
223
+ if (error.code !== "EEXIST")
224
+ throw error;
225
+ }
226
+ safeDirectory(dir);
227
+ }
228
+ const directory = join2(home, ".hasna/hooks/native"), path = join2(directory, filename), lock = path + ".lock";
229
+ let lockFD;
230
+ try {
231
+ lockFD = openSync2(lock, constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY, 384);
232
+ } catch {
233
+ throw new NativeSafetyError("binding_busy");
234
+ }
235
+ const lockIdentity = fstatSync2(lockFD);
236
+ let temporaryIdentity;
237
+ const temporary = join2(directory, `.${filename}.${randomUUID()}`);
238
+ try {
239
+ writeFileSync(lockFD, JSON.stringify({ pid: process.pid, expectedSHA256: options.expectedSHA256 ?? "absent" }));
240
+ fsyncSync(lockFD);
241
+ syncDirectory(directory);
242
+ let before;
243
+ try {
244
+ before = readNativeSafetyRegistration(home);
245
+ } catch (error) {
246
+ if (!(error instanceof NativeSafetyError) || error.code !== "binding_missing")
247
+ throw error;
248
+ }
249
+ if (before?.sha256 === hash(text))
250
+ return { ok: true, changed: false, path, sha256: before.sha256 };
251
+ if (before ? options.expectedSHA256 !== before.sha256 : options.expectedSHA256 !== undefined && options.expectedSHA256 !== "absent")
252
+ throw new NativeSafetyError("binding_changed");
253
+ if (before) {
254
+ const backup = path + ".before-" + before.sha256;
255
+ if (existsSync2(backup)) {
256
+ if (hash(readOwned(backup)) !== before.sha256)
257
+ throw new NativeSafetyError("binding_changed");
258
+ } else {
259
+ const original = readOwned(path);
260
+ if (hash(original) !== before.sha256)
261
+ throw new NativeSafetyError("binding_changed");
262
+ const fd2 = openSync2(backup, constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY, 384);
263
+ try {
264
+ writeFileSync(fd2, original);
265
+ fsyncSync(fd2);
266
+ } finally {
267
+ closeSync2(fd2);
268
+ }
269
+ syncDirectory(directory);
270
+ }
271
+ }
272
+ const fd = openSync2(temporary, constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY, 384);
273
+ try {
274
+ temporaryIdentity = fstatSync2(fd);
275
+ writeFileSync(fd, text);
276
+ fsyncSync(fd);
277
+ } finally {
278
+ closeSync2(fd);
279
+ }
280
+ if (before) {
281
+ if (hash(readOwned(path)) !== before.sha256)
282
+ throw new NativeSafetyError("binding_changed");
283
+ renameSync(temporary, path);
284
+ } else {
285
+ linkSync(temporary, path);
286
+ unlinkSync(temporary);
287
+ }
288
+ syncDirectory(directory);
289
+ const after = readNativeSafetyRegistration(home);
290
+ if (after.sha256 !== hash(text))
291
+ throw new NativeSafetyError("binding_changed");
292
+ return { ok: true, changed: true, path, sha256: after.sha256 };
293
+ } finally {
294
+ closeSync2(lockFD);
295
+ const currentTemporary = lstatSync2(temporary, { throwIfNoEntry: false });
296
+ if (temporaryIdentity && currentTemporary?.dev === temporaryIdentity.dev && currentTemporary.ino === temporaryIdentity.ino)
297
+ unlinkSync(temporary);
298
+ const currentLock = lstatSync2(lock, { throwIfNoEntry: false });
299
+ if (currentLock?.dev !== lockIdentity.dev || currentLock.ino !== lockIdentity.ino)
300
+ throw new NativeSafetyError("binding_changed");
301
+ unlinkSync(lock);
302
+ syncDirectory(directory);
303
+ }
304
+ }
305
+ async function evaluateNativeSafety(event, options = {}) {
306
+ return evaluateRecord(event, readNativeSafetyRegistration(options.home).record, options.signal);
307
+ }
308
+ async function evaluateRecord(event, record, signal) {
309
+ if (!event || event.hook_event_name !== "PreToolUse" || !["Bash", "apply_patch"].includes(event.tool_name) || typeof event.cwd !== "string" || !isAbsolute2(event.cwd) || !event.tool_input || typeof event.tool_input !== "object" || Array.isArray(event.tool_input))
310
+ throw new NativeSafetyError("input_invalid");
311
+ const input = JSON.stringify(event);
312
+ if (Buffer.byteLength(input) > 1048576)
313
+ throw new NativeSafetyError("input_invalid");
314
+ const output = await new Promise((accept, reject) => {
315
+ const child = execFile("/bin/sh", ["-c", record.command], { cwd: "/", env: {}, encoding: "utf8", timeout: 8000, maxBuffer: 1048576, signal }, (error, stdout, stderr) => {
316
+ if (error || stderr) {
317
+ reject(new NativeSafetyError("guard_failed"));
318
+ return;
319
+ }
320
+ accept(stdout);
321
+ });
322
+ child.stdin?.on("error", () => {});
323
+ child.stdin?.end(input);
324
+ });
325
+ if (!output)
326
+ return { decision: "continue" };
327
+ try {
328
+ const value = JSON.parse(output);
329
+ if (Object.keys(value).join() !== "hookSpecificOutput")
330
+ throw new Error;
331
+ const verdict = value.hookSpecificOutput;
332
+ if (verdict.hookEventName !== "PreToolUse")
333
+ throw new Error;
334
+ if (verdict.permissionDecision === "deny" && Object.keys(verdict).sort().join() === "hookEventName,permissionDecision,permissionDecisionReason" && typeof verdict.permissionDecisionReason === "string" && verdict.permissionDecisionReason.trim())
335
+ return { decision: "deny", reason: verdict.permissionDecisionReason };
336
+ if (event.tool_name === "Bash" && verdict.permissionDecision === "allow" && Object.keys(verdict).sort().join() === "hookEventName,permissionDecision,updatedInput" && typeof verdict.updatedInput?.command === "string" && verdict.updatedInput.command.trim())
337
+ return { decision: "rewrite", command: verdict.updatedInput.command };
338
+ } catch {}
339
+ throw new NativeSafetyError("guard_failed");
340
+ }
341
+ async function verifyNativeSafetyCommand(command, home = homedir2()) {
342
+ home = canonicalHome(home);
343
+ const record = parseRecord(JSON.stringify({ schema: "hasna.hooks.native-safety.v1", harness: "sumi", command }), home);
344
+ const verdict = await evaluateRecord({ hook_event_name: "PreToolUse", tool_name: "Bash", cwd: home, tool_input: { command: "rm -- .hasna-trash-verification-only" } }, record);
345
+ if (verdict.decision !== "rewrite")
346
+ throw new NativeSafetyError("guard_failed", verdict.decision === "deny" ? verdict.reason.slice(0, 512) : undefined);
347
+ }
348
+ async function verifyNativeSafetyRegistration(home = homedir2()) {
349
+ const before = readNativeSafetyRegistration(home);
350
+ await verifyNativeSafetyCommand(before.record.command, home);
351
+ const after = readNativeSafetyRegistration(home);
352
+ if (after.sha256 !== before.sha256)
353
+ throw new NativeSafetyError("binding_changed");
354
+ return { path: before.path, sha256: before.sha256 };
355
+ }
356
+ export {
357
+ verifyNativeSafetyRegistration,
358
+ verifyNativeSafetyCommand,
359
+ registerNativeSafety,
360
+ readNativeSafetyRegistration,
361
+ evaluateNativeSafety,
362
+ NativeSafetyError
363
+ };
package/dist/sdk/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- // ../hooks0102-standalone-lock/node_modules/@hasna/contracts/dist/client/transport.js
2
+ // ../../node_modules/.bun/@hasna+contracts@1.2.2+944b86d8024e79fa/node_modules/@hasna/contracts/dist/client/transport.js
3
3
  import { isIP } from "net";
4
4
  import { spawnSync } from "child_process";
5
5
  import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync2 } from "fs";