@hasna/hooks 0.11.3 → 0.11.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/bin/index.js CHANGED
@@ -732,19 +732,350 @@ var init_registry = __esm(() => {
732
732
  ];
733
733
  });
734
734
 
735
+ // src/lib/readiness-process.ts
736
+ import { spawn } from "child_process";
737
+ function runReadinessProcess(command, args, options) {
738
+ if (!["darwin", "linux"].includes(process.platform)) {
739
+ return Promise.resolve({ status: null, error: { code: "ENOTSUP" }, stdout: "", stderr: "" });
740
+ }
741
+ if (options.signal?.aborted) {
742
+ return Promise.resolve({ status: null, error: { code: "ABORT_ERR" }, stdout: "", stderr: "" });
743
+ }
744
+ return new Promise((resolve2) => {
745
+ const child = spawn(command, ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", "--eval", supervisor, "--", JSON.stringify({ command, args, maxBuffer: options.maxBuffer, timeout: options.timeout })], {
746
+ cwd: options.cwd,
747
+ env: options.env,
748
+ detached: true,
749
+ stdio: ["pipe", "pipe", "pipe"]
750
+ });
751
+ let status = null, error;
752
+ let stopping = false, exited = false, finished = false;
753
+ let stdout = "", stderr = "", control = "";
754
+ const maximumEnvelope = options.maxBuffer * 12 + 1024;
755
+ let cleanup;
756
+ const finish = () => {
757
+ if (finished)
758
+ return;
759
+ finished = true;
760
+ clearTimeout(deadline);
761
+ clearTimeout(cleanup);
762
+ options.signal?.removeEventListener("abort", abort);
763
+ child.stdin?.destroy();
764
+ child.stdout?.destroy();
765
+ child.stderr?.destroy();
766
+ resolve2({ status, error, stdout, stderr });
767
+ };
768
+ const stop = (code) => {
769
+ if (code && !finished)
770
+ error ??= { code };
771
+ if (finished || stopping)
772
+ return;
773
+ stopping = true;
774
+ if (exited || !child.pid) {
775
+ error ??= { code: "ECHILD" };
776
+ } else {
777
+ try {
778
+ child.stdin.end();
779
+ } catch {
780
+ error = { code: "ECHILD" };
781
+ }
782
+ }
783
+ cleanup = setTimeout(() => {
784
+ error = { code: "ECHILD" };
785
+ finish();
786
+ }, 1000);
787
+ };
788
+ const abort = () => stop("ABORT_ERR");
789
+ const deadline = setTimeout(() => stop("ETIMEDOUT"), options.timeout);
790
+ options.signal?.addEventListener("abort", abort, { once: true });
791
+ if (options.signal?.aborted)
792
+ abort();
793
+ child.stdin?.on("error", () => {
794
+ if (!stopping)
795
+ stop("ECHILD");
796
+ });
797
+ child.stdout?.on("data", (bytes) => {
798
+ if (stopping)
799
+ return;
800
+ control += bytes.toString("utf8");
801
+ if (Buffer.byteLength(control) > maximumEnvelope)
802
+ return stop("ENOBUFS");
803
+ if (!control.endsWith(`
804
+ `))
805
+ return;
806
+ try {
807
+ const value = JSON.parse(control);
808
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["status", "error", "stdout", "stderr"].includes(key)) || !(value.status === null || Number.isInteger(value.status) && value.status >= 0 && value.status <= 255) || typeof value.stdout !== "string" || typeof value.stderr !== "string" || Buffer.byteLength(value.stdout) > options.maxBuffer || Buffer.byteLength(value.stderr) > options.maxBuffer || value.error !== undefined && (Object.keys(value.error).join() !== "code" || !["ENOBUFS", "ECHILD"].includes(value.error.code))) {
809
+ return stop("EPROTO");
810
+ }
811
+ status = value.status;
812
+ stdout = value.stdout;
813
+ stderr = value.stderr;
814
+ stop(value.error?.code);
815
+ } catch {
816
+ stop("EPROTO");
817
+ }
818
+ });
819
+ child.stdout?.on("error", () => stop("EPROTO"));
820
+ child.stderr?.on("data", () => stop("ECHILD"));
821
+ child.stderr?.on("error", () => stop("ECHILD"));
822
+ child.on("error", () => {
823
+ error = { code: "ECHILD" };
824
+ stop();
825
+ });
826
+ child.on("exit", () => {
827
+ exited = true;
828
+ if (!stopping)
829
+ stop("ECHILD");
830
+ });
831
+ child.on("close", () => {
832
+ if (!stopping)
833
+ error = { code: "ECHILD" };
834
+ finish();
835
+ });
836
+ });
837
+ }
838
+ var supervisor;
839
+ var init_readiness_process = __esm(() => {
840
+ supervisor = String.raw`
841
+ const {spawn}=require("node:child_process");
842
+ const spec=JSON.parse(process.argv.at(-1));
843
+ // This runtime was created detached and is its group's live leader. Its
844
+ // independent timer and parent pipe remain effective if the observer exits
845
+ // or its event loop stops responding. Only this freshly created tree is hit.
846
+ const cleanup=()=>{try{process.kill(-process.pid,"SIGKILL")}catch{process.exit(1)}};
847
+ setTimeout(cleanup,spec.timeout);
848
+ process.stdin.once("end",cleanup);
849
+ process.stdin.once("error",cleanup);
850
+ process.stdin.on("data",cleanup);
851
+ process.stdin.resume();
852
+ for(const signal of ["SIGTERM","SIGINT","SIGHUP"])process.once(signal,cleanup);
853
+ let done=false, out=Buffer.alloc(0), err=Buffer.alloc(0);
854
+ function report(status,error){
855
+ if(done)return; done=true;
856
+ process.stdout.write(JSON.stringify({status,error,stdout:out.toString("utf8"),stderr:err.toString("utf8")})+"\n");
857
+ }
858
+ const child=spawn(spec.command,spec.args,{stdio:["ignore","pipe","pipe"],env:process.env});
859
+ for(const [stream,name] of [[child.stdout,"stdout"],[child.stderr,"stderr"]]){
860
+ stream.on("data",bytes=>{
861
+ if(done)return;
862
+ const before=name==="stdout"?out:err;
863
+ if(before.length+bytes.length>spec.maxBuffer){report(null,{code:"ENOBUFS"});return}
864
+ if(name==="stdout")out=Buffer.concat([out,bytes]);else err=Buffer.concat([err,bytes]);
865
+ });
866
+ stream.on("error",()=>report(null,{code:"ECHILD"}));
867
+ }
868
+ child.on("error",()=>report(null,{code:"ECHILD"}));
869
+ // A successful observation includes complete streams. A nested client that
870
+ // keeps them open after its parent exits is a failure requiring group cleanup.
871
+ child.on("exit",()=>setTimeout(()=>report(null,{code:"ECHILD"}),100));
872
+ child.on("close",(status,signal)=>report(status,signal?{code:"ECHILD"}:undefined));
873
+ `;
874
+ });
875
+
876
+ // src/native-readiness.ts
877
+ var exports_native_readiness = {};
878
+ __export(exports_native_readiness, {
879
+ trashReadinessBindingPath: () => trashReadinessBindingPath,
880
+ runTrashReadiness: () => runTrashReadiness,
881
+ readinessSessionStartOutput: () => readinessSessionStartOutput,
882
+ readinessContext: () => readinessContext,
883
+ readTrashReadinessBindingFile: () => readTrashReadinessBindingFile,
884
+ parseTrashReadinessBinding: () => parseTrashReadinessBinding,
885
+ isReadinessProvider: () => isReadinessProvider,
886
+ handleReadinessSessionStart: () => handleReadinessSessionStart,
887
+ checkTrashReadinessBinding: () => checkTrashReadinessBinding,
888
+ TRASH_READINESS_NATIVE_TIMEOUT_SECONDS: () => TRASH_READINESS_NATIVE_TIMEOUT_SECONDS,
889
+ TRASH_READINESS_MAX_TIMEOUT_MS: () => TRASH_READINESS_MAX_TIMEOUT_MS,
890
+ TRASH_READINESS_DEFAULT_TIMEOUT_MS: () => TRASH_READINESS_DEFAULT_TIMEOUT_MS
891
+ });
892
+ import { createHash } from "crypto";
893
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync } from "fs";
894
+ import { homedir as homedir2 } from "os";
895
+ import { dirname, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
896
+ function need(value, code) {
897
+ if (!value)
898
+ throw new ReadinessError(code);
899
+ }
900
+ function isReadinessProvider(value) {
901
+ return value === "claude" || value === "codex" || value === "sumi";
902
+ }
903
+ function inside(home, path) {
904
+ const part = relative(home, path);
905
+ return part !== "" && part !== ".." && !part.startsWith("../") && !isAbsolute(part);
906
+ }
907
+ function parseTrashReadinessBinding(input, provider, home) {
908
+ const b = input;
909
+ need(isReadinessProvider(provider) && absolute(home), "binding_invalid");
910
+ need(keys(b, ["schema", "provider", "home", "runtime", "trash", "plan", "timeoutMs"]) && b.schema === "hasna.hooks.trash-readiness/v1" && b.provider === provider && b.home === home && pin(b.runtime) && pin(b.plan) && keys(b.trash, ["root", "version", "cliSha256"]) && absolute(b.trash.root) && typeof b.trash.version === "string" && b.trash.version.length <= 128 && /^\d+\.\d+\.\d+$/.test(b.trash.version) && digest(b.trash.cliSha256) && Number.isInteger(b.timeoutMs) && b.timeoutMs >= 1000 && b.timeoutMs <= TRASH_READINESS_MAX_TIMEOUT_MS && inside(home, b.plan.path) && inside(home, b.trash.root), "binding_invalid");
911
+ return structuredClone(b);
912
+ }
913
+ function trashReadinessBindingPath(provider, home = homedir2()) {
914
+ need(isReadinessProvider(provider) && absolute(home), "binding_invalid");
915
+ return join2(home, ".hasna", "hooks", "native", "trash-readiness", provider + ".json");
916
+ }
917
+ function readSafe(path, maximum, executable = false) {
918
+ need(absolute(path) && realpathSync(path) === path, "unsafe_path");
919
+ for (let parent = dirname(path);; parent = dirname(parent)) {
920
+ const stat = lstatSync(parent);
921
+ need(stat.isDirectory() && [0, process.getuid?.()].includes(stat.uid) && !(stat.mode & 18), "unsafe_path");
922
+ if (parent === dirname(parent))
923
+ break;
924
+ }
925
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
926
+ try {
927
+ const stat = fstatSync(fd);
928
+ need(stat.isFile() && [0, process.getuid?.()].includes(stat.uid) && !(stat.mode & 18) && stat.size <= maximum && (!executable || !!(stat.mode & 64)), "unsafe_path");
929
+ const bytes = Buffer.alloc(stat.size + 1);
930
+ let length = 0;
931
+ while (length < bytes.length) {
932
+ const count = readSync(fd, bytes, length, bytes.length - length, null);
933
+ if (!count)
934
+ break;
935
+ length += count;
936
+ }
937
+ const after = fstatSync(fd);
938
+ need(length === stat.size && after.size === stat.size && after.mtimeMs === stat.mtimeMs && after.ctimeMs === stat.ctimeMs, "file_changed");
939
+ return bytes.subarray(0, length);
940
+ } finally {
941
+ closeSync(fd);
942
+ }
943
+ }
944
+ function readTrashReadinessBindingFile(path, provider, home = realpathSync(homedir2())) {
945
+ const raw = readSafe(path, 65536);
946
+ return { binding: parseTrashReadinessBinding(JSON.parse(raw.toString("utf8")), provider, home), raw, sha256: sha(raw) };
947
+ }
948
+ async function runTrashReadiness(provider, d = {}) {
949
+ const started = performance.now();
950
+ const result = (code, extra = {}) => ({
951
+ schema: "hasna.hooks.trash-readiness-result/v1",
952
+ provider,
953
+ ready: code === "ready",
954
+ code,
955
+ nativeAdoptionVerified: false,
956
+ ...extra
957
+ });
958
+ let phase = "binding";
959
+ try {
960
+ const home = d.home ?? realpathSync(homedir2()), read = d.read ?? readSafe;
961
+ need(isReadinessProvider(provider), "binding_invalid");
962
+ const bindingPath = trashReadinessBindingPath(provider, home), bindingBytes = read(bindingPath, 65536);
963
+ const b = parseTrashReadinessBinding(JSON.parse(bindingBytes.toString("utf8")), provider, home);
964
+ phase = "consumer";
965
+ const checkPin = (p, maximum, executable = false) => {
966
+ const bytes = read(p.path, maximum, executable);
967
+ need(sha(bytes) === p.sha256, "file_changed");
968
+ return bytes;
969
+ };
970
+ checkPin(b.runtime, 200000000, true);
971
+ const manifestPath = join2(b.trash.root, "package.json"), manifestBytes = read(manifestPath, 65536);
972
+ const manifest = JSON.parse(manifestBytes.toString("utf8"));
973
+ need(manifest.name === "@hasna/trash" && manifest.version === b.trash.version && manifest.bin?.trash === "dist/cli/index.js", "package_mismatch");
974
+ const cli = join2(b.trash.root, "dist", "cli", "index.js");
975
+ checkPin({ path: cli, sha256: b.trash.cliSha256 }, 64 * 1024 * 1024, true);
976
+ const plan = JSON.parse(checkPin(b.plan, 65536).toString("utf8"));
977
+ need(plan?.schema === "hasna.trash.station-setup-plan/v1" && plan.stage === "ready" && plan.configurationReady === true && plan.nativeAdoptionVerified === false && digest(plan.planDigest) && plan.spec?.provider === provider && plan.spec.home === home && typeof plan.spec.station === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(plan.spec.station) && plan.spec.runtime?.path === b.runtime.path && plan.spec.runtime?.sha256 === b.runtime.sha256 && absolute(plan.spec.cwd) && (plan.spec.cwd === home || inside(home, plan.spec.cwd)) && typeof plan.spec.path === "string" && plan.spec.path.length <= 8192 && plan.spec.path.split(":").every(absolute), "plan_invalid");
978
+ const args = ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", cli, "station", "check", "--plan", b.plan.path];
979
+ const remaining = () => Math.floor(b.timeoutMs - (performance.now() - started));
980
+ const timeout = remaining();
981
+ need(timeout > 0, "deadline_exceeded");
982
+ const child = await (d.spawn ?? runReadinessProcess)(b.runtime.path, args, {
983
+ cwd: home,
984
+ env: { HOME: home, PATH: plan.spec.path, LANG: "C", NO_COLOR: "1" },
985
+ timeout,
986
+ maxBuffer: 16384,
987
+ signal: d.signal
988
+ });
989
+ need(remaining() > 0, "deadline_exceeded");
990
+ if (child.error && "code" in child.error && child.error.code === "ETIMEDOUT")
991
+ return result("deadline_exceeded");
992
+ if (child.error || child.signal || child.status === null)
993
+ return result("execution_failed");
994
+ need(typeof child.stdout === "string" && typeof child.stderr === "string" && Buffer.byteLength(child.stdout) <= 16384 && Buffer.byteLength(child.stderr) <= 16384, "output_invalid");
995
+ let value;
996
+ try {
997
+ value = JSON.parse(child.stdout);
998
+ } catch {
999
+ return result("output_invalid");
1000
+ }
1001
+ if (child.status !== 0 || value?.configurationReady !== true) {
1002
+ return result(safeFailureCodes.has(value?.code) ? value.code : "check_failed");
1003
+ }
1004
+ need(value.schema === "hasna.trash.station-setup-result/v1" && value.station === plan.spec.station && value.provider === provider && value.planDigest === plan.planDigest && value.serviceReady === true && value.guardVerified === true && value.managedInstructionsVerified === true && value.mcpConfigurationVerified === true && value.mcpExecutionVerified === false && value.nativeAdoptionVerified === false && child.stderr.length === 0, "output_invalid");
1005
+ checkPin(b.plan, 65536);
1006
+ checkPin({ path: cli, sha256: b.trash.cliSha256 }, 64 * 1024 * 1024, true);
1007
+ checkPin(b.runtime, 200000000, true);
1008
+ need(read(bindingPath, 65536).equals(bindingBytes) && read(manifestPath, 65536).equals(manifestBytes), "file_changed");
1009
+ need(remaining() > 0, "deadline_exceeded");
1010
+ return result("ready", { station: value.station, planDigest: value.planDigest });
1011
+ } catch (error) {
1012
+ return result(error instanceof ReadinessError ? error.code : phase === "binding" && error?.code === "ENOENT" ? "binding_missing" : "binding_invalid");
1013
+ }
1014
+ }
1015
+ function checkTrashReadinessBinding(input) {
1016
+ const home = realpathSync(homedir2()), b = parseTrashReadinessBinding(input, input?.provider, home);
1017
+ const path = trashReadinessBindingPath(b.provider, home), raw = Buffer.from(JSON.stringify(b) + `
1018
+ `);
1019
+ return runTrashReadiness(b.provider, { home, read: (selected, maximum, executable) => selected === path ? raw : readSafe(selected, maximum, executable) });
1020
+ }
1021
+ function readinessContext(result) {
1022
+ return result.ready ? `Trash readiness verified for ${result.station} (${result.provider}). Use Trash for recoverable file removal.` : `Trash readiness needs attention (${result.provider}: ${result.code}). Preserve files; do not fall back to direct deletion. Refresh the station setup before removal.`;
1023
+ }
1024
+ function readinessSessionStartOutput(result) {
1025
+ return { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: readinessContext(result) } };
1026
+ }
1027
+ async function handleReadinessSessionStart(provider, event, dependencies = {}) {
1028
+ const input = event;
1029
+ if (!object(input) || input.hook_event_name !== "SessionStart" || !["startup", "resume", "clear", "fork", "compact"].includes(input.source)) {
1030
+ return readinessSessionStartOutput({ schema: "hasna.hooks.trash-readiness-result/v1", provider, ready: false, code: "input_invalid", nativeAdoptionVerified: false });
1031
+ }
1032
+ if (input.source === "compact")
1033
+ return {};
1034
+ return readinessSessionStartOutput(await runTrashReadiness(provider, dependencies));
1035
+ }
1036
+ var TRASH_READINESS_DEFAULT_TIMEOUT_MS = 45000, TRASH_READINESS_MAX_TIMEOUT_MS = 60000, TRASH_READINESS_NATIVE_TIMEOUT_SECONDS, ReadinessError, sha = (value) => createHash("sha256").update(value).digest("hex"), digest = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v), absolute = (v) => typeof v === "string" && v.length <= 4096 && isAbsolute(v) && resolve2(v) === v && !/[\0\r\n]/.test(v), object = (v) => v !== null && typeof v === "object" && !Array.isArray(v), keys = (v, expected) => object(v) && Object.keys(v).sort().join() === expected.sort().join(), pin = (v) => keys(v, ["path", "sha256"]) && absolute(v.path) && digest(v.sha256), safeFailureCodes;
1037
+ var init_native_readiness = __esm(() => {
1038
+ init_readiness_process();
1039
+ TRASH_READINESS_NATIVE_TIMEOUT_SECONDS = TRASH_READINESS_MAX_TIMEOUT_MS / 1000 + 5;
1040
+ ReadinessError = class ReadinessError extends Error {
1041
+ code;
1042
+ constructor(code) {
1043
+ super(code);
1044
+ this.code = code;
1045
+ }
1046
+ };
1047
+ safeFailureCodes = new Set([
1048
+ "unsupported_platform",
1049
+ "station_detection_failed",
1050
+ "station_mismatch",
1051
+ "hosted_unavailable",
1052
+ "authentication_refused",
1053
+ "station_setup_required",
1054
+ "invalid_status",
1055
+ "deadline_exceeded",
1056
+ "retention_mismatch",
1057
+ "managed_instructions_refused",
1058
+ "removal_instruction_missing",
1059
+ "instructions_changed",
1060
+ "native_config_changed",
1061
+ "guard_refused",
1062
+ "owner_refused"
1063
+ ]);
1064
+ });
1065
+
735
1066
  // src/config.ts
736
- import { join as join2 } from "path";
1067
+ import { join as join3 } from "path";
737
1068
  function getHooksDataDir() {
738
1069
  return getEffectiveDataRoot();
739
1070
  }
740
1071
  function getCustomHooksDir() {
741
- return join2(getHooksDataDir(), "hooks");
1072
+ return join3(getHooksDataDir(), "hooks");
742
1073
  }
743
1074
  function getLockPath() {
744
1075
  const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
745
1076
  if (explicit)
746
1077
  return explicit;
747
- return join2(getHooksDataDir(), "hooks.lock");
1078
+ return join3(getHooksDataDir(), "hooks.lock");
748
1079
  }
749
1080
  var init_config = __esm(() => {
750
1081
  init_app_home();
@@ -774,8 +1105,8 @@ __export(exports_manifest, {
774
1105
  ScriptContainmentError: () => ScriptContainmentError,
775
1106
  HOOK_NAME_RE: () => HOOK_NAME_RE
776
1107
  });
777
- import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "fs";
778
- import { basename, dirname, isAbsolute, join as join3, normalize, resolve as resolve2, sep } from "path";
1108
+ import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, realpathSync as realpathSync2, writeFileSync } from "fs";
1109
+ import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join4, normalize, resolve as resolve3, sep } from "path";
779
1110
  import { z } from "zod";
780
1111
  function parseManifest(json) {
781
1112
  const parsed = JSON.parse(json);
@@ -791,8 +1122,8 @@ function isWithin(child, parent) {
791
1122
  return child === parent || child.startsWith(parent + sep);
792
1123
  }
793
1124
  function assertContainedLexical(target, root, raw) {
794
- const lexical = resolve2(target);
795
- const rootLexical = resolve2(root);
1125
+ const lexical = resolve3(target);
1126
+ const rootLexical = resolve3(root);
796
1127
  if (!isWithin(lexical, rootLexical)) {
797
1128
  throw new ScriptContainmentError(target, root, raw);
798
1129
  }
@@ -802,16 +1133,16 @@ function realpathNearest(path) {
802
1133
  const tail = [];
803
1134
  let cur = path;
804
1135
  while (!existsSync2(cur)) {
805
- const parent = dirname(cur);
1136
+ const parent = dirname2(cur);
806
1137
  if (parent === cur)
807
1138
  break;
808
1139
  tail.unshift(basename(cur));
809
1140
  cur = parent;
810
1141
  }
811
- return join3(realpathSync(cur), ...tail);
1142
+ return join4(realpathSync2(cur), ...tail);
812
1143
  }
813
1144
  function assertContainedReal(root, target, raw) {
814
- const rootReal = realpathSync(root);
1145
+ const rootReal = realpathSync2(root);
815
1146
  const targetReal = realpathNearest(target);
816
1147
  if (!isWithin(targetReal, rootReal)) {
817
1148
  throw new ScriptContainmentError(target, root, raw);
@@ -832,7 +1163,7 @@ function scriptRelFor(manifest) {
832
1163
  return `script${ext}`;
833
1164
  }
834
1165
  const rel = normalize(manifest.script);
835
- if (isAbsolute(rel)) {
1166
+ if (isAbsolute2(rel)) {
836
1167
  throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
837
1168
  }
838
1169
  return rel;
@@ -841,25 +1172,25 @@ function resolveScript(manifest, manifestDir) {
841
1172
  if (isInlineScript(manifest))
842
1173
  return { path: scriptRelFor(manifest), content: manifest.script };
843
1174
  const rel = scriptRelFor(manifest);
844
- const resolved = assertContained(join3(manifestDir, rel), manifestDir, manifest.script);
1175
+ const resolved = assertContained(join4(manifestDir, rel), manifestDir, manifest.script);
845
1176
  if (!existsSync2(resolved)) {
846
1177
  throw new Error(`manifest script file not found: ${resolved}`);
847
1178
  }
848
1179
  return { path: rel, content: readFileSync(resolved, "utf-8") };
849
1180
  }
850
1181
  function customHookDir(name) {
851
- return join3(getCustomHooksDir(), shortManifestName(name));
1182
+ return join4(getCustomHooksDir(), shortManifestName(name));
852
1183
  }
853
1184
  function readCustomManifest(name) {
854
1185
  const dir = customHookDir(name);
855
- const manifestPath = join3(dir, "manifest.json");
1186
+ const manifestPath = join4(dir, "manifest.json");
856
1187
  if (!existsSync2(manifestPath))
857
1188
  return;
858
1189
  try {
859
1190
  const manifest = parseManifest(readFileSync(manifestPath, "utf-8"));
860
1191
  const script = resolveScript(manifest, dir);
861
1192
  const scriptIsInline = isInlineScript(manifest);
862
- return { manifest, scriptPath: join3(dir, script.path), scriptContent: script.content, scriptIsInline };
1193
+ return { manifest, scriptPath: join4(dir, script.path), scriptContent: script.content, scriptIsInline };
863
1194
  } catch (err) {
864
1195
  if (err instanceof ScriptContainmentError)
865
1196
  throw err;
@@ -880,18 +1211,18 @@ function listCustomHooks() {
880
1211
  }
881
1212
  function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
882
1213
  const dir = customHookDir(name);
883
- if (isAbsolute(scriptRelPath)) {
1214
+ if (isAbsolute2(scriptRelPath)) {
884
1215
  throw new ScriptContainmentError(scriptRelPath, dir);
885
1216
  }
886
- const scriptPath = scriptRelPath.includes("/") ? join3(dir, scriptRelPath) : join3(dir, scriptRelPath);
1217
+ const scriptPath = scriptRelPath.includes("/") ? join4(dir, scriptRelPath) : join4(dir, scriptRelPath);
887
1218
  assertContainedLexical(scriptPath, dir, scriptRelPath);
888
1219
  mkdirSync(dir, { recursive: true });
889
1220
  assertContainedReal(dir, scriptPath, scriptRelPath);
890
- const scriptDir = dirname(scriptPath);
1221
+ const scriptDir = dirname2(scriptPath);
891
1222
  if (scriptDir !== dir)
892
1223
  mkdirSync(scriptDir, { recursive: true });
893
1224
  writeFileSync(scriptPath, scriptContent, "utf-8");
894
- writeFileSync(join3(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
1225
+ writeFileSync(join4(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
895
1226
  `, "utf-8");
896
1227
  return { dir, scriptPath };
897
1228
  }
@@ -935,14 +1266,14 @@ __export(exports_resolve, {
935
1266
  resolveHook: () => resolveHook,
936
1267
  resolveBundledHooksDir: () => resolveBundledHooksDir
937
1268
  });
938
- import { dirname as dirname2, join as join4 } from "path";
1269
+ import { dirname as dirname3, join as join5 } from "path";
939
1270
  import { existsSync as existsSync3 } from "fs";
940
1271
  import { fileURLToPath } from "url";
941
- function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
942
- const sourceLayout = join4(moduleDir, "..", "..", "hooks");
943
- if (existsSync3(join4(sourceLayout, "hook-gitguard")))
1272
+ function resolveBundledHooksDir(moduleDir = dirname3(fileURLToPath(import.meta.url))) {
1273
+ const sourceLayout = join5(moduleDir, "..", "..", "hooks");
1274
+ if (existsSync3(join5(sourceLayout, "hook-gitguard")))
944
1275
  return sourceLayout;
945
- return join4(moduleDir, "..", "hooks");
1276
+ return join5(moduleDir, "..", "hooks");
946
1277
  }
947
1278
  function resolveHookDir(name) {
948
1279
  const custom = readCustomManifest(name);
@@ -953,10 +1284,10 @@ function resolveHookDir(name) {
953
1284
  return;
954
1285
  const short = shortManifestName(name);
955
1286
  const bundledBase = resolveBundledHooksDir();
956
- const direct = join4(bundledBase, short);
1287
+ const direct = join5(bundledBase, short);
957
1288
  if (existsSync3(direct))
958
1289
  return direct;
959
- const prefixed = join4(bundledBase, `hook-${short}`);
1290
+ const prefixed = join5(bundledBase, `hook-${short}`);
960
1291
  return existsSync3(prefixed) ? prefixed : undefined;
961
1292
  }
962
1293
  function resolveScriptPath(name) {
@@ -966,7 +1297,7 @@ function resolveScriptPath(name) {
966
1297
  const dir = resolveHookDir(name);
967
1298
  if (!dir)
968
1299
  return;
969
- const script = join4(dir, "src", "hook.ts");
1300
+ const script = join5(dir, "src", "hook.ts");
970
1301
  return existsSync3(script) ? script : undefined;
971
1302
  }
972
1303
  function resolveHookMeta(name) {
@@ -1023,15 +1354,15 @@ var init_resolve = __esm(() => {
1023
1354
  });
1024
1355
 
1025
1356
  // src/lib/native-safety.ts
1026
- import { createHash } from "crypto";
1027
- import { closeSync, constants, existsSync as existsSync4, fstatSync, lstatSync, openSync, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
1028
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join5 } from "path";
1029
- import { homedir as homedir2 } from "os";
1357
+ import { createHash as createHash2 } from "crypto";
1358
+ import { closeSync as closeSync2, constants as constants2, existsSync as existsSync4, fstatSync as fstatSync2, lstatSync as lstatSync2, openSync as openSync2, readFileSync as readFileSync2, realpathSync as realpathSync3 } from "fs";
1359
+ import { dirname as dirname4, isAbsolute as isAbsolute3, join as join6 } from "path";
1360
+ import { homedir as homedir3 } from "os";
1030
1361
  function isNativeSafetyName(name) {
1031
1362
  return name === "trash-guard" || name === "workspace-repos-guard";
1032
1363
  }
1033
1364
  function validateBinding(b) {
1034
- if (!b || !isNativeSafetyName(b.name) || ![b.runtime, b.worker, b.home].every((p) => typeof p === "string" && isAbsolute2(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) => !isAbsolute2(p) || /[\0\r\n]/.test(p)))
1365
+ if (!b || !isNativeSafetyName(b.name) || ![b.runtime, b.worker, b.home].every((p) => typeof p === "string" && isAbsolute3(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) => !isAbsolute3(p) || /[\0\r\n]/.test(p)))
1035
1366
  throw new Error("Invalid native safety binding");
1036
1367
  }
1037
1368
  function buildNativeSafetyCommand(binding) {
@@ -1056,27 +1387,27 @@ function nativeSafetyCommandBinding(command) {
1056
1387
  }
1057
1388
  }
1058
1389
  function pinEligibleFile(file, maximum, executable = false) {
1059
- if (!isAbsolute2(file) || realpathSync2(file) !== file)
1390
+ if (!isAbsolute3(file) || realpathSync3(file) !== file)
1060
1391
  throw new Error("Native safety requires canonical files");
1061
- for (let parent = dirname3(file);; ) {
1062
- const s = lstatSync(parent);
1392
+ for (let parent = dirname4(file);; ) {
1393
+ const s = lstatSync2(parent);
1063
1394
  if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || s.mode & 18)
1064
1395
  throw new Error("Native safety requires trusted, non-writable file ancestry");
1065
- if (parent === dirname3(parent))
1396
+ if (parent === dirname4(parent))
1066
1397
  break;
1067
- parent = dirname3(parent);
1398
+ parent = dirname4(parent);
1068
1399
  }
1069
- const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
1400
+ const fd = openSync2(file, constants2.O_RDONLY | constants2.O_NOFOLLOW);
1070
1401
  try {
1071
- const s = fstatSync(fd);
1402
+ const s = fstatSync2(fd);
1072
1403
  if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || s.mode & 18 || s.size > maximum || executable && !(s.mode & 64))
1073
1404
  throw new Error("Native safety requires an eligible installed file");
1074
1405
  const bytes = readFileSync2(fd);
1075
1406
  if (bytes.length > maximum)
1076
1407
  throw new Error("Native safety file exceeds its bound");
1077
- return createHash("sha256").update(bytes).digest("hex");
1408
+ return createHash2("sha256").update(bytes).digest("hex");
1078
1409
  } finally {
1079
- closeSync(fd);
1410
+ closeSync2(fd);
1080
1411
  }
1081
1412
  }
1082
1413
  function pinVerified(file, expected, maximum, executable = false) {
@@ -1093,14 +1424,14 @@ function nativeSafetyBindingVerified(command) {
1093
1424
  return pinVerified(binding.runtime, binding.runtimeSHA256, 200000000, true) && pinVerified(binding.worker, binding.workerSHA256, 256000);
1094
1425
  }
1095
1426
  function installedNativeSafetyCommand(name) {
1096
- const home = realpathSync2(homedir2());
1097
- const runtime = realpathSync2(process.execPath);
1098
- const candidates = [join5(import.meta.dir, "../../bin/native-safety-entry.js"), join5(import.meta.dir, "../bin/native-safety-entry.js"), join5(import.meta.dir, "native-safety-entry.js")];
1427
+ const home = realpathSync3(homedir3());
1428
+ const runtime = realpathSync3(process.execPath);
1429
+ const candidates = [join6(import.meta.dir, "../../bin/native-safety-entry.js"), join6(import.meta.dir, "../bin/native-safety-entry.js"), join6(import.meta.dir, "native-safety-entry.js")];
1099
1430
  const worker = candidates.find(existsSync4);
1100
1431
  if (!worker)
1101
1432
  throw new Error("Bundled native safety runner is missing; build or reinstall @hasna/hooks before installing its guards.");
1102
- const canonicalWorker = realpathSync2(worker);
1103
- return buildNativeSafetyCommand({ name, runtime, runtimeSHA256: pinEligibleFile(runtime, 200000000, true), worker: canonicalWorker, workerSHA256: pinEligibleFile(canonicalWorker, 256000), home, path: `${dirname3(runtime)}:${join5(home, ".bun/bin")}:/usr/local/bin:/usr/bin:/bin` });
1433
+ const canonicalWorker = realpathSync3(worker);
1434
+ return buildNativeSafetyCommand({ name, runtime, runtimeSHA256: pinEligibleFile(runtime, 200000000, true), worker: canonicalWorker, workerSHA256: pinEligibleFile(canonicalWorker, 256000), home, path: `${dirname4(runtime)}:${join6(home, ".bun/bin")}:/usr/local/bin:/usr/bin:/bin` });
1104
1435
  }
1105
1436
  var supervisorSource, quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'", program, shell, prefix;
1106
1437
  var init_native_safety = __esm(() => {
@@ -1171,333 +1502,6 @@ if [ "$result" -ne 0 ]; then printf '%s\\n' 'Native safety guard refused: verifi
1171
1502
  prefix = `/usr/bin/env -i /bin/sh -c ${quote(shell)} hasna-native-safety-v1 `;
1172
1503
  });
1173
1504
 
1174
- // src/lib/readiness-process.ts
1175
- import { spawn } from "child_process";
1176
- function runReadinessProcess(command, args, options) {
1177
- if (!["darwin", "linux"].includes(process.platform)) {
1178
- return Promise.resolve({ status: null, error: { code: "ENOTSUP" }, stdout: "", stderr: "" });
1179
- }
1180
- if (options.signal?.aborted) {
1181
- return Promise.resolve({ status: null, error: { code: "ABORT_ERR" }, stdout: "", stderr: "" });
1182
- }
1183
- return new Promise((resolve3) => {
1184
- const child = spawn(command, ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", "--eval", supervisor, "--", JSON.stringify({ command, args, maxBuffer: options.maxBuffer, timeout: options.timeout })], {
1185
- cwd: options.cwd,
1186
- env: options.env,
1187
- detached: true,
1188
- stdio: ["pipe", "pipe", "pipe"]
1189
- });
1190
- let status = null, error;
1191
- let stopping = false, exited = false, finished = false;
1192
- let stdout = "", stderr = "", control = "";
1193
- const maximumEnvelope = options.maxBuffer * 12 + 1024;
1194
- let cleanup;
1195
- const finish = () => {
1196
- if (finished)
1197
- return;
1198
- finished = true;
1199
- clearTimeout(deadline);
1200
- clearTimeout(cleanup);
1201
- options.signal?.removeEventListener("abort", abort);
1202
- child.stdin?.destroy();
1203
- child.stdout?.destroy();
1204
- child.stderr?.destroy();
1205
- resolve3({ status, error, stdout, stderr });
1206
- };
1207
- const stop = (code) => {
1208
- if (code && !finished)
1209
- error ??= { code };
1210
- if (finished || stopping)
1211
- return;
1212
- stopping = true;
1213
- if (exited || !child.pid) {
1214
- error ??= { code: "ECHILD" };
1215
- } else {
1216
- try {
1217
- child.stdin.end();
1218
- } catch {
1219
- error = { code: "ECHILD" };
1220
- }
1221
- }
1222
- cleanup = setTimeout(() => {
1223
- error = { code: "ECHILD" };
1224
- finish();
1225
- }, 1000);
1226
- };
1227
- const abort = () => stop("ABORT_ERR");
1228
- const deadline = setTimeout(() => stop("ETIMEDOUT"), options.timeout);
1229
- options.signal?.addEventListener("abort", abort, { once: true });
1230
- if (options.signal?.aborted)
1231
- abort();
1232
- child.stdin?.on("error", () => {
1233
- if (!stopping)
1234
- stop("ECHILD");
1235
- });
1236
- child.stdout?.on("data", (bytes) => {
1237
- if (stopping)
1238
- return;
1239
- control += bytes.toString("utf8");
1240
- if (Buffer.byteLength(control) > maximumEnvelope)
1241
- return stop("ENOBUFS");
1242
- if (!control.endsWith(`
1243
- `))
1244
- return;
1245
- try {
1246
- const value = JSON.parse(control);
1247
- if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["status", "error", "stdout", "stderr"].includes(key)) || !(value.status === null || Number.isInteger(value.status) && value.status >= 0 && value.status <= 255) || typeof value.stdout !== "string" || typeof value.stderr !== "string" || Buffer.byteLength(value.stdout) > options.maxBuffer || Buffer.byteLength(value.stderr) > options.maxBuffer || value.error !== undefined && (Object.keys(value.error).join() !== "code" || !["ENOBUFS", "ECHILD"].includes(value.error.code))) {
1248
- return stop("EPROTO");
1249
- }
1250
- status = value.status;
1251
- stdout = value.stdout;
1252
- stderr = value.stderr;
1253
- stop(value.error?.code);
1254
- } catch {
1255
- stop("EPROTO");
1256
- }
1257
- });
1258
- child.stdout?.on("error", () => stop("EPROTO"));
1259
- child.stderr?.on("data", () => stop("ECHILD"));
1260
- child.stderr?.on("error", () => stop("ECHILD"));
1261
- child.on("error", () => {
1262
- error = { code: "ECHILD" };
1263
- stop();
1264
- });
1265
- child.on("exit", () => {
1266
- exited = true;
1267
- if (!stopping)
1268
- stop("ECHILD");
1269
- });
1270
- child.on("close", () => {
1271
- if (!stopping)
1272
- error = { code: "ECHILD" };
1273
- finish();
1274
- });
1275
- });
1276
- }
1277
- var supervisor;
1278
- var init_readiness_process = __esm(() => {
1279
- supervisor = String.raw`
1280
- const {spawn}=require("node:child_process");
1281
- const spec=JSON.parse(process.argv.at(-1));
1282
- // This runtime was created detached and is its group's live leader. Its
1283
- // independent timer and parent pipe remain effective if the observer exits
1284
- // or its event loop stops responding. Only this freshly created tree is hit.
1285
- const cleanup=()=>{try{process.kill(-process.pid,"SIGKILL")}catch{process.exit(1)}};
1286
- setTimeout(cleanup,spec.timeout);
1287
- process.stdin.once("end",cleanup);
1288
- process.stdin.once("error",cleanup);
1289
- process.stdin.on("data",cleanup);
1290
- process.stdin.resume();
1291
- for(const signal of ["SIGTERM","SIGINT","SIGHUP"])process.once(signal,cleanup);
1292
- let done=false, out=Buffer.alloc(0), err=Buffer.alloc(0);
1293
- function report(status,error){
1294
- if(done)return; done=true;
1295
- process.stdout.write(JSON.stringify({status,error,stdout:out.toString("utf8"),stderr:err.toString("utf8")})+"\n");
1296
- }
1297
- const child=spawn(spec.command,spec.args,{stdio:["ignore","pipe","pipe"],env:process.env});
1298
- for(const [stream,name] of [[child.stdout,"stdout"],[child.stderr,"stderr"]]){
1299
- stream.on("data",bytes=>{
1300
- if(done)return;
1301
- const before=name==="stdout"?out:err;
1302
- if(before.length+bytes.length>spec.maxBuffer){report(null,{code:"ENOBUFS"});return}
1303
- if(name==="stdout")out=Buffer.concat([out,bytes]);else err=Buffer.concat([err,bytes]);
1304
- });
1305
- stream.on("error",()=>report(null,{code:"ECHILD"}));
1306
- }
1307
- child.on("error",()=>report(null,{code:"ECHILD"}));
1308
- // A successful observation includes complete streams. A nested client that
1309
- // keeps them open after its parent exits is a failure requiring group cleanup.
1310
- child.on("exit",()=>setTimeout(()=>report(null,{code:"ECHILD"}),100));
1311
- child.on("close",(status,signal)=>report(status,signal?{code:"ECHILD"}:undefined));
1312
- `;
1313
- });
1314
-
1315
- // src/native-readiness.ts
1316
- var exports_native_readiness = {};
1317
- __export(exports_native_readiness, {
1318
- trashReadinessBindingPath: () => trashReadinessBindingPath,
1319
- runTrashReadiness: () => runTrashReadiness,
1320
- readinessSessionStartOutput: () => readinessSessionStartOutput,
1321
- readinessContext: () => readinessContext,
1322
- readTrashReadinessBindingFile: () => readTrashReadinessBindingFile,
1323
- parseTrashReadinessBinding: () => parseTrashReadinessBinding,
1324
- isReadinessProvider: () => isReadinessProvider,
1325
- handleReadinessSessionStart: () => handleReadinessSessionStart,
1326
- checkTrashReadinessBinding: () => checkTrashReadinessBinding
1327
- });
1328
- import { createHash as createHash2 } from "crypto";
1329
- import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, lstatSync as lstatSync2, openSync as openSync2, readSync, realpathSync as realpathSync3 } from "fs";
1330
- import { homedir as homedir3 } from "os";
1331
- import { dirname as dirname4, isAbsolute as isAbsolute3, join as join6, relative, resolve as resolve3 } from "path";
1332
- function need(value, code) {
1333
- if (!value)
1334
- throw new ReadinessError(code);
1335
- }
1336
- function isReadinessProvider(value) {
1337
- return value === "claude" || value === "codex" || value === "sumi";
1338
- }
1339
- function inside(home, path) {
1340
- const part = relative(home, path);
1341
- return part !== "" && part !== ".." && !part.startsWith("../") && !isAbsolute3(part);
1342
- }
1343
- function parseTrashReadinessBinding(input, provider, home) {
1344
- const b = input;
1345
- need(isReadinessProvider(provider) && absolute(home), "binding_invalid");
1346
- need(keys(b, ["schema", "provider", "home", "runtime", "trash", "plan", "timeoutMs"]) && b.schema === "hasna.hooks.trash-readiness/v1" && b.provider === provider && b.home === home && pin(b.runtime) && pin(b.plan) && keys(b.trash, ["root", "version", "cliSha256"]) && absolute(b.trash.root) && typeof b.trash.version === "string" && b.trash.version.length <= 128 && /^\d+\.\d+\.\d+$/.test(b.trash.version) && digest(b.trash.cliSha256) && Number.isInteger(b.timeoutMs) && b.timeoutMs >= 1000 && b.timeoutMs <= 15000 && inside(home, b.plan.path) && inside(home, b.trash.root), "binding_invalid");
1347
- return structuredClone(b);
1348
- }
1349
- function trashReadinessBindingPath(provider, home = homedir3()) {
1350
- need(isReadinessProvider(provider) && absolute(home), "binding_invalid");
1351
- return join6(home, ".hasna", "hooks", "native", "trash-readiness", provider + ".json");
1352
- }
1353
- function readSafe(path, maximum, executable = false) {
1354
- need(absolute(path) && realpathSync3(path) === path, "unsafe_path");
1355
- for (let parent = dirname4(path);; parent = dirname4(parent)) {
1356
- const stat = lstatSync2(parent);
1357
- need(stat.isDirectory() && [0, process.getuid?.()].includes(stat.uid) && !(stat.mode & 18), "unsafe_path");
1358
- if (parent === dirname4(parent))
1359
- break;
1360
- }
1361
- const fd = openSync2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
1362
- try {
1363
- const stat = fstatSync2(fd);
1364
- need(stat.isFile() && [0, process.getuid?.()].includes(stat.uid) && !(stat.mode & 18) && stat.size <= maximum && (!executable || !!(stat.mode & 64)), "unsafe_path");
1365
- const bytes = Buffer.alloc(stat.size + 1);
1366
- let length = 0;
1367
- while (length < bytes.length) {
1368
- const count = readSync(fd, bytes, length, bytes.length - length, null);
1369
- if (!count)
1370
- break;
1371
- length += count;
1372
- }
1373
- const after = fstatSync2(fd);
1374
- need(length === stat.size && after.size === stat.size && after.mtimeMs === stat.mtimeMs && after.ctimeMs === stat.ctimeMs, "file_changed");
1375
- return bytes.subarray(0, length);
1376
- } finally {
1377
- closeSync2(fd);
1378
- }
1379
- }
1380
- function readTrashReadinessBindingFile(path, provider, home = realpathSync3(homedir3())) {
1381
- const raw = readSafe(path, 65536);
1382
- return { binding: parseTrashReadinessBinding(JSON.parse(raw.toString("utf8")), provider, home), raw, sha256: sha(raw) };
1383
- }
1384
- async function runTrashReadiness(provider, d = {}) {
1385
- const started = performance.now();
1386
- const result = (code, extra = {}) => ({
1387
- schema: "hasna.hooks.trash-readiness-result/v1",
1388
- provider,
1389
- ready: code === "ready",
1390
- code,
1391
- nativeAdoptionVerified: false,
1392
- ...extra
1393
- });
1394
- let phase = "binding";
1395
- try {
1396
- const home = d.home ?? realpathSync3(homedir3()), read = d.read ?? readSafe;
1397
- need(isReadinessProvider(provider), "binding_invalid");
1398
- const bindingPath = trashReadinessBindingPath(provider, home), bindingBytes = read(bindingPath, 65536);
1399
- const b = parseTrashReadinessBinding(JSON.parse(bindingBytes.toString("utf8")), provider, home);
1400
- phase = "consumer";
1401
- const checkPin = (p, maximum, executable = false) => {
1402
- const bytes = read(p.path, maximum, executable);
1403
- need(sha(bytes) === p.sha256, "file_changed");
1404
- return bytes;
1405
- };
1406
- checkPin(b.runtime, 200000000, true);
1407
- const manifestPath = join6(b.trash.root, "package.json"), manifestBytes = read(manifestPath, 65536);
1408
- const manifest = JSON.parse(manifestBytes.toString("utf8"));
1409
- need(manifest.name === "@hasna/trash" && manifest.version === b.trash.version && manifest.bin?.trash === "dist/cli/index.js", "package_mismatch");
1410
- const cli = join6(b.trash.root, "dist", "cli", "index.js");
1411
- checkPin({ path: cli, sha256: b.trash.cliSha256 }, 64 * 1024 * 1024, true);
1412
- const plan = JSON.parse(checkPin(b.plan, 65536).toString("utf8"));
1413
- need(plan?.schema === "hasna.trash.station-setup-plan/v1" && plan.stage === "ready" && plan.configurationReady === true && plan.nativeAdoptionVerified === false && digest(plan.planDigest) && plan.spec?.provider === provider && plan.spec.home === home && typeof plan.spec.station === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(plan.spec.station) && plan.spec.runtime?.path === b.runtime.path && plan.spec.runtime?.sha256 === b.runtime.sha256 && absolute(plan.spec.cwd) && (plan.spec.cwd === home || inside(home, plan.spec.cwd)) && typeof plan.spec.path === "string" && plan.spec.path.length <= 8192 && plan.spec.path.split(":").every(absolute), "plan_invalid");
1414
- const args = ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", cli, "station", "check", "--plan", b.plan.path];
1415
- const remaining = () => Math.floor(b.timeoutMs - (performance.now() - started));
1416
- const timeout = remaining();
1417
- need(timeout > 0, "deadline_exceeded");
1418
- const child = await (d.spawn ?? runReadinessProcess)(b.runtime.path, args, {
1419
- cwd: home,
1420
- env: { HOME: home, PATH: plan.spec.path, LANG: "C", NO_COLOR: "1" },
1421
- timeout,
1422
- maxBuffer: 16384,
1423
- signal: d.signal
1424
- });
1425
- need(remaining() > 0, "deadline_exceeded");
1426
- if (child.error && "code" in child.error && child.error.code === "ETIMEDOUT")
1427
- return result("deadline_exceeded");
1428
- if (child.error || child.signal || child.status === null)
1429
- return result("execution_failed");
1430
- need(typeof child.stdout === "string" && typeof child.stderr === "string" && Buffer.byteLength(child.stdout) <= 16384 && Buffer.byteLength(child.stderr) <= 16384, "output_invalid");
1431
- let value;
1432
- try {
1433
- value = JSON.parse(child.stdout);
1434
- } catch {
1435
- return result("output_invalid");
1436
- }
1437
- if (child.status !== 0 || value?.configurationReady !== true) {
1438
- return result(safeFailureCodes.has(value?.code) ? value.code : "check_failed");
1439
- }
1440
- need(value.schema === "hasna.trash.station-setup-result/v1" && value.station === plan.spec.station && value.provider === provider && value.planDigest === plan.planDigest && value.serviceReady === true && value.guardVerified === true && value.managedInstructionsVerified === true && value.mcpConfigurationVerified === true && value.mcpExecutionVerified === false && value.nativeAdoptionVerified === false && child.stderr.length === 0, "output_invalid");
1441
- checkPin(b.plan, 65536);
1442
- checkPin({ path: cli, sha256: b.trash.cliSha256 }, 64 * 1024 * 1024, true);
1443
- checkPin(b.runtime, 200000000, true);
1444
- need(read(bindingPath, 65536).equals(bindingBytes) && read(manifestPath, 65536).equals(manifestBytes), "file_changed");
1445
- need(remaining() > 0, "deadline_exceeded");
1446
- return result("ready", { station: value.station, planDigest: value.planDigest });
1447
- } catch (error) {
1448
- return result(error instanceof ReadinessError ? error.code : phase === "binding" && error?.code === "ENOENT" ? "binding_missing" : "binding_invalid");
1449
- }
1450
- }
1451
- function checkTrashReadinessBinding(input) {
1452
- const home = realpathSync3(homedir3()), b = parseTrashReadinessBinding(input, input?.provider, home);
1453
- const path = trashReadinessBindingPath(b.provider, home), raw = Buffer.from(JSON.stringify(b) + `
1454
- `);
1455
- return runTrashReadiness(b.provider, { home, read: (selected, maximum, executable) => selected === path ? raw : readSafe(selected, maximum, executable) });
1456
- }
1457
- function readinessContext(result) {
1458
- return result.ready ? `Trash readiness verified for ${result.station} (${result.provider}). Use Trash for recoverable file removal.` : `Trash readiness needs attention (${result.provider}: ${result.code}). Preserve files; do not fall back to direct deletion. Refresh the station setup before removal.`;
1459
- }
1460
- function readinessSessionStartOutput(result) {
1461
- return { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: readinessContext(result) } };
1462
- }
1463
- async function handleReadinessSessionStart(provider, event, dependencies = {}) {
1464
- const input = event;
1465
- if (!object(input) || input.hook_event_name !== "SessionStart" || !["startup", "resume", "clear", "fork", "compact"].includes(input.source)) {
1466
- return readinessSessionStartOutput({ schema: "hasna.hooks.trash-readiness-result/v1", provider, ready: false, code: "input_invalid", nativeAdoptionVerified: false });
1467
- }
1468
- if (input.source === "compact")
1469
- return {};
1470
- return readinessSessionStartOutput(await runTrashReadiness(provider, dependencies));
1471
- }
1472
- var ReadinessError, sha = (value) => createHash2("sha256").update(value).digest("hex"), digest = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v), absolute = (v) => typeof v === "string" && v.length <= 4096 && isAbsolute3(v) && resolve3(v) === v && !/[\0\r\n]/.test(v), object = (v) => v !== null && typeof v === "object" && !Array.isArray(v), keys = (v, expected) => object(v) && Object.keys(v).sort().join() === expected.sort().join(), pin = (v) => keys(v, ["path", "sha256"]) && absolute(v.path) && digest(v.sha256), safeFailureCodes;
1473
- var init_native_readiness = __esm(() => {
1474
- init_readiness_process();
1475
- ReadinessError = class ReadinessError extends Error {
1476
- code;
1477
- constructor(code) {
1478
- super(code);
1479
- this.code = code;
1480
- }
1481
- };
1482
- safeFailureCodes = new Set([
1483
- "unsupported_platform",
1484
- "station_detection_failed",
1485
- "station_mismatch",
1486
- "hosted_unavailable",
1487
- "authentication_refused",
1488
- "station_setup_required",
1489
- "invalid_status",
1490
- "deadline_exceeded",
1491
- "retention_mismatch",
1492
- "managed_instructions_refused",
1493
- "removal_instruction_missing",
1494
- "instructions_changed",
1495
- "native_config_changed",
1496
- "guard_refused",
1497
- "owner_refused"
1498
- ]);
1499
- });
1500
-
1501
1505
  // src/lib/native-readiness-command.ts
1502
1506
  import { createHash as createHash3 } from "crypto";
1503
1507
  import { constants as constants3, closeSync as closeSync3, existsSync as existsSync5, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3, readFileSync as readFileSync3, realpathSync as realpathSync4 } from "fs";
@@ -33965,7 +33969,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
33965
33969
  name,
33966
33970
  event: "SessionStart",
33967
33971
  matcher: "^(startup|resume|clear|fork)$",
33968
- timeoutSeconds: 20,
33972
+ timeoutSeconds: TRASH_READINESS_NATIVE_TIMEOUT_SECONDS,
33969
33973
  displayName: "Trash Readiness",
33970
33974
  description: "Read-only native startup readiness",
33971
33975
  version: "0.1.0",
@@ -34351,6 +34355,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
34351
34355
  var __dirname2, HOOKS_DIR, EVENT_MAP, settingsPreimages, getInstalledHooks, JSON_SETTINGS_TARGETS;
34352
34356
  var init_installer = __esm(() => {
34353
34357
  init_registry();
34358
+ init_native_readiness();
34354
34359
  init_resolve();
34355
34360
  init_manifest();
34356
34361
  init_registration();
@@ -35177,7 +35182,7 @@ function selectCodexSafety(discovery, configuration, home, cwd, featureEnabled,
35177
35182
  need8(selected.length !== 0, "guard_missing");
35178
35183
  need8(selected.length === 1, "guard_ambiguous");
35179
35184
  const hook = selected[0], binding = readiness ? nativeReadinessCommandBinding(hook.command) : nativeSafetyCommandBinding(hook.command);
35180
- need8(binding.home === home && (!readiness || binding?.provider === "codex") && hook.eventName === (readiness ? "sessionStart" : "preToolUse") && hook.handlerType === "command" && hook.matcher === (readiness ? "^(startup|resume|clear|fork)$" : matcher) && hook.timeoutSec === (readiness ? 20 : 10) && hook.async === false && hook.statusMessage === null && hook.additionalContextLimit === null && typeof hook.key === "string" && hook.key.length <= 4096 && typeof hook.sourcePath === "string" && isAbsolute18(hook.sourcePath) && hook.pluginId === null && hook.isManaged === false && ["user", "project"].includes(hook.source) && typeof hook.currentHash === "string" && /^sha256:[a-f0-9]{64}$/.test(hook.currentHash), "guard_definition_invalid");
35185
+ need8(binding.home === home && (!readiness || binding?.provider === "codex") && hook.eventName === (readiness ? "sessionStart" : "preToolUse") && hook.handlerType === "command" && hook.matcher === (readiness ? "^(startup|resume|clear|fork)$" : matcher) && hook.timeoutSec === (readiness ? TRASH_READINESS_NATIVE_TIMEOUT_SECONDS : 10) && hook.async === false && hook.statusMessage === null && hook.additionalContextLimit === null && typeof hook.key === "string" && hook.key.length <= 4096 && typeof hook.sourcePath === "string" && isAbsolute18(hook.sourcePath) && hook.pluginId === null && hook.isManaged === false && ["user", "project"].includes(hook.source) && typeof hook.currentHash === "string" && /^sha256:[a-f0-9]{64}$/.test(hook.currentHash), "guard_definition_invalid");
35181
35186
  need8(typeof hook.enabled === "boolean" && ["trusted", "untrusted", "modified"].includes(hook.trustStatus), "guard_definition_invalid");
35182
35187
  if (requireReady) {
35183
35188
  need8(hook.enabled === true, "guard_disabled");
@@ -35248,6 +35253,7 @@ var init_codex_safety_check = __esm(() => {
35248
35253
  init_native_safety();
35249
35254
  init_native_readiness_command();
35250
35255
  init_native_safety2();
35256
+ init_native_readiness();
35251
35257
  versions = new Set(["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.154.0-alpha.6.1", "codex-cli 0.155.0", "codex-cli 0.155.1", "codex-cli 0.156.1", "codex-cli 0.157.0", "codex-cli 0.157.1"]);
35252
35258
  CodexSafetyError = class CodexSafetyError extends Error {
35253
35259
  code;