@lousy-agents/cli 5.18.0 → 5.18.2

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
@@ -3032,7 +3032,7 @@ exports.basename = (path, { windows } = {}) => {
3032
3032
 
3033
3033
 
3034
3034
  },
3035
- 6367(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
3035
+ 7015(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
3036
3036
  // NAMESPACE OBJECT: ../../node_modules/micromark/lib/constructs.js
3037
3037
  var constructs_namespaceObject = {};
3038
3038
  __webpack_require__.r(constructs_namespaceObject);
@@ -3070,40 +3070,16 @@ function categorizeFsSafeError(code) {
3070
3070
  class errors_FsSafeError extends Error {
3071
3071
  code;
3072
3072
  category;
3073
+ details;
3073
3074
  constructor(code, message, options = {}) {
3074
3075
  super(message, options);
3075
3076
  this.name = "FsSafeError";
3076
3077
  this.code = code;
3077
3078
  this.category = categorizeFsSafeError(code);
3079
+ this.details = options.details;
3078
3080
  }
3079
3081
  }
3080
3082
 
3081
- // EXTERNAL MODULE: external "node:stream/promises"
3082
- var external_node_stream_promises_ = __webpack_require__(6466);
3083
- // EXTERNAL MODULE: external "node:stream"
3084
- var external_node_stream_ = __webpack_require__(7075);
3085
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/bounded-read-stream.js
3086
-
3087
-
3088
- function createMaxBytesTransform(maxBytes) {
3089
- let bytes = 0;
3090
- return new external_node_stream_.Transform({
3091
- transform(chunk, _encoding, callback) {
3092
- const buffer = chunk instanceof Buffer ? chunk : Buffer.from(chunk);
3093
- bytes += buffer.byteLength;
3094
- if (bytes > maxBytes) {
3095
- callback(new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`));
3096
- return;
3097
- }
3098
- callback(null, buffer);
3099
- },
3100
- });
3101
- }
3102
- function createBoundedReadStream(opened, maxBytes) {
3103
- const stream = opened.handle.createReadStream();
3104
- return maxBytes === undefined ? stream : stream.pipe(createMaxBytesTransform(maxBytes));
3105
- }
3106
-
3107
3083
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/file-identity.js
3108
3084
 
3109
3085
  function isZero(value) {
@@ -3128,86 +3104,11 @@ function file_identity_sameFileIdentity(left, right, platform = process.platform
3128
3104
  return platform === "win32" && (isZero(left.dev) || isZero(right.dev));
3129
3105
  }
3130
3106
 
3131
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/string-coerce.js
3132
- function readStringValue(value) {
3133
- return typeof value === "string" ? value : undefined;
3134
- }
3135
- function normalizeNullableString(value) {
3136
- if (typeof value !== "string") {
3137
- return null;
3138
- }
3139
- const trimmed = value.trim();
3140
- return trimmed ? trimmed : null;
3141
- }
3142
- function normalizeOptionalString(value) {
3143
- return normalizeNullableString(value) ?? undefined;
3144
- }
3145
- function normalizeStringifiedOptionalString(value) {
3146
- if (typeof value === "string") {
3147
- return normalizeOptionalString(value);
3148
- }
3149
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
3150
- return normalizeOptionalString(String(value));
3151
- }
3152
- return undefined;
3153
- }
3154
- function normalizeOptionalLowercaseString(value) {
3155
- return normalizeOptionalString(value)?.toLowerCase();
3156
- }
3157
- function normalizeLowercaseStringOrEmpty(value) {
3158
- return normalizeOptionalLowercaseString(value) ?? "";
3159
- }
3160
- function normalizeFastMode(raw) {
3161
- if (typeof raw === "boolean") {
3162
- return raw;
3163
- }
3164
- if (!raw) {
3165
- return undefined;
3166
- }
3167
- const key = normalizeLowercaseStringOrEmpty(raw);
3168
- if (["off", "false", "no", "0", "disable", "disabled", "normal"].includes(key)) {
3169
- return false;
3170
- }
3171
- if (["on", "true", "yes", "1", "enable", "enabled", "fast"].includes(key)) {
3172
- return true;
3173
- }
3174
- return undefined;
3175
- }
3176
- function lowercasePreservingWhitespace(value) {
3177
- return value.toLowerCase();
3178
- }
3179
- function localeLowercasePreservingWhitespace(value) {
3180
- return value.toLocaleLowerCase();
3181
- }
3182
- function resolvePrimaryStringValue(value) {
3183
- if (typeof value === "string") {
3184
- return normalizeOptionalString(value);
3185
- }
3186
- if (!value || typeof value !== "object") {
3187
- return undefined;
3188
- }
3189
- return normalizeOptionalString(value.primary);
3190
- }
3191
- function normalizeOptionalThreadValue(value) {
3192
- if (typeof value === "number") {
3193
- return Number.isFinite(value) ? Math.trunc(value) : undefined;
3194
- }
3195
- return normalizeOptionalString(value);
3196
- }
3197
- function normalizeOptionalStringifiedId(value) {
3198
- const normalized = normalizeOptionalThreadValue(value);
3199
- return normalized == null ? undefined : String(normalized);
3200
- }
3201
- function hasNonEmptyString(value) {
3202
- return normalizeOptionalString(value) !== undefined;
3203
- }
3204
-
3205
3107
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/path.js
3206
3108
 
3207
3109
 
3208
3110
 
3209
3111
 
3210
-
3211
3112
  const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
3212
3113
  const SYMLINK_OPEN_CODES = new Set(["ELOOP", "EINVAL", "ENOTSUP"]);
3213
3114
  const POSIX_SEPARATOR_CHAR_CODE = 0x2f;
@@ -3219,7 +3120,9 @@ function normalizeWindowsPathForComparison(input) {
3219
3120
  normalized = `\\\\${normalized.slice(4)}`;
3220
3121
  }
3221
3122
  }
3222
- return normalizeLowercaseStringOrEmpty(normalized.replaceAll("/", "\\"));
3123
+ // Lowercase only: this value feeds Windows containment math, so surrounding
3124
+ // whitespace is part of the path and must not be trimmed away.
3125
+ return normalized.replaceAll("/", "\\").toLowerCase();
3223
3126
  }
3224
3127
  function isNodeError(value) {
3225
3128
  return Boolean(value && typeof value === "object" && "code" in value);
@@ -3718,6 +3621,27 @@ function isSameOrChildPath(candidate, parent) {
3718
3621
  const parentPrefix = parent.endsWith(external_node_path_.sep) ? parent : `${parent}${external_node_path_.sep}`;
3719
3622
  return candidate === parent || candidate.startsWith(parentPrefix);
3720
3623
  }
3624
+ async function realpathOrThrowNotFile(target) {
3625
+ try {
3626
+ return external_node_path_.resolve(await promises_.realpath(target));
3627
+ }
3628
+ catch (error) {
3629
+ if (path_isNotFoundPathError(error)) {
3630
+ // A dangling symlink (or a component removed between lstat and
3631
+ // realpath) is not a usable directory component.
3632
+ throw new errors_FsSafeError("not-file", "directory component must be a directory", {
3633
+ cause: error instanceof Error ? error : undefined,
3634
+ });
3635
+ }
3636
+ throw error;
3637
+ }
3638
+ }
3639
+ /**
3640
+ * Creates each missing path component from `rootReal` down to `targetPath`,
3641
+ * guarding every step. Returns the real (symlink-resolved) path of the final
3642
+ * component so callers can guard/use that path directly instead of
3643
+ * re-deriving it from the original, possibly-symlinked, lexical path.
3644
+ */
3721
3645
  async function mkdirPathComponentsWithGuards(params) {
3722
3646
  const root = external_node_path_.resolve(params.rootReal);
3723
3647
  const rootCanonical = external_node_path_.resolve(await promises_.realpath(root));
@@ -3741,18 +3665,39 @@ async function mkdirPathComponentsWithGuards(params) {
3741
3665
  }
3742
3666
  }
3743
3667
  const stat = await promises_.lstat(next);
3744
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
3668
+ if (!stat.isSymbolicLink() && !stat.isDirectory()) {
3745
3669
  throw new errors_FsSafeError("not-file", "directory component must be a directory");
3746
3670
  }
3747
3671
  // Node's recursive mkdir follows symlinks in missing components. Build one
3748
3672
  // segment at a time and realpath-check each segment before descending.
3749
- if (!isSameOrChildPath(external_node_path_.resolve(await promises_.realpath(next)), rootCanonical)) {
3673
+ const nextReal = await realpathOrThrowNotFile(next);
3674
+ if (!isSameOrChildPath(nextReal, rootCanonical)) {
3750
3675
  throw new errors_FsSafeError("outside-workspace", "directory escaped workspace root");
3751
3676
  }
3677
+ if (stat.isSymbolicLink()) {
3678
+ // An existing path component may legitimately be a symlink to a real
3679
+ // directory inside the root (e.g. a skill-bank layout). We already
3680
+ // verified above that it resolves inside the root, so treat the
3681
+ // resolved real path as the directory for the rest of this walk
3682
+ // instead of rejecting it outright. Guard checks from here on operate
3683
+ // on the real (non-symlink) path, preserving TOCTOU protection for
3684
+ // every subsequent segment. Callers that need the final directory
3685
+ // (e.g. to guard it themselves after this function returns) must use
3686
+ // the returned resolved path, not their own lexical parent path.
3687
+ const targetStat = await promises_.stat(nextReal);
3688
+ if (!targetStat.isDirectory()) {
3689
+ throw new errors_FsSafeError("not-file", "directory component must be a directory");
3690
+ }
3691
+ await directory_guard_createAsyncDirectoryGuard(nextReal);
3692
+ await assertAsyncDirectoryGuard(parentGuard);
3693
+ current = nextReal;
3694
+ continue;
3695
+ }
3752
3696
  await directory_guard_createAsyncDirectoryGuard(next);
3753
3697
  await assertAsyncDirectoryGuard(parentGuard);
3754
3698
  current = next;
3755
3699
  }
3700
+ return current;
3756
3701
  }
3757
3702
 
3758
3703
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/guarded-mutation.js
@@ -4018,8 +3963,20 @@ async function resolveOpenedFileRealPathFromParent(handleStat, ioPath) {
4018
3963
  return null;
4019
3964
  }
4020
3965
 
4021
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-python-config.js
3966
+ // EXTERNAL MODULE: external "node:module"
3967
+ var external_node_module_ = __webpack_require__(8995);
3968
+ // EXTERNAL MODULE: external "node:url"
3969
+ var external_node_url_ = __webpack_require__(3136);
3970
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/native-config.js
4022
3971
  let overrideConfig = {};
3972
+ let legacyWarningEmitted = false;
3973
+ const legacyModeEnvKeys = ["FS_SAFE_PYTHON_MODE", "OPENCLAW_FS_SAFE_PYTHON_MODE"];
3974
+ const legacyPathEnvKeys = [
3975
+ "FS_SAFE_PYTHON",
3976
+ "OPENCLAW_FS_SAFE_PYTHON",
3977
+ "OPENCLAW_PINNED_PYTHON",
3978
+ "OPENCLAW_PINNED_WRITE_PYTHON",
3979
+ ];
4023
3980
  function parseMode(value) {
4024
3981
  if (!value) {
4025
3982
  return undefined;
@@ -4036,682 +3993,495 @@ function parseMode(value) {
4036
3993
  }
4037
3994
  return undefined;
4038
3995
  }
4039
- function configureFsSafePython(config) {
3996
+ function configureFsSafeNative(config) {
4040
3997
  overrideConfig = { ...overrideConfig, ...config };
4041
3998
  }
4042
- function getFsSafePythonConfig() {
3999
+ function warnLegacyPythonConfiguration(source, mappedMode) {
4000
+ if (legacyWarningEmitted) {
4001
+ return;
4002
+ }
4003
+ legacyWarningEmitted = true;
4004
+ process.emitWarning(`${source} is a deprecated 0.4 compatibility bridge; mapped to native mode ` +
4005
+ `"${mappedMode}". Use configureFsSafeNative({ mode: "${mappedMode}" }) or ` +
4006
+ `FS_SAFE_NATIVE_MODE=${mappedMode}. Python interpreter paths are no longer used.`, { code: "FS_SAFE_PYTHON_DEPRECATED", type: "DeprecationWarning" });
4007
+ }
4008
+ function readLegacyPythonMode() {
4009
+ const configuredKeys = [...legacyModeEnvKeys, ...legacyPathEnvKeys].filter((key) => process.env[key] !== undefined);
4010
+ if (configuredKeys.length === 0) {
4011
+ return undefined;
4012
+ }
4013
+ const rawMode = legacyModeEnvKeys.map((key) => process.env[key]).find((value) => value !== undefined);
4014
+ const mappedMode = parseMode(rawMode) ?? "auto";
4015
+ warnLegacyPythonConfiguration(`Legacy ${configuredKeys.join(", ")}`, mappedMode);
4016
+ return mappedMode;
4017
+ }
4018
+ /**
4019
+ * @deprecated Compatibility bridge for 0.4 upgrades. Use configureFsSafeNative.
4020
+ */
4021
+ function configureFsSafePython(config) {
4022
+ const mappedMode = config.mode ?? "auto";
4023
+ warnLegacyPythonConfiguration("configureFsSafePython()", mappedMode);
4024
+ if (config.mode !== undefined) {
4025
+ configureFsSafeNative({ mode: config.mode });
4026
+ }
4027
+ }
4028
+ function getFsSafeNativeConfig() {
4029
+ const legacyMode = readLegacyPythonMode();
4043
4030
  return {
4044
4031
  mode: overrideConfig.mode ??
4045
- parseMode(process.env.FS_SAFE_PYTHON_MODE) ??
4046
- parseMode(process.env.OPENCLAW_FS_SAFE_PYTHON_MODE) ??
4032
+ parseMode(process.env.FS_SAFE_NATIVE_MODE) ??
4033
+ parseMode(process.env.OPENCLAW_FS_SAFE_NATIVE_MODE) ??
4034
+ legacyMode ??
4047
4035
  "auto",
4048
- pythonPath: overrideConfig.pythonPath ??
4049
- process.env.FS_SAFE_PYTHON ??
4050
- process.env.OPENCLAW_FS_SAFE_PYTHON ??
4051
- process.env.OPENCLAW_PINNED_PYTHON ??
4052
- process.env.OPENCLAW_PINNED_WRITE_PYTHON,
4053
4036
  };
4054
4037
  }
4055
- function canFallbackFromPythonError(error) {
4056
- const code = error instanceof Error && "code" in error ? error.code : undefined;
4057
- return (getFsSafePythonConfig().mode !== "require" &&
4058
- (code === "helper-unavailable" || code === "unsupported-platform"));
4038
+ function __resetFsSafeNativeConfigForTest() {
4039
+ overrideConfig = {};
4040
+ legacyWarningEmitted = false;
4059
4041
  }
4060
4042
 
4061
- // EXTERNAL MODULE: external "node:child_process"
4062
- var external_node_child_process_ = __webpack_require__(1421);
4063
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-python.js
4064
-
4065
-
4066
-
4067
-
4068
- const PINNED_PYTHON_WORKER_SOURCE = String.raw `
4069
- import base64, errno, json, os, secrets, stat, sys
4070
- DIR_FLAGS = os.O_RDONLY
4071
- if hasattr(os, "O_DIRECTORY"): DIR_FLAGS |= os.O_DIRECTORY
4072
- if hasattr(os, "O_NOFOLLOW"): DIR_FLAGS |= os.O_NOFOLLOW
4073
- READ_FLAGS = os.O_RDONLY
4074
- if hasattr(os, "O_NONBLOCK"): READ_FLAGS |= os.O_NONBLOCK
4075
- if hasattr(os, "O_NOFOLLOW"): READ_FLAGS |= os.O_NOFOLLOW
4076
- WRITE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL
4077
- if hasattr(os, "O_NOFOLLOW"):
4078
- WRITE_FLAGS |= os.O_NOFOLLOW
4079
- def split_relative(value):
4080
- if value in ("", "."):
4081
- return []
4082
- if "\x00" in value or value.startswith("/") or value.startswith("//"):
4083
- raise OSError(errno.EPERM, "invalid relative path")
4084
- if value.startswith("..\\"):
4085
- raise OSError(errno.EPERM, "path traversal is not allowed")
4086
- parts = [part for part in value.split("/") if part and part != "."]
4087
- for part in parts:
4088
- if part == "..":
4089
- raise OSError(errno.EPERM, "path traversal is not allowed")
4090
- return parts
4091
- def open_dir(path_value, dir_fd=None):
4092
- return os.open(path_value, DIR_FLAGS, dir_fd=dir_fd)
4093
- def walk_dir(root_fd, segments, mkdir_enabled=False):
4094
- current_fd = os.dup(root_fd)
4095
- try:
4096
- for segment in segments:
4097
- try:
4098
- next_fd = open_dir(segment, dir_fd=current_fd)
4099
- except FileNotFoundError:
4100
- if not mkdir_enabled:
4101
- raise
4102
- os.mkdir(segment, 0o777, dir_fd=current_fd)
4103
- next_fd = open_dir(segment, dir_fd=current_fd)
4104
- os.close(current_fd)
4105
- current_fd = next_fd
4106
- return current_fd
4107
- except Exception:
4108
- os.close(current_fd)
4109
- raise
4110
- def parent_and_basename(root_fd, relative):
4111
- segments = split_relative(relative)
4112
- if not segments:
4113
- raise OSError(errno.EPERM, "operation requires a non-root path")
4114
- parent_fd = walk_dir(root_fd, segments[:-1])
4115
- return parent_fd, segments[-1]
4116
- def encode_stat(st):
4117
- mode = st.st_mode
4118
- return {
4119
- "dev": st.st_dev,
4120
- "gid": st.st_gid,
4121
- "ino": st.st_ino,
4122
- "isDirectory": stat.S_ISDIR(mode),
4123
- "isFile": stat.S_ISREG(mode),
4124
- "isSymbolicLink": stat.S_ISLNK(mode),
4125
- "mode": mode,
4126
- "mtimeMs": st.st_mtime * 1000,
4127
- "nlink": st.st_nlink,
4128
- "size": st.st_size,
4129
- "uid": st.st_uid,
4130
- }
4131
- def reject_unsafe_endpoint(st):
4132
- mode = st.st_mode
4133
- if stat.S_ISLNK(mode):
4134
- raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
4135
- if stat.S_ISREG(mode) and st.st_nlink > 1:
4136
- raise OSError(errno.EPERM, "hardlinked file endpoint is not allowed")
4137
- def copy_bytes(source_fd, dest_fd):
4138
- while True:
4139
- chunk = os.read(source_fd, 65536)
4140
- if not chunk:
4141
- break
4142
- view = memoryview(chunk)
4143
- while view:
4144
- written = os.write(dest_fd, view)
4145
- if written <= 0:
4146
- raise OSError(errno.EIO, "short write")
4147
- view = view[written:]
4148
- def write_all(fd, data):
4149
- view = memoryview(data)
4150
- while view:
4151
- written = os.write(fd, view)
4152
- if written <= 0:
4153
- raise OSError(errno.EIO, "short write")
4154
- view = view[written:]
4155
- def fsync_best_effort(fd):
4156
- try: os.fsync(fd)
4157
- except OSError as error:
4158
- if error.errno != errno.EPERM: raise
4159
- def link_unsupported(exc):
4160
- unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
4161
- return getattr(exc, "errno", None) in unsupported
4162
- def link_no_replace(name, new_name, source_fd, target_fd):
4163
- linked = False
4164
- try:
4165
- os.link(name, new_name, src_dir_fd=source_fd, dst_dir_fd=target_fd, follow_symlinks=False)
4166
- linked = True
4167
- os.unlink(name, dir_fd=source_fd)
4168
- except Exception:
4169
- if linked:
4170
- try: os.unlink(new_name, dir_fd=target_fd)
4171
- except FileNotFoundError: pass
4172
- raise
4173
- os.fsync(source_fd)
4174
- if source_fd != target_fd:
4175
- os.fsync(target_fd)
4176
- def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basename, mode, expected=None, unlink_source=False):
4177
- source_fd = os.open(source_name, READ_FLAGS, dir_fd=source_parent_fd)
4178
- dest_fd = None; success = False; dest_stat = None
4179
- try:
4180
- if expected is not None:
4181
- source_stat = os.fstat(source_fd)
4182
- if source_stat.st_dev != expected.st_dev or source_stat.st_ino != expected.st_ino:
4183
- raise RuntimeError("fs-safe-source-mismatch")
4184
- dest_fd = os.open(basename, WRITE_FLAGS, mode, dir_fd=target_parent_fd)
4185
- copy_bytes(source_fd, dest_fd)
4186
- os.fsync(dest_fd)
4187
- dest_stat = os.fstat(dest_fd)
4188
- success = True
4189
- finally:
4190
- os.close(source_fd)
4191
- if dest_fd is not None:
4192
- os.close(dest_fd)
4193
- if dest_fd is not None and not success:
4194
- try: os.unlink(basename, dir_fd=target_parent_fd)
4195
- except FileNotFoundError: pass
4196
- if unlink_source:
4197
- try:
4198
- os.unlink(source_name, dir_fd=source_parent_fd)
4199
- except Exception:
4200
- try: os.unlink(basename, dir_fd=target_parent_fd)
4201
- except FileNotFoundError: pass
4202
- raise
4203
- return dest_stat
4204
- def same_identity(left, right):
4205
- return left.st_dev == right.st_dev and left.st_ino == right.st_ino
4206
- def verify_temp_name(parent_fd, temp_name, expected_stat):
4207
- current_stat = os.lstat(temp_name, dir_fd=parent_fd)
4208
- if stat.S_ISLNK(current_stat.st_mode) or not same_identity(current_stat, expected_stat):
4209
- raise RuntimeError("fs-safe-temp-mismatch")
4210
- def verify_committed_temp(parent_fd, basename, expected_stat):
4211
- final_stat = os.lstat(basename, dir_fd=parent_fd)
4212
- if not stat.S_ISLNK(final_stat.st_mode) and same_identity(final_stat, expected_stat):
4213
- return final_stat
4214
- try: os.unlink(basename, dir_fd=parent_fd)
4215
- except FileNotFoundError: pass
4216
- raise RuntimeError("fs-safe-temp-mismatch")
4217
- def commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, expected_stat):
4218
- verify_temp_name(parent_fd, temp_name, expected_stat)
4219
- if overwrite:
4220
- os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
4221
- return verify_committed_temp(parent_fd, basename, expected_stat)
4222
- else:
4223
- try:
4224
- os.link(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd, follow_symlinks=False)
4225
- final_stat = verify_committed_temp(parent_fd, basename, expected_stat)
4226
- os.unlink(temp_name, dir_fd=parent_fd)
4227
- return final_stat
4228
- except OSError as exc:
4229
- if not link_unsupported(exc):
4230
- raise
4231
- return copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode, expected_stat, True)
4232
- def assert_expected_root(root_fd, payload):
4233
- if "rootDev" in payload or "rootIno" in payload:
4234
- root_stat = os.fstat(root_fd)
4235
- if root_stat.st_dev != int(payload["rootDev"]) or root_stat.st_ino != int(payload["rootIno"]):
4236
- raise RuntimeError("fs-safe-root-mismatch")
4237
- def stat_path(root_fd, payload):
4238
- relative = payload.get("relativePath", "")
4239
- segments = split_relative(relative)
4240
- if not segments:
4241
- return encode_stat(os.fstat(root_fd))
4242
- parent_fd, basename = parent_and_basename(root_fd, relative)
4243
- try:
4244
- st = os.lstat(basename, dir_fd=parent_fd)
4245
- if payload.get("rejectSymlink", True) and stat.S_ISLNK(st.st_mode):
4246
- raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
4247
- return encode_stat(st)
4248
- finally:
4249
- os.close(parent_fd)
4250
- def readdir_path(root_fd, payload):
4251
- dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")))
4252
- try:
4253
- names = sorted(os.listdir(dir_fd))
4254
- if not payload.get("withFileTypes", False):
4255
- return names
4256
- entries = []
4257
- for name in names:
4258
- st = os.lstat(name, dir_fd=dir_fd)
4259
- entry = encode_stat(st)
4260
- entry["name"] = name
4261
- entries.append(entry)
4262
- return entries
4263
- finally:
4264
- os.close(dir_fd)
4265
- def mkdirp_path(root_fd, payload):
4266
- dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")), mkdir_enabled=True)
4267
- os.close(dir_fd); return None
4268
- def remove_tree(parent_fd, basename):
4269
- st = os.lstat(basename, dir_fd=parent_fd)
4270
- if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
4271
- dir_fd = open_dir(basename, dir_fd=parent_fd)
4272
- try:
4273
- for child in os.listdir(dir_fd):
4274
- remove_tree(dir_fd, child)
4275
- finally:
4276
- os.close(dir_fd)
4277
- os.rmdir(basename, dir_fd=parent_fd)
4278
- else:
4279
- os.unlink(basename, dir_fd=parent_fd)
4280
- def remove_path(root_fd, payload):
4281
- parent_fd, basename = parent_and_basename(root_fd, payload.get("relativePath", ""))
4282
- try:
4283
- try:
4284
- st = os.lstat(basename, dir_fd=parent_fd)
4285
- except FileNotFoundError:
4286
- if payload.get("force", True):
4287
- return None
4288
- raise
4289
- if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
4290
- if payload.get("recursive", False):
4291
- remove_tree(parent_fd, basename)
4292
- else:
4293
- os.rmdir(basename, dir_fd=parent_fd)
4294
- else:
4295
- os.unlink(basename, dir_fd=parent_fd)
4296
- return None
4297
- finally:
4298
- os.close(parent_fd)
4299
-
4300
- def rename_path(root_fd, payload):
4301
- from_parent_fd, from_base = parent_and_basename(root_fd, payload["from"])
4302
- to_parent_fd, to_base = parent_and_basename(root_fd, payload["to"])
4303
- try:
4304
- from_stat = os.lstat(from_base, dir_fd=from_parent_fd)
4305
- reject_unsafe_endpoint(from_stat)
4306
- overwrite = payload.get("overwrite", True)
4307
- if not overwrite and stat.S_ISREG(from_stat.st_mode):
4308
- try:
4309
- link_no_replace(from_base, to_base, from_parent_fd, to_parent_fd)
4310
- except OSError as exc:
4311
- if not link_unsupported(exc):
4312
- raise
4313
- copy_file_no_replace(from_parent_fd, from_base, to_parent_fd, to_base, stat.S_IMODE(from_stat.st_mode), from_stat, True)
4314
- return None
4315
- if not overwrite and stat.S_ISDIR(from_stat.st_mode):
4316
- raise RuntimeError("fs-safe-directory-noreplace-unsupported")
4317
- if not overwrite:
4318
- try:
4319
- os.lstat(to_base, dir_fd=to_parent_fd)
4320
- raise FileExistsError(errno.EEXIST, "destination exists", to_base)
4321
- except FileNotFoundError:
4322
- pass
4323
- os.rename(from_base, to_base, src_dir_fd=from_parent_fd, dst_dir_fd=to_parent_fd)
4324
- os.fsync(from_parent_fd)
4325
- if from_parent_fd != to_parent_fd:
4326
- os.fsync(to_parent_fd)
4327
- return None
4328
- finally:
4329
- os.close(from_parent_fd)
4330
- os.close(to_parent_fd)
4331
-
4332
- def create_temp_file(parent_fd, basename, mode):
4333
- prefix = "." + basename + "."
4334
- for _ in range(128):
4335
- candidate = prefix + secrets.token_hex(6) + ".tmp"
4336
- try:
4337
- fd = os.open(candidate, WRITE_FLAGS, mode, dir_fd=parent_fd)
4338
- return candidate, fd
4339
- except FileExistsError:
4340
- continue
4341
- raise RuntimeError("failed to allocate pinned temp file")
4342
-
4343
- def write_path(root_fd, payload):
4344
- parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
4345
- temp_fd = None
4346
- temp_name = None
4347
- basename = payload["basename"]
4348
- mode = int(payload.get("mode", 0o600))
4349
- overwrite = bool(payload.get("overwrite", True))
4350
- max_bytes = int(payload.get("maxBytes", -1))
4351
- data = base64.b64decode(payload.get("base64", ""))
4352
- try:
4353
- if max_bytes >= 0 and len(data) > max_bytes:
4354
- raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, len(data)))
4355
- if not overwrite:
4356
- try:
4357
- os.lstat(basename, dir_fd=parent_fd)
4358
- raise FileExistsError(errno.EEXIST, "destination exists", basename)
4359
- except FileNotFoundError:
4360
- pass
4361
- temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
4362
- os.fchmod(temp_fd, mode)
4363
- write_all(temp_fd, data)
4364
- fsync_best_effort(temp_fd)
4365
- temp_stat = os.fstat(temp_fd)
4366
- os.close(temp_fd)
4367
- temp_fd = None
4368
- result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
4369
- temp_name = None
4370
- fsync_best_effort(parent_fd)
4371
- return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
4372
- finally:
4373
- if temp_fd is not None:
4374
- os.close(temp_fd)
4375
- if temp_name is not None:
4376
- try:
4377
- os.unlink(temp_name, dir_fd=parent_fd)
4378
- except FileNotFoundError:
4379
- pass
4380
- os.close(parent_fd)
4381
-
4382
- def copy_path(root_fd, payload):
4383
- source_fd = os.open(payload["sourcePath"], READ_FLAGS)
4384
- parent_fd = None
4385
- temp_fd = None
4386
- temp_name = None
4387
- try:
4388
- source_stat = os.fstat(source_fd)
4389
- if not stat.S_ISREG(source_stat.st_mode):
4390
- raise RuntimeError("fs-safe-not-file")
4391
- if source_stat.st_dev != int(payload["sourceDev"]) or source_stat.st_ino != int(payload["sourceIno"]):
4392
- raise RuntimeError("fs-safe-source-mismatch")
4393
- basename = payload["basename"]
4394
- mode = int(payload.get("mode", 0o600))
4395
- overwrite = bool(payload.get("overwrite", True))
4396
- max_bytes = int(payload.get("maxBytes", -1))
4397
- if max_bytes >= 0 and source_stat.st_size > max_bytes:
4398
- raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
4399
- parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
4400
- temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
4401
- os.fchmod(temp_fd, mode)
4402
- written_bytes = 0
4403
- while True:
4404
- chunk = os.read(source_fd, 65536)
4405
- if not chunk:
4406
- break
4407
- written_bytes += len(chunk)
4408
- if max_bytes >= 0 and written_bytes > max_bytes:
4409
- raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, written_bytes))
4410
- view = memoryview(chunk)
4411
- while view:
4412
- written = os.write(temp_fd, view)
4413
- if written <= 0:
4414
- raise OSError(errno.EIO, "short write")
4415
- view = view[written:]
4416
- os.fsync(temp_fd)
4417
- temp_stat = os.fstat(temp_fd)
4418
- os.close(temp_fd)
4419
- temp_fd = None
4420
- result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
4421
- temp_name = None
4422
- os.fsync(parent_fd)
4423
- return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
4424
- finally:
4425
- os.close(source_fd)
4426
- if temp_fd is not None:
4427
- os.close(temp_fd)
4428
- if temp_name is not None and parent_fd is not None:
4429
- try:
4430
- os.unlink(temp_name, dir_fd=parent_fd)
4431
- except FileNotFoundError:
4432
- pass
4433
- if parent_fd is not None:
4434
- os.close(parent_fd)
4435
-
4436
- def run_operation(operation, root_path, payload):
4437
- root_fd = open_dir(root_path)
4438
- try:
4439
- assert_expected_root(root_fd, payload)
4440
- if operation == "stat":
4441
- return stat_path(root_fd, payload)
4442
- if operation == "readdir":
4443
- return readdir_path(root_fd, payload)
4444
- if operation == "mkdirp":
4445
- return mkdirp_path(root_fd, payload)
4446
- if operation == "remove":
4447
- return remove_path(root_fd, payload)
4448
- if operation == "rename":
4449
- return rename_path(root_fd, payload)
4450
- if operation == "write":
4451
- return write_path(root_fd, payload)
4452
- if operation == "copy":
4453
- return copy_path(root_fd, payload)
4454
- raise RuntimeError("unknown operation: " + operation)
4455
- finally:
4456
- os.close(root_fd)
4457
-
4458
- for line in sys.stdin:
4459
- try:
4460
- request = json.loads(line)
4461
- result = run_operation(request["operation"], request["rootPath"], request.get("payload") or {})
4462
- response = {"id": request["id"], "ok": True, "result": result}
4463
- except Exception as exc:
4464
- response = {
4465
- "id": request.get("id") if isinstance(locals().get("request"), dict) else None,
4466
- "ok": False,
4467
- "code": exc.__class__.__name__,
4468
- "errno": getattr(exc, "errno", None),
4469
- "message": str(exc),
4470
- }
4471
- print(json.dumps(response, separators=(",", ":")), flush=True)
4472
- `;
4473
- let nextRequestId = 1;
4474
- let worker = null;
4475
- function __resetPinnedPythonWorkerForTest() {
4476
- const currentWorker = worker;
4477
- worker = null;
4478
- if (!currentWorker) {
4479
- return;
4480
- }
4481
- currentWorker.pending.clear();
4482
- currentWorker.child.kill("SIGTERM");
4043
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/native.js
4044
+
4045
+
4046
+
4047
+
4048
+
4049
+
4050
+ const native_require = (0,external_node_module_.createRequire)(import.meta.url);
4051
+ const bundledNativeDirectory = (0,external_node_url_.fileURLToPath)(new URL("../dist/native/", import.meta.url));
4052
+ let native_binding;
4053
+ let loadError;
4054
+ let attempted = false;
4055
+ let loadBinding = loadBundledBinding;
4056
+ function isMuslFilename(file) {
4057
+ return file.includes("libc.musl-") || file.includes("ld-musl-");
4483
4058
  }
4484
- const PYTHON_CANDIDATE_DEFAULTS = [
4485
- "/usr/bin/python3",
4486
- "/opt/homebrew/bin/python3",
4487
- "/usr/local/bin/python3",
4488
- ];
4489
- function canExecute(binPath) {
4059
+ function isMuslFromReport() {
4490
4060
  try {
4491
- external_node_fs_.accessSync(binPath, external_node_fs_.constants.X_OK);
4492
- return true;
4061
+ if (!process.report || typeof process.report.getReport !== "function")
4062
+ return undefined;
4063
+ const report = process.report.getReport();
4064
+ if (report.header?.glibcVersionRuntime)
4065
+ return false;
4066
+ if (report.sharedObjects?.some(isMuslFilename))
4067
+ return true;
4493
4068
  }
4494
4069
  catch {
4495
- return false;
4070
+ // Continue with filesystem and ELF inspection.
4496
4071
  }
4072
+ return undefined;
4497
4073
  }
4498
- function resolvePython() {
4499
- const configured = getFsSafePythonConfig().pythonPath;
4500
- if (configured) {
4501
- return configured;
4502
- }
4503
- for (const candidate of PYTHON_CANDIDATE_DEFAULTS) {
4504
- if (canExecute(candidate)) {
4505
- return candidate;
4074
+ function isMuslFromFilesystem() {
4075
+ for (const directory of ["/lib", "/usr/lib"]) {
4076
+ try {
4077
+ if ((0,external_node_fs_.readdirSync)(directory).some(isMuslFilename))
4078
+ return true;
4079
+ }
4080
+ catch {
4081
+ // A missing or unreadable conventional library directory is inconclusive.
4506
4082
  }
4507
4083
  }
4508
- return "python3";
4084
+ return undefined;
4509
4085
  }
4510
- function assertPinnedHelperSupported() {
4511
- if (process.platform === "win32") {
4512
- throw new errors_FsSafeError("unsupported-platform", "fd-relative pinned filesystem operations are not available on Windows");
4086
+ function readUInt(buffer, offset, bytes, littleEndian) {
4087
+ if (offset < 0 || offset + bytes > buffer.length)
4088
+ return undefined;
4089
+ if (bytes === 2)
4090
+ return littleEndian ? buffer.readUInt16LE(offset) : buffer.readUInt16BE(offset);
4091
+ if (bytes === 4)
4092
+ return littleEndian ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset);
4093
+ const value = littleEndian ? buffer.readBigUInt64LE(offset) : buffer.readBigUInt64BE(offset);
4094
+ return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : undefined;
4095
+ }
4096
+ function isMuslFromElfInterpreter() {
4097
+ let fd;
4098
+ try {
4099
+ fd = (0,external_node_fs_.openSync)(process.execPath, "r");
4100
+ const header = Buffer.alloc(64);
4101
+ if ((0,external_node_fs_.readSync)(fd, header, 0, header.length, 0) < 52)
4102
+ return undefined;
4103
+ if (!header.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])))
4104
+ return undefined;
4105
+ const elfClass = header[4];
4106
+ const dataEncoding = header[5];
4107
+ if ((elfClass !== 1 && elfClass !== 2) || (dataEncoding !== 1 && dataEncoding !== 2)) {
4108
+ return undefined;
4109
+ }
4110
+ const littleEndian = dataEncoding === 1;
4111
+ const is64Bit = elfClass === 2;
4112
+ const tableOffset = readUInt(header, is64Bit ? 32 : 28, is64Bit ? 8 : 4, littleEndian);
4113
+ const entrySize = readUInt(header, is64Bit ? 54 : 42, 2, littleEndian);
4114
+ const entryCount = readUInt(header, is64Bit ? 56 : 44, 2, littleEndian);
4115
+ if (tableOffset === undefined ||
4116
+ entrySize === undefined ||
4117
+ entryCount === undefined ||
4118
+ entrySize < (is64Bit ? 56 : 32) ||
4119
+ entryCount > 1024) {
4120
+ return undefined;
4121
+ }
4122
+ const programHeader = Buffer.alloc(entrySize);
4123
+ for (let index = 0; index < entryCount; index += 1) {
4124
+ const offset = tableOffset + index * entrySize;
4125
+ if ((0,external_node_fs_.readSync)(fd, programHeader, 0, entrySize, offset) !== entrySize)
4126
+ return undefined;
4127
+ if (readUInt(programHeader, 0, 4, littleEndian) !== 3)
4128
+ continue;
4129
+ const interpreterOffset = readUInt(programHeader, is64Bit ? 8 : 4, is64Bit ? 8 : 4, littleEndian);
4130
+ const interpreterSize = readUInt(programHeader, is64Bit ? 32 : 16, is64Bit ? 8 : 4, littleEndian);
4131
+ if (interpreterOffset === undefined ||
4132
+ interpreterSize === undefined ||
4133
+ interpreterSize < 1 ||
4134
+ interpreterSize > 4096) {
4135
+ return undefined;
4136
+ }
4137
+ const interpreter = Buffer.alloc(interpreterSize);
4138
+ if ((0,external_node_fs_.readSync)(fd, interpreter, 0, interpreterSize, interpreterOffset) !== interpreterSize) {
4139
+ return undefined;
4140
+ }
4141
+ return isMuslFilename(interpreter.toString("utf8"));
4142
+ }
4513
4143
  }
4514
- if (getFsSafePythonConfig().mode === "off") {
4515
- throw new errors_FsSafeError("helper-unavailable", "Python helper is disabled");
4144
+ catch {
4145
+ return undefined;
4146
+ }
4147
+ finally {
4148
+ if (fd !== undefined) {
4149
+ try {
4150
+ (0,external_node_fs_.closeSync)(fd);
4151
+ }
4152
+ catch {
4153
+ // Best-effort detection must never turn close failure into import failure.
4154
+ }
4155
+ }
4516
4156
  }
4157
+ return undefined;
4517
4158
  }
4518
- function isSpawnUnavailable(error) {
4519
- if (!(error instanceof Error)) {
4159
+ function isMusl() {
4160
+ if (process.platform !== "linux")
4520
4161
  return false;
4162
+ for (const detector of [isMuslFromReport, isMuslFromFilesystem, isMuslFromElfInterpreter]) {
4163
+ const result = detector();
4164
+ if (result !== undefined)
4165
+ return result;
4521
4166
  }
4522
- const maybeErrno = error;
4523
- return (typeof maybeErrno.syscall === "string" &&
4524
- maybeErrno.syscall.startsWith("spawn") &&
4525
- ["EACCES", "ENOENT", "ENOEXEC"].includes(maybeErrno.code ?? ""));
4167
+ // Unknown Linux libc: try the glibc binary and let its require fail normally.
4168
+ return false;
4526
4169
  }
4527
- function mapWorkerError(response) {
4528
- const code = typeof response.code === "string" ? response.code : "";
4529
- const errno = typeof response.errno === "number" ? response.errno : undefined;
4530
- const message = typeof response.message === "string" && response.message
4531
- ? response.message
4532
- : "pinned helper failed";
4533
- const tooLarge = message.match(/fs-safe-too-large:(\d+):(\d+)/);
4534
- if (tooLarge) {
4535
- const [, limit, got] = tooLarge;
4536
- return new errors_FsSafeError("too-large", `file exceeds limit of ${limit} bytes (got at least ${got})`);
4537
- }
4538
- if (message.includes("fs-safe-not-file")) {
4539
- return new errors_FsSafeError("not-file", "not a file");
4540
- }
4541
- if (message.includes("fs-safe-source-mismatch")) {
4542
- return new errors_FsSafeError("path-mismatch", "source path changed during copy");
4543
- }
4544
- if (message.includes("fs-safe-temp-mismatch")) {
4545
- return new errors_FsSafeError("path-mismatch", "temp path changed during write");
4546
- }
4547
- if (message.includes("fs-safe-root-mismatch")) {
4548
- return new errors_FsSafeError("path-mismatch", "root path changed during operation");
4549
- }
4550
- if (message.includes("fs-safe-directory-noreplace-unsupported")) {
4551
- return new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
4552
- }
4553
- if (code === "FileNotFoundError" || errno === 2) {
4554
- return new errors_FsSafeError("not-found", "file not found");
4555
- }
4556
- if (code === "FileExistsError" || errno === 17) {
4557
- return new errors_FsSafeError("already-exists", message);
4170
+ function targetFor(platform, arch, musl) {
4171
+ if (platform === "win32" && arch === "x64")
4172
+ return "win32-x64-msvc";
4173
+ if (platform === "darwin" && arch === "x64")
4174
+ return "darwin-x64";
4175
+ if (platform === "darwin" && arch === "arm64")
4176
+ return "darwin-arm64";
4177
+ if (platform === "linux" && (arch === "x64" || arch === "arm64")) {
4178
+ return `linux-${arch}-${musl ? "musl" : "gnu"}`;
4558
4179
  }
4559
- if (errno === 39) {
4560
- return new errors_FsSafeError("not-empty", "directory is not empty");
4180
+ return undefined;
4181
+ }
4182
+ function bundledTarget() {
4183
+ return targetFor(process.platform, process.arch, isMusl());
4184
+ }
4185
+ function loadBundledBinding() {
4186
+ const target = bundledTarget();
4187
+ if (!target) {
4188
+ throw new Error(`Unsupported OS or architecture: ${process.platform}-${process.arch}`);
4561
4189
  }
4562
- if (errno === 1 || errno === 13 || errno === 21) {
4563
- return new errors_FsSafeError("not-removable", "path is not removable under root");
4190
+ return native_require((0,external_node_path_.join)(bundledNativeDirectory, target, "fs-safe-native.node"));
4191
+ }
4192
+ function __loadBundledNativeForTest() {
4193
+ return loadBundledBinding();
4194
+ }
4195
+ function __nativeLoaderDetectorsForTest() {
4196
+ return {
4197
+ report: isMuslFromReport(),
4198
+ filesystem: isMuslFromFilesystem(),
4199
+ elfInterpreter: isMuslFromElfInterpreter(),
4200
+ };
4201
+ }
4202
+ function __nativeTargetForTest(platform, arch, musl = false) {
4203
+ return targetFor(platform, arch, musl);
4204
+ }
4205
+ function getNativeBinding() {
4206
+ const { mode } = getFsSafeNativeConfig();
4207
+ if (mode === "off")
4208
+ return undefined;
4209
+ if (!attempted) {
4210
+ attempted = true;
4211
+ try {
4212
+ native_binding = loadBinding();
4213
+ }
4214
+ catch (error) {
4215
+ loadError = error;
4216
+ }
4564
4217
  }
4565
- if (code === "NotADirectoryError" || code === "OSError" || errno === 20 || errno === 40) {
4566
- return new errors_FsSafeError("path-alias", message);
4218
+ if (native_binding)
4219
+ return native_binding;
4220
+ if (mode === "require") {
4221
+ throw new errors_FsSafeError("helper-unavailable", "native fs-safe helper is unavailable", {
4222
+ cause: loadError,
4223
+ });
4567
4224
  }
4568
- return new errors_FsSafeError("helper-failed", message);
4225
+ return undefined;
4569
4226
  }
4570
- function rejectPending(error, targetWorker = worker) {
4571
- if (!targetWorker || worker !== targetWorker) {
4572
- return;
4227
+ function requireNativeBinding() {
4228
+ const native = getNativeBinding();
4229
+ if (!native) {
4230
+ throw new FsSafeError("helper-unavailable", "native fs-safe helper is unavailable", {
4231
+ cause: loadError,
4232
+ });
4573
4233
  }
4574
- setWorkerRef(targetWorker, false);
4575
- for (const pending of targetWorker.pending.values()) {
4576
- pending.reject(error);
4234
+ return native;
4235
+ }
4236
+ function __setNativeLoaderForTest(loader) {
4237
+ native_binding = undefined;
4238
+ loadError = undefined;
4239
+ attempted = false;
4240
+ loadBinding = loader;
4241
+ }
4242
+ function __resetNativeLoaderForTest() {
4243
+ native_binding = undefined;
4244
+ loadError = undefined;
4245
+ attempted = false;
4246
+ loadBinding = loadBundledBinding;
4247
+ }
4248
+
4249
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/native-operations.js
4250
+
4251
+
4252
+
4253
+
4254
+
4255
+ function nativeOpenFlags(flags) {
4256
+ const closeOnExec = external_node_fs_.constants.O_CLOEXEC;
4257
+ return (flags |
4258
+ (closeOnExec ?? 0) |
4259
+ (typeof external_node_fs_.constants.O_NOFOLLOW === "number" ? external_node_fs_.constants.O_NOFOLLOW : 0));
4260
+ }
4261
+ function writeAll(fd, data) {
4262
+ let offset = 0;
4263
+ while (offset < data.byteLength) {
4264
+ const written = external_node_fs_.writeSync(fd, data, offset, data.byteLength - offset);
4265
+ if (written <= 0) {
4266
+ throw Object.assign(new Error("native file write made no progress"), { code: "EIO" });
4267
+ }
4268
+ offset += written;
4577
4269
  }
4578
- targetWorker.pending.clear();
4579
- worker = null;
4580
4270
  }
4581
- function handleWorkerLine(currentWorker, line) {
4582
- if (worker !== currentWorker || !line.trim()) {
4271
+ function wrapNativeFd(fd, containment) {
4272
+ let open = true;
4273
+ return {
4274
+ fd,
4275
+ containment,
4276
+ async close() {
4277
+ if (open) {
4278
+ open = false;
4279
+ external_node_fs_.closeSync(fd);
4280
+ }
4281
+ },
4282
+ async stat() {
4283
+ return external_node_fs_.fstatSync(fd);
4284
+ },
4285
+ async writeFile(data, encoding) {
4286
+ writeAll(fd, Buffer.isBuffer(data) ? data : Buffer.from(data, encoding ?? "utf8"));
4287
+ },
4288
+ };
4289
+ }
4290
+ function removeNativeCreatedFileIfStillPinned(params) {
4291
+ if (!params.created) {
4583
4292
  return;
4584
4293
  }
4585
- let decoded;
4586
4294
  try {
4587
- decoded = JSON.parse(line);
4295
+ const parentPathStat = external_node_fs_.lstatSync(params.parentPath);
4296
+ const parentFdStat = params.binding.fstatIdentity(params.parentFd);
4297
+ const targetPath = external_node_path_.join(params.parentPath, params.basename);
4298
+ const target = external_node_fs_.lstatSync(targetPath);
4299
+ if (!parentPathStat.isSymbolicLink() &&
4300
+ parentPathStat.dev === parentFdStat.dev &&
4301
+ parentPathStat.ino === parentFdStat.ino &&
4302
+ !target.isSymbolicLink() &&
4303
+ file_identity_sameFileIdentity(target, params.created)) {
4304
+ external_node_fs_.rmSync(targetPath);
4305
+ }
4588
4306
  }
4589
4307
  catch {
4590
- rejectPending(new errors_FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`), currentWorker);
4591
- return;
4308
+ // Failed cleanup leaves a fail-closed artifact instead of deleting an
4309
+ // unverified pathname.
4592
4310
  }
4593
- if (typeof decoded !== "object" || decoded === null || !("id" in decoded)) {
4594
- rejectPending(new errors_FsSafeError("helper-failed", "pinned helper returned invalid response"), currentWorker);
4595
- return;
4311
+ }
4312
+ async function createNativeExclusiveFile(targetPath, mode) {
4313
+ const binding = getNativeBinding();
4314
+ if (!binding) {
4315
+ return undefined;
4596
4316
  }
4597
- const response = decoded;
4598
- const id = typeof response.id === "number" ? response.id : undefined;
4599
- if (id === undefined) {
4600
- return;
4317
+ const parentPath = external_node_path_.dirname(targetPath);
4318
+ const basename = external_node_path_.basename(targetPath);
4319
+ const parent = await promises_.open(parentPath, external_node_fs_.constants.O_RDONLY |
4320
+ (typeof external_node_fs_.constants.O_DIRECTORY === "number" ? external_node_fs_.constants.O_DIRECTORY : 0));
4321
+ let fd;
4322
+ let created;
4323
+ try {
4324
+ const opened = binding.openBeneath(parent.fd, basename, nativeOpenFlags(external_node_fs_.constants.O_WRONLY | external_node_fs_.constants.O_CREAT | external_node_fs_.constants.O_EXCL));
4325
+ fd = opened.fd;
4326
+ external_node_fs_.fchmodSync(fd, mode);
4327
+ created = external_node_fs_.fstatSync(fd);
4328
+ return wrapNativeFd(fd, opened.containment);
4601
4329
  }
4602
- const pending = currentWorker.pending.get(id);
4603
- if (!pending) {
4604
- return;
4330
+ catch (error) {
4331
+ if (fd !== undefined) {
4332
+ try {
4333
+ external_node_fs_.closeSync(fd);
4334
+ }
4335
+ catch {
4336
+ // Preserve the original error.
4337
+ }
4338
+ removeNativeCreatedFileIfStillPinned({
4339
+ binding,
4340
+ parentPath,
4341
+ parentFd: parent.fd,
4342
+ basename,
4343
+ created,
4344
+ });
4345
+ }
4346
+ throw error;
4605
4347
  }
4606
- currentWorker.pending.delete(id);
4607
- if (currentWorker.pending.size === 0) {
4608
- setWorkerRef(currentWorker, false);
4348
+ finally {
4349
+ await parent.close().catch(() => undefined);
4609
4350
  }
4610
- if (response.ok === true) {
4611
- pending.resolve(response.result);
4612
- return;
4351
+ }
4352
+ function syncNativeFileBestEffort(fd) {
4353
+ try {
4354
+ external_node_fs_.fsyncSync(fd);
4355
+ }
4356
+ catch (error) {
4357
+ if (error.code !== "EPERM") {
4358
+ throw error;
4359
+ }
4613
4360
  }
4614
- pending.reject(mapWorkerError(decoded));
4615
4361
  }
4616
- function getWorker() {
4617
- assertPinnedHelperSupported();
4618
- if (worker) {
4619
- return worker;
4362
+ function writeNativeFd(fd, data) {
4363
+ writeAll(fd, data);
4364
+ }
4365
+
4366
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/native-pinned-write.js
4367
+
4368
+
4369
+
4370
+
4371
+
4372
+
4373
+ function assertWithinMaxBytes(bytes, maxBytes) {
4374
+ if (maxBytes !== undefined && bytes > maxBytes) {
4375
+ throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
4620
4376
  }
4621
- const child = (0,external_node_child_process_.spawn)(resolvePython(), ["-u", "-c", PINNED_PYTHON_WORKER_SOURCE], {
4622
- stdio: ["pipe", "pipe", "pipe"],
4623
- });
4624
- const currentWorker = { child, pending: new Map(), stderr: "", stdoutBuffer: "" };
4625
- worker = currentWorker;
4626
- child.stdout.setEncoding("utf8");
4627
- child.stderr.setEncoding("utf8");
4628
- child.stdout.on("data", (chunk) => {
4629
- if (worker !== currentWorker) {
4630
- return;
4377
+ }
4378
+ async function inputToBuffer(input, maxBytes) {
4379
+ if (input.kind === "buffer") {
4380
+ const data = typeof input.data === "string"
4381
+ ? Buffer.from(input.data, input.encoding ?? "utf8")
4382
+ : Buffer.from(input.data);
4383
+ assertWithinMaxBytes(data.byteLength, maxBytes);
4384
+ return data;
4385
+ }
4386
+ const chunks = [];
4387
+ let bytes = 0;
4388
+ for await (const chunk of input.stream) {
4389
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4390
+ bytes += buffer.byteLength;
4391
+ assertWithinMaxBytes(bytes, maxBytes);
4392
+ chunks.push(buffer);
4393
+ }
4394
+ return Buffer.concat(chunks, bytes);
4395
+ }
4396
+ function native_pinned_write_nativeOpenFlags(flags) {
4397
+ const closeOnExec = external_node_fs_.constants.O_CLOEXEC;
4398
+ return (flags |
4399
+ (closeOnExec ?? 0) |
4400
+ (typeof external_node_fs_.constants.O_NOFOLLOW === "number" ? external_node_fs_.constants.O_NOFOLLOW : 0));
4401
+ }
4402
+ function sameNativeIdentity(left, right) {
4403
+ return left.dev === right.dev && left.ino === right.ino;
4404
+ }
4405
+ async function runPinnedWriteNative(binding, params) {
4406
+ const data = await inputToBuffer(params.input, params.maxBytes);
4407
+ const root = await promises_.open(params.rootPath, external_node_fs_.constants.O_RDONLY |
4408
+ (typeof external_node_fs_.constants.O_DIRECTORY === "number" ? external_node_fs_.constants.O_DIRECTORY : 0));
4409
+ let parentFd;
4410
+ let tempFd;
4411
+ let targetFd;
4412
+ let tempIdentity;
4413
+ let parentPath = params.rootPath;
4414
+ const tempName = `.${params.basename}.${(0,external_node_crypto_.randomUUID)()}.native.tmp`;
4415
+ let renamed = false;
4416
+ try {
4417
+ const rootIdentity = binding.fstatIdentity(root.fd);
4418
+ if (params.rootIdentity && !sameNativeIdentity(params.rootIdentity, rootIdentity)) {
4419
+ throw new errors_FsSafeError("path-mismatch", "root path changed during native write");
4420
+ }
4421
+ if (params.mkdir) {
4422
+ binding.mkdirBeneath(root.fd, params.relativeParentPath, 0o777);
4423
+ }
4424
+ const parentFlags = external_node_fs_.constants.O_RDONLY |
4425
+ (typeof external_node_fs_.constants.O_DIRECTORY === "number" ? external_node_fs_.constants.O_DIRECTORY : 0);
4426
+ parentFd = binding.openBeneath(root.fd, params.relativeParentPath, parentFlags).fd;
4427
+ parentPath = await promises_.realpath(params.relativeParentPath
4428
+ ? external_node_path_.join(params.rootPath, ...params.relativeParentPath.split("/"))
4429
+ : params.rootPath);
4430
+ const parentPathStat = await promises_.lstat(parentPath);
4431
+ const parentIdentity = binding.fstatIdentity(parentFd);
4432
+ if (parentPathStat.isSymbolicLink() ||
4433
+ !sameNativeIdentity(parentPathStat, parentIdentity)) {
4434
+ throw new errors_FsSafeError("path-mismatch", "native write parent changed during resolution");
4631
4435
  }
4632
- currentWorker.stdoutBuffer += chunk;
4633
- for (;;) {
4634
- const newline = currentWorker.stdoutBuffer.indexOf("\n");
4635
- if (newline < 0) {
4636
- break;
4436
+ try {
4437
+ await promises_.lstat(external_node_path_.join(parentPath, params.basename));
4438
+ throw Object.assign(new Error("destination already exists"), { code: "EEXIST" });
4439
+ }
4440
+ catch (error) {
4441
+ if (error.code !== "ENOENT") {
4442
+ throw error;
4637
4443
  }
4638
- const line = currentWorker.stdoutBuffer.slice(0, newline);
4639
- currentWorker.stdoutBuffer = currentWorker.stdoutBuffer.slice(newline + 1);
4640
- handleWorkerLine(currentWorker, line);
4641
4444
  }
4642
- });
4643
- child.stderr.on("data", (chunk) => {
4644
- if (worker === currentWorker) {
4645
- currentWorker.stderr = `${currentWorker.stderr}${chunk}`.slice(-4096);
4445
+ tempFd = binding.openBeneath(parentFd, tempName, native_pinned_write_nativeOpenFlags(external_node_fs_.constants.O_WRONLY | external_node_fs_.constants.O_CREAT | external_node_fs_.constants.O_EXCL)).fd;
4446
+ external_node_fs_.fchmodSync(tempFd, params.mode || 0o600);
4447
+ writeNativeFd(tempFd, data);
4448
+ syncNativeFileBestEffort(tempFd);
4449
+ tempIdentity = external_node_fs_.fstatSync(tempFd);
4450
+ binding.renameNoReplace(parentFd, tempName, parentFd, params.basename);
4451
+ renamed = true;
4452
+ targetFd = binding.openBeneath(parentFd, params.basename, native_pinned_write_nativeOpenFlags(external_node_fs_.constants.O_RDONLY)).fd;
4453
+ const targetIdentity = binding.fstatIdentity(targetFd);
4454
+ if (!targetIdentity.isFile || !sameNativeIdentity(tempIdentity, targetIdentity)) {
4455
+ throw new errors_FsSafeError("path-mismatch", "native write target changed after rename");
4646
4456
  }
4647
- });
4648
- child.once("error", (error) => {
4649
- const mapped = isSpawnUnavailable(error)
4650
- ? new errors_FsSafeError("helper-unavailable", "Python helper is unavailable", { cause: error })
4651
- : error instanceof Error
4652
- ? error
4653
- : new Error(String(error));
4654
- rejectPending(mapped, currentWorker);
4655
- });
4656
- child.once("close", (code, signal) => {
4657
- const stderr = currentWorker.stderr.trim();
4658
- rejectPending(new errors_FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`), currentWorker);
4659
- });
4660
- process.once("exit", () => {
4661
- child.kill("SIGTERM");
4662
- });
4663
- setWorkerRef(currentWorker, false);
4664
- return currentWorker;
4665
- }
4666
- function setRefable(value, ref) {
4667
- if (!value) {
4668
- return;
4457
+ syncNativeFileBestEffort(parentFd);
4458
+ return { dev: targetIdentity.dev, ino: targetIdentity.ino };
4669
4459
  }
4670
- const method = ref ? "ref" : "unref";
4671
- const refable = value;
4672
- refable[method]?.();
4673
- }
4674
- function setWorkerRef(currentWorker, ref) {
4675
- setRefable(currentWorker.child, ref);
4676
- setRefable(currentWorker.child.stdin, ref);
4677
- setRefable(currentWorker.child.stdout, ref);
4678
- setRefable(currentWorker.child.stderr, ref);
4679
- }
4680
- async function runPinnedPythonOperation(params) {
4681
- const requestId = nextRequestId++;
4682
- const currentWorker = getWorker();
4683
- if (typeof currentWorker.child.stdin?.write !== "function") {
4684
- throw new errors_FsSafeError("helper-unavailable", "Python helper stdin is unavailable");
4685
- }
4686
- setWorkerRef(currentWorker, true);
4687
- return await new Promise((resolve, reject) => {
4688
- currentWorker.pending.set(requestId, {
4689
- reject,
4690
- resolve: (value) => resolve(value),
4691
- });
4692
- const request = JSON.stringify({
4693
- id: requestId,
4694
- operation: params.operation,
4695
- rootPath: params.rootPath,
4696
- payload: params.payload,
4697
- });
4698
- currentWorker.child.stdin.write(`${request}\n`, (error) => {
4699
- if (error) {
4700
- currentWorker.pending.delete(requestId);
4701
- if (currentWorker.pending.size === 0) {
4702
- setWorkerRef(currentWorker, false);
4703
- }
4704
- reject(error);
4705
- }
4706
- });
4707
- });
4708
- }
4709
- function assertPinnedPythonOperationAvailable() {
4710
- const currentWorker = getWorker();
4711
- if (typeof currentWorker.child.stdin?.write !== "function") {
4712
- throw new errors_FsSafeError("helper-unavailable", "Python helper stdin is unavailable");
4460
+ finally {
4461
+ if (targetFd !== undefined) {
4462
+ external_node_fs_.closeSync(targetFd);
4463
+ }
4464
+ if (tempFd !== undefined) {
4465
+ external_node_fs_.closeSync(tempFd);
4466
+ }
4467
+ if (!renamed && parentFd !== undefined) {
4468
+ removeNativeCreatedFileIfStillPinned({
4469
+ binding,
4470
+ parentPath,
4471
+ parentFd,
4472
+ basename: tempName,
4473
+ created: tempIdentity,
4474
+ });
4475
+ }
4476
+ if (parentFd !== undefined) {
4477
+ external_node_fs_.closeSync(parentFd);
4478
+ }
4479
+ await root.close().catch(() => undefined);
4713
4480
  }
4714
4481
  }
4482
+
4483
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-operation.js
4484
+
4715
4485
  function validatePinnedOperationPayload(payload) {
4716
4486
  if (typeof payload.relativePath === "string") {
4717
4487
  validatePinnedRelativePath(payload.relativePath);
@@ -4726,9 +4496,6 @@ function validatePinnedOperationPayload(payload) {
4726
4496
  validatePinnedRelativePath(payload.to);
4727
4497
  }
4728
4498
  }
4729
- function isPinnedHelperUnavailable(error) {
4730
- return error instanceof Error && "code" in error && error.code === "helper-unavailable";
4731
- }
4732
4499
  function validatePinnedRelativePath(relativePath) {
4733
4500
  if (relativePath.length === 0 || relativePath === ".") {
4734
4501
  return;
@@ -4750,47 +4517,91 @@ function validatePinnedRelativePath(relativePath) {
4750
4517
  }
4751
4518
  }
4752
4519
 
4753
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-helper.js
4520
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/bounded-read.js
4754
4521
 
4755
4522
 
4756
- async function runPinnedHelper(operation, rootDir, payload) {
4757
- validatePinnedOperationPayload(payload);
4758
- return await runPinnedPythonOperation({
4759
- operation,
4760
- rootPath: rootDir,
4761
- payload,
4762
- });
4763
- }
4764
- async function helperStat(rootDir, relativePath) {
4765
- return await runPinnedHelper("stat", rootDir, { relativePath });
4523
+ const READ_CHUNK_BYTES = 64 * 1024;
4524
+ function assertMaxBytes(maxBytes) {
4525
+ if (maxBytes === Number.POSITIVE_INFINITY) {
4526
+ return;
4527
+ }
4528
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
4529
+ throw new RangeError("maxBytes must be a non-negative safe integer or Infinity");
4530
+ }
4766
4531
  }
4767
- async function helperReaddir(rootDir, relativePath, withFileTypes) {
4768
- return await runPinnedHelper("readdir", rootDir, {
4769
- relativePath,
4770
- withFileTypes,
4771
- });
4532
+ function createScratchBuffer(maxBytes) {
4533
+ const initialReadBytes = Number.isFinite(maxBytes)
4534
+ ? Math.min(READ_CHUNK_BYTES, maxBytes + 1)
4535
+ : READ_CHUNK_BYTES;
4536
+ return Buffer.allocUnsafe(Math.max(1, initialReadBytes));
4772
4537
  }
4773
-
4774
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-path.js
4775
-
4776
-
4777
-
4778
- function isPinnedPathHelperSpawnError(error) {
4779
- return canFallbackFromPythonError(error);
4538
+ function nextReadLength(total, maxBytes, capacity) {
4539
+ return Number.isFinite(maxBytes)
4540
+ ? Math.min(capacity, maxBytes - total + 1)
4541
+ : capacity;
4780
4542
  }
4781
- async function runPinnedPathHelper(params) {
4782
- try {
4783
- await runPinnedHelper(params.operation, params.rootPath, {
4784
- relativePath: params.relativePath,
4785
- });
4543
+ function appendChunk(params) {
4544
+ const total = params.total + params.bytesRead;
4545
+ if (total > params.maxBytes) {
4546
+ throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got at least ${total})`);
4786
4547
  }
4787
- catch (error) {
4788
- if (error instanceof errors_FsSafeError) {
4789
- throw error;
4548
+ params.chunks.push(Buffer.from(params.scratch.subarray(0, params.bytesRead)));
4549
+ return total;
4550
+ }
4551
+ async function readBoundedAsync(maxBytes, readChunk) {
4552
+ assertMaxBytes(maxBytes);
4553
+ const chunks = [];
4554
+ const scratch = createScratchBuffer(maxBytes);
4555
+ let total = 0;
4556
+ while (true) {
4557
+ const length = nextReadLength(total, maxBytes, scratch.length);
4558
+ const bytesRead = await readChunk(scratch, length);
4559
+ if (bytesRead === 0) {
4560
+ return Buffer.concat(chunks, total);
4790
4561
  }
4791
- throw new errors_FsSafeError("helper-failed", "pinned path helper failed", {
4792
- cause: error instanceof Error ? error : undefined,
4562
+ total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
4563
+ }
4564
+ }
4565
+ /**
4566
+ * Reads from the handle's current offset without closing it. A bounded read
4567
+ * consumes at most maxBytes + 1 bytes so growth after an earlier stat cannot
4568
+ * force an unbounded allocation.
4569
+ */
4570
+ async function readFileHandleBounded(handle, maxBytes) {
4571
+ return await readBoundedAsync(maxBytes, async (scratch, length) => {
4572
+ return (await handle.read(scratch, 0, length, null)).bytesRead;
4573
+ });
4574
+ }
4575
+ function readDescriptorChunk(fd, scratch, length) {
4576
+ return new Promise((resolve, reject) => {
4577
+ fs.read(fd, scratch, 0, length, null, (error, bytesRead) => {
4578
+ if (error) {
4579
+ reject(error);
4580
+ return;
4581
+ }
4582
+ resolve(bytesRead);
4793
4583
  });
4584
+ });
4585
+ }
4586
+ /** Async bounded read from a numeric descriptor. The caller owns the descriptor. */
4587
+ async function readFileDescriptorBounded(fd, maxBytes) {
4588
+ return await readBoundedAsync(maxBytes, async (scratch, length) => {
4589
+ return await readDescriptorChunk(fd, scratch, length);
4590
+ });
4591
+ }
4592
+ /** Sync bounded read from a numeric descriptor. The caller owns the descriptor. */
4593
+ function readFileDescriptorBoundedSync(fd, maxBytes) {
4594
+ assertMaxBytes(maxBytes);
4595
+ const chunks = [];
4596
+ const scratch = createScratchBuffer(maxBytes);
4597
+ let total = 0;
4598
+ while (true) {
4599
+ const length = nextReadLength(total, maxBytes, scratch.length);
4600
+ const bytesRead = fs.readSync(fd, scratch, 0, length, null);
4601
+ if (bytesRead === 0) {
4602
+ return Buffer.concat(chunks, total);
4603
+ }
4604
+ total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
4794
4605
  }
4795
4606
  }
4796
4607
 
@@ -4798,6 +4609,11 @@ async function runPinnedPathHelper(params) {
4798
4609
 
4799
4610
 
4800
4611
 
4612
+
4613
+
4614
+
4615
+
4616
+ const MAX_LOCK_PAYLOAD_BYTES = 1024 * 1024;
4801
4617
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES = 16;
4802
4618
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS = SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES * 8;
4803
4619
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX = "\t".repeat(8);
@@ -4821,28 +4637,98 @@ function serializeSidecarLockPayload(payload) {
4821
4637
  ownershipToken,
4822
4638
  };
4823
4639
  }
4824
- async function readSidecarLockSnapshot(lockPath) {
4640
+ function relativeSidecarLockPath(lockRoot, lockPath) {
4641
+ const resolved = external_node_path_.resolve(lockPath);
4642
+ const lexicalRelative = external_node_path_.relative(lockRoot.rootDir, resolved);
4643
+ const relative = lexicalRelative !== ".." &&
4644
+ !lexicalRelative.startsWith(`..${external_node_path_.sep}`) &&
4645
+ !external_node_path_.isAbsolute(lexicalRelative)
4646
+ ? lexicalRelative
4647
+ : external_node_path_.relative(lockRoot.rootReal, resolved);
4648
+ if (!relative || relative === ".." || relative.startsWith(`..${external_node_path_.sep}`) || external_node_path_.isAbsolute(relative)) {
4649
+ throw new errors_FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
4650
+ }
4651
+ return relative.split(external_node_path_.sep).join(external_node_path_.posix.sep);
4652
+ }
4653
+ function parseSidecarLockPayload(raw, parser) {
4654
+ if (parser) {
4655
+ return parser(raw);
4656
+ }
4657
+ try {
4658
+ const parsed = JSON.parse(raw);
4659
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
4660
+ }
4661
+ catch {
4662
+ return null;
4663
+ }
4664
+ }
4665
+ async function readSidecarLockSnapshot(lockPath, options = {}) {
4825
4666
  try {
4667
+ if (options.lockRoot) {
4668
+ const opened = await options.lockRoot.open(relativeSidecarLockPath(options.lockRoot, lockPath));
4669
+ try {
4670
+ const raw = (await readFileHandleBounded(opened.handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
4671
+ return {
4672
+ raw,
4673
+ payload: parseSidecarLockPayload(raw, options.parsePayload),
4674
+ stat: opened.stat,
4675
+ };
4676
+ }
4677
+ finally {
4678
+ await opened.handle.close().catch(() => undefined);
4679
+ }
4680
+ }
4826
4681
  const stat = await promises_.lstat(lockPath);
4827
4682
  const raw = await promises_.readFile(lockPath, "utf8");
4828
- try {
4829
- const parsed = JSON.parse(raw);
4830
- const payload = parsed && typeof parsed === "object" && !Array.isArray(parsed)
4831
- ? parsed
4832
- : null;
4833
- return { raw, payload, stat };
4834
- }
4835
- catch {
4836
- return { raw, payload: null, stat };
4837
- }
4683
+ return { raw, payload: parseSidecarLockPayload(raw, options.parsePayload), stat };
4838
4684
  }
4839
4685
  catch (err) {
4840
- if (err.code === "ENOENT") {
4686
+ if (err.code === "ENOENT" ||
4687
+ (err instanceof errors_FsSafeError && err.code === "not-found")) {
4841
4688
  return null;
4842
4689
  }
4843
4690
  throw err;
4844
4691
  }
4845
4692
  }
4693
+ function readSidecarLockSnapshotSync(lockPath, parsePayload) {
4694
+ let fd;
4695
+ try {
4696
+ const before = fsSync.lstatSync(lockPath);
4697
+ if (!before.isFile() || before.isSymbolicLink())
4698
+ return null;
4699
+ const noFollow = process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
4700
+ ? fsSync.constants.O_NOFOLLOW
4701
+ : 0;
4702
+ fd = fsSync.openSync(lockPath, fsSync.constants.O_RDONLY | noFollow);
4703
+ const opened = fsSync.fstatSync(fd);
4704
+ const raw = fsSync.readFileSync(fd, "utf8");
4705
+ const after = fsSync.lstatSync(lockPath);
4706
+ if (!sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
4707
+ return null;
4708
+ return {
4709
+ raw,
4710
+ payload: parseSidecarLockPayload(raw, parsePayload),
4711
+ stat: after,
4712
+ ownershipToken: readSidecarLockOwnershipToken(raw),
4713
+ };
4714
+ }
4715
+ catch (error) {
4716
+ if (error.code === "ENOENT")
4717
+ return null;
4718
+ throw error;
4719
+ }
4720
+ finally {
4721
+ if (fd !== undefined)
4722
+ fsSync.closeSync(fd);
4723
+ }
4724
+ }
4725
+ function removeSidecarLockIfUnchangedSync(lockPath, observed) {
4726
+ const current = readSidecarLockSnapshotSync(lockPath);
4727
+ if (!current || !sidecarLockSnapshotMatches(current, observed))
4728
+ return false;
4729
+ fsSync.rmSync(lockPath);
4730
+ return true;
4731
+ }
4846
4732
  function sidecarLockSnapshotMatches(current, observed) {
4847
4733
  if (observed.ownershipToken !== undefined) {
4848
4734
  return (current.stat?.isFile() === true &&
@@ -4860,16 +4746,21 @@ function sidecarLockSnapshotMatches(current, observed) {
4860
4746
  }
4861
4747
  return observed.stat !== undefined && current.stat !== undefined;
4862
4748
  }
4863
- async function removeSidecarLockIfUnchanged(lockPath, observed) {
4864
- const current = await readSidecarLockSnapshot(lockPath);
4749
+ async function removeSidecarLockIfUnchanged(lockPath, observed, options = {}) {
4750
+ const current = await readSidecarLockSnapshot(lockPath, options);
4865
4751
  if (!current || !observed || !sidecarLockSnapshotMatches(current, observed)) {
4866
4752
  return false;
4867
4753
  }
4868
- await promises_.rm(lockPath, { force: true }).catch(() => undefined);
4754
+ if (options.lockRoot) {
4755
+ await options.lockRoot.remove(relativeSidecarLockPath(options.lockRoot, lockPath)).catch(() => undefined);
4756
+ }
4757
+ else {
4758
+ await promises_.rm(lockPath, { force: true }).catch(() => undefined);
4759
+ }
4869
4760
  return true;
4870
4761
  }
4871
- async function sidecarLockSnapshotStillPresent(lockPath, observed) {
4872
- const current = await readSidecarLockSnapshot(lockPath);
4762
+ async function sidecarLockSnapshotStillPresent(lockPath, observed, options = {}) {
4763
+ const current = await readSidecarLockSnapshot(lockPath, options);
4873
4764
  return !!current && !!observed && sidecarLockSnapshotMatches(current, observed);
4874
4765
  }
4875
4766
  async function sidecarReclaimGuardExists(pathname) {
@@ -4905,7 +4796,8 @@ async function removeStaleSidecarLockIfAllowed(params) {
4905
4796
  if (!params.shouldRemoveStaleLock || params.snapshot.raw === undefined) {
4906
4797
  return "not-approved";
4907
4798
  }
4908
- if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot))) {
4799
+ const ioOptions = { lockRoot: params.lockRoot, parsePayload: params.parsePayload };
4800
+ if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot, ioOptions))) {
4909
4801
  return "changed";
4910
4802
  }
4911
4803
  if (!(await params.shouldRemoveStaleLock({
@@ -4916,11 +4808,16 @@ async function removeStaleSidecarLockIfAllowed(params) {
4916
4808
  }))) {
4917
4809
  return "not-approved";
4918
4810
  }
4919
- if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot))) {
4811
+ if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot, ioOptions))) {
4920
4812
  return "changed";
4921
4813
  }
4922
4814
  try {
4923
- await promises_.rm(params.lockPath);
4815
+ if (params.lockRoot) {
4816
+ await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath));
4817
+ }
4818
+ else {
4819
+ await promises_.rm(params.lockPath);
4820
+ }
4924
4821
  return "removed";
4925
4822
  }
4926
4823
  catch (err) {
@@ -4931,12 +4828,77 @@ async function removeStaleSidecarLockIfAllowed(params) {
4931
4828
  }
4932
4829
  }
4933
4830
 
4831
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.js
4832
+
4833
+ function createSidecarLockHandle(params) {
4834
+ let released = false;
4835
+ const release = async () => {
4836
+ if (released)
4837
+ return;
4838
+ released = true;
4839
+ await params.release();
4840
+ };
4841
+ return {
4842
+ lockPath: params.lockPath,
4843
+ normalizedTargetPath: params.normalizedTargetPath,
4844
+ verifyStillHeld: params.verifyStillHeld,
4845
+ release,
4846
+ [Symbol.asyncDispose]: release,
4847
+ };
4848
+ }
4849
+ function createHeldSidecarLockHandle(params) {
4850
+ return createSidecarLockHandle({
4851
+ lockPath: params.held.lockPath,
4852
+ normalizedTargetPath: params.normalizedTargetPath,
4853
+ verifyStillHeld: async () => await sidecarLockSnapshotStillPresent(params.held.lockPath, params.held.snapshot, {
4854
+ lockRoot: params.held.lockRoot,
4855
+ parsePayload: params.held.parsePayload,
4856
+ }),
4857
+ release: params.release,
4858
+ });
4859
+ }
4860
+
4861
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-policy.js
4862
+
4863
+ function computeSidecarLockDelayMs(retry, attempt) {
4864
+ const minTimeout = retry.minTimeout ?? 50;
4865
+ const maxTimeout = retry.maxTimeout ?? 1000;
4866
+ const factor = retry.factor ?? 1;
4867
+ const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
4868
+ const jitter = retry.randomize ? 1 + Math.random() : 1;
4869
+ return Math.min(maxTimeout, Math.round(base * jitter));
4870
+ }
4871
+ function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
4872
+ const createdAt = payload &&
4873
+ typeof payload === "object" &&
4874
+ "createdAt" in payload &&
4875
+ typeof payload.createdAt === "string"
4876
+ ? payload.createdAt
4877
+ : "";
4878
+ const createdAtMs = Date.parse(createdAt);
4879
+ return Number.isFinite(createdAtMs) && nowMs - createdAtMs > staleMs;
4880
+ }
4881
+ async function defaultSidecarLockShouldReclaim(params) {
4882
+ if (sidecarLockPayloadIsStale(params.payload, params.staleMs, params.nowMs))
4883
+ return true;
4884
+ try {
4885
+ return params.nowMs - (await promises_.stat(params.lockPath)).mtimeMs > params.staleMs;
4886
+ }
4887
+ catch {
4888
+ return true;
4889
+ }
4890
+ }
4891
+
4934
4892
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
4935
4893
 
4936
4894
 
4937
4895
 
4938
4896
 
4939
4897
 
4898
+
4899
+
4900
+
4901
+
4940
4902
  const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
4941
4903
  function getGlobalManagers() {
4942
4904
  const globalWithState = globalThis;
@@ -4962,6 +4924,9 @@ function resolveManagerState(key) {
4962
4924
  // Backfill state created by fs-safe versions that predate reclaim guards.
4963
4925
  state.reclaimCleanupRegistered ??= false;
4964
4926
  state.reclaimGuards ??= new Set();
4927
+ for (const held of state.held.values()) {
4928
+ held.refCount ??= 1;
4929
+ }
4965
4930
  }
4966
4931
  return state;
4967
4932
  }
@@ -5017,28 +4982,6 @@ async function resolveNormalizedTargetPath(targetPath) {
5017
4982
  return resolved;
5018
4983
  }
5019
4984
  }
5020
- function computeDelayMs(retry, attempt) {
5021
- const minTimeout = retry.minTimeout ?? 50;
5022
- const maxTimeout = retry.maxTimeout ?? 1000;
5023
- const factor = retry.factor ?? 1;
5024
- const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
5025
- const jitter = retry.randomize ? 1 + Math.random() : 1;
5026
- return Math.min(maxTimeout, Math.round(base * jitter));
5027
- }
5028
- async function defaultShouldReclaim(params) {
5029
- const createdAt = typeof params.payload?.createdAt === "string" ? params.payload.createdAt : "";
5030
- const createdAtMs = Date.parse(createdAt);
5031
- if (Number.isFinite(createdAtMs) && params.nowMs - createdAtMs > params.staleMs) {
5032
- return true;
5033
- }
5034
- try {
5035
- const stat = await promises_.stat(params.lockPath);
5036
- return params.nowMs - stat.mtimeMs > params.staleMs;
5037
- }
5038
- catch {
5039
- return true;
5040
- }
5041
- }
5042
4985
  function releaseAllReclaimGuardsSync(state) {
5043
4986
  for (const reclaimGuardPath of state.reclaimGuards) {
5044
4987
  try {
@@ -5054,7 +4997,7 @@ function releaseAllLocksSync(state) {
5054
4997
  for (const [normalizedTargetPath, held] of state.held) {
5055
4998
  void held.handle.close().catch(() => undefined);
5056
4999
  try {
5057
- if (snapshotMatchesSync(held.lockPath, held.snapshot)) {
5000
+ if (!held.lockRoot && snapshotMatchesSync(held.lockPath, held.snapshot)) {
5058
5001
  external_node_fs_.rmSync(held.lockPath, { force: true });
5059
5002
  }
5060
5003
  }
@@ -5065,17 +5008,17 @@ function releaseAllLocksSync(state) {
5065
5008
  }
5066
5009
  releaseAllReclaimGuardsSync(state);
5067
5010
  }
5068
- async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
5011
+ async function releaseHeldLock(state, normalizedTargetPath, held, options = {}) {
5069
5012
  const current = state.held.get(normalizedTargetPath);
5070
5013
  if (current !== held) {
5071
5014
  return false;
5072
5015
  }
5073
- if (opts.force) {
5074
- held.count = 0;
5016
+ if (options.force) {
5017
+ held.refCount = 0;
5075
5018
  }
5076
5019
  else {
5077
- held.count -= 1;
5078
- if (held.count > 0) {
5020
+ held.refCount -= 1;
5021
+ if (held.refCount > 0) {
5079
5022
  return false;
5080
5023
  }
5081
5024
  }
@@ -5084,9 +5027,16 @@ async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
5084
5027
  return true;
5085
5028
  }
5086
5029
  state.held.delete(normalizedTargetPath);
5030
+ if (held.compromiseTimer) {
5031
+ clearInterval(held.compromiseTimer);
5032
+ held.compromiseTimer = undefined;
5033
+ }
5087
5034
  held.releasePromise = (async () => {
5088
5035
  await held.handle.close().catch(() => undefined);
5089
- await removeSidecarLockIfUnchanged(held.lockPath, held.snapshot);
5036
+ await removeSidecarLockIfUnchanged(held.lockPath, held.snapshot, {
5037
+ lockRoot: held.lockRoot,
5038
+ parsePayload: held.parsePayload,
5039
+ });
5090
5040
  })();
5091
5041
  try {
5092
5042
  await held.releasePromise;
@@ -5096,6 +5046,13 @@ async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
5096
5046
  held.releasePromise = undefined;
5097
5047
  }
5098
5048
  }
5049
+ function handleForHeldLock(state, normalizedTargetPath, held) {
5050
+ return createHeldSidecarLockHandle({
5051
+ normalizedTargetPath,
5052
+ held,
5053
+ release: async () => await releaseHeldLock(state, normalizedTargetPath, held),
5054
+ });
5055
+ }
5099
5056
  function createSidecarLockManager(key) {
5100
5057
  const state = resolveManagerState(key);
5101
5058
  function ensureExitCleanupRegistered() {
@@ -5115,15 +5072,12 @@ function createSidecarLockManager(key) {
5115
5072
  const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
5116
5073
  const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
5117
5074
  const held = state.held.get(normalizedTargetPath);
5118
- if (held && options.allowReentrant) {
5119
- held.count += 1;
5120
- const release = () => releaseHeldLock(state, normalizedTargetPath, held).then(() => undefined);
5121
- return {
5122
- lockPath,
5123
- normalizedTargetPath,
5124
- release,
5125
- [Symbol.asyncDispose]: release,
5126
- };
5075
+ if (held &&
5076
+ options.reentrantOwner !== undefined &&
5077
+ held.reentrantOwner !== undefined &&
5078
+ options.reentrantOwner === held.reentrantOwner) {
5079
+ held.refCount += 1;
5080
+ return handleForHeldLock(state, normalizedTargetPath, held);
5127
5081
  }
5128
5082
  const startedAt = Date.now();
5129
5083
  const retry = options.retry ?? {};
@@ -5146,7 +5100,7 @@ function createSidecarLockManager(key) {
5146
5100
  const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
5147
5101
  ? Number.POSITIVE_INFINITY
5148
5102
  : Math.max(0, options.timeoutMs - elapsed);
5149
- const delay = Math.min(computeDelayMs(retry, attempt), remaining);
5103
+ const delay = Math.min(computeSidecarLockDelayMs(retry, attempt), remaining);
5150
5104
  attempt += 1;
5151
5105
  await new Promise((resolve) => setTimeout(resolve, delay));
5152
5106
  };
@@ -5158,18 +5112,36 @@ function createSidecarLockManager(key) {
5158
5112
  }
5159
5113
  let handle = null;
5160
5114
  try {
5161
- handle = await promises_.open(lockPath, "wx");
5162
5115
  const payload = await options.payload();
5163
5116
  const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
5164
- await handle.writeFile(raw, "utf8");
5117
+ if (options.lockRoot) {
5118
+ const relativeLockPath = relativeSidecarLockPath(options.lockRoot, lockPath);
5119
+ try {
5120
+ await options.lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
5121
+ }
5122
+ catch (error) {
5123
+ if (error instanceof errors_FsSafeError && error.code === "already-exists") {
5124
+ throw Object.assign(new Error("sidecar lock exists"), { code: "EEXIST" });
5125
+ }
5126
+ throw error;
5127
+ }
5128
+ handle = (await options.lockRoot.open(relativeLockPath)).handle;
5129
+ }
5130
+ else {
5131
+ handle = (await createNativeExclusiveFile(lockPath, 0o600)) ?? await promises_.open(lockPath, "wx");
5132
+ await handle.writeFile(raw, "utf8");
5133
+ }
5165
5134
  const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
5166
5135
  const createdHeld = {
5167
- count: 1,
5136
+ refCount: 1,
5137
+ reentrantOwner: options.reentrantOwner,
5168
5138
  handle,
5169
5139
  lockPath,
5170
5140
  snapshot,
5171
5141
  acquiredAt: Date.now(),
5172
5142
  metadata: options.metadata ?? {},
5143
+ lockRoot: options.lockRoot,
5144
+ parsePayload: options.parsePayload,
5173
5145
  };
5174
5146
  state.held.set(normalizedTargetPath, createdHeld);
5175
5147
  if (ownsReclaimGuard) {
@@ -5182,13 +5154,21 @@ function createSidecarLockManager(key) {
5182
5154
  throw err;
5183
5155
  }
5184
5156
  }
5185
- const release = () => releaseHeldLock(state, normalizedTargetPath, createdHeld).then(() => undefined);
5186
- return {
5187
- lockPath,
5188
- normalizedTargetPath,
5189
- release,
5190
- [Symbol.asyncDispose]: release,
5191
- };
5157
+ const returnedHandle = handleForHeldLock(state, normalizedTargetPath, createdHeld);
5158
+ const interval = options.compromiseCheckIntervalMs;
5159
+ if (options.onCompromised && interval !== undefined && interval > 0) {
5160
+ createdHeld.compromiseTimer = setInterval(() => {
5161
+ void returnedHandle.verifyStillHeld().then((stillHeld) => {
5162
+ if (!stillHeld && createdHeld.compromiseTimer) {
5163
+ clearInterval(createdHeld.compromiseTimer);
5164
+ createdHeld.compromiseTimer = undefined;
5165
+ options.onCompromised?.({ lockPath, normalizedTargetPath });
5166
+ }
5167
+ });
5168
+ }, interval);
5169
+ createdHeld.compromiseTimer.unref();
5170
+ }
5171
+ return returnedHandle;
5192
5172
  }
5193
5173
  catch (err) {
5194
5174
  if (handle) {
@@ -5205,11 +5185,16 @@ function createSidecarLockManager(key) {
5205
5185
  }
5206
5186
  // If payload serialization/write fails, the file may be empty or
5207
5187
  // partial JSON, so remove while our exclusive handle is still open.
5208
- await promises_.rm(lockPath, { force: true }).catch(() => undefined);
5188
+ if (!options.lockRoot) {
5189
+ await promises_.rm(lockPath, { force: true }).catch(() => undefined);
5190
+ }
5209
5191
  await handle.close().catch(() => undefined);
5210
5192
  // Windows can refuse removing an open file; retry after close but
5211
5193
  // only if the path still points at the file identity we created.
5212
- await removeSidecarLockIfUnchanged(lockPath, failedSnapshot);
5194
+ await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
5195
+ lockRoot: options.lockRoot,
5196
+ parsePayload: options.parsePayload,
5197
+ });
5213
5198
  }
5214
5199
  if (err.code !== "EEXIST") {
5215
5200
  throw err;
@@ -5220,11 +5205,18 @@ function createSidecarLockManager(key) {
5220
5205
  continue;
5221
5206
  }
5222
5207
  const nowMs = Date.now();
5223
- const snapshot = await readSidecarLockSnapshot(lockPath);
5208
+ const snapshot = await readSidecarLockSnapshot(lockPath, {
5209
+ lockRoot: options.lockRoot,
5210
+ parsePayload: options.parsePayload,
5211
+ });
5224
5212
  if (!snapshot) {
5225
5213
  continue;
5226
5214
  }
5227
- const shouldReclaim = options.shouldReclaim ?? defaultShouldReclaim;
5215
+ if (state.held.has(normalizedTargetPath)) {
5216
+ await waitForRetry();
5217
+ continue;
5218
+ }
5219
+ const shouldReclaim = options.shouldReclaim ?? defaultSidecarLockShouldReclaim;
5228
5220
  if (await shouldReclaim({
5229
5221
  lockPath,
5230
5222
  normalizedTargetPath,
@@ -5233,7 +5225,10 @@ function createSidecarLockManager(key) {
5233
5225
  nowMs,
5234
5226
  heldByThisProcess: state.held.has(normalizedTargetPath),
5235
5227
  })) {
5236
- if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot))) {
5228
+ if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot, {
5229
+ lockRoot: options.lockRoot,
5230
+ parsePayload: options.parsePayload,
5231
+ }))) {
5237
5232
  continue;
5238
5233
  }
5239
5234
  const staleRecovery = options.staleRecovery ?? "fail-closed";
@@ -5248,6 +5243,8 @@ function createSidecarLockManager(key) {
5248
5243
  normalizedTargetPath,
5249
5244
  snapshot,
5250
5245
  shouldRemoveStaleLock: options.shouldRemoveStaleLock,
5246
+ lockRoot: options.lockRoot,
5247
+ parsePayload: options.parsePayload,
5251
5248
  });
5252
5249
  if (removal === "removed" || removal === "changed") {
5253
5250
  continue;
@@ -5335,6 +5332,7 @@ function __setFsSafeTestHooksForTest(hooks) {
5335
5332
 
5336
5333
 
5337
5334
 
5335
+
5338
5336
  function pinned_write_byteLength(input, encoding) {
5339
5337
  return typeof input === "string"
5340
5338
  ? Buffer.byteLength(input, encoding ?? "utf8")
@@ -5349,7 +5347,7 @@ function assertSafeBasename(basename) {
5349
5347
  throw new errors_FsSafeError("invalid-path", "invalid target path");
5350
5348
  }
5351
5349
  }
5352
- function assertWithinMaxBytes(bytes, maxBytes) {
5350
+ function pinned_write_assertWithinMaxBytes(bytes, maxBytes) {
5353
5351
  if (maxBytes !== undefined && bytes > maxBytes) {
5354
5352
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
5355
5353
  }
@@ -5369,7 +5367,7 @@ async function writeStreamToHandle(stream, handle, maxBytes) {
5369
5367
  for await (const chunk of stream) {
5370
5368
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5371
5369
  bytes += buffer.byteLength;
5372
- assertWithinMaxBytes(bytes, maxBytes);
5370
+ pinned_write_assertWithinMaxBytes(bytes, maxBytes);
5373
5371
  let offset = 0;
5374
5372
  while (offset < buffer.byteLength) {
5375
5373
  const { bytesWritten } = await handle.write(buffer, offset, buffer.byteLength - offset);
@@ -5380,74 +5378,21 @@ async function writeStreamToHandle(stream, handle, maxBytes) {
5380
5378
  }
5381
5379
  }
5382
5380
  }
5383
- async function inputToBase64(input, maxBytes) {
5384
- if (input.kind === "buffer") {
5385
- assertWithinMaxBytes(pinned_write_byteLength(input.data, input.encoding), maxBytes);
5386
- return (typeof input.data === "string"
5387
- ? Buffer.from(input.data, input.encoding ?? "utf8")
5388
- : input.data).toString("base64");
5389
- }
5390
- const chunks = [];
5391
- let bytes = 0;
5392
- for await (const chunk of input.stream) {
5393
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5394
- bytes += buffer.byteLength;
5395
- assertWithinMaxBytes(bytes, maxBytes);
5396
- chunks.push(buffer);
5397
- }
5398
- return Buffer.concat(chunks, bytes).toString("base64");
5399
- }
5400
5381
  async function runPinnedWriteHelper(params) {
5401
5382
  assertSafeBasename(params.basename);
5402
5383
  validatePinnedOperationPayload({
5403
5384
  relativeParentPath: params.relativeParentPath,
5404
5385
  });
5405
- // The Python helper deliberately enforces the strict post-rename inode
5406
- // contract. The explicit compatibility policy therefore uses the guarded
5407
- // Node fallback, where content verification can replace that one check.
5386
+ // The explicit compatibility policy uses the guarded Node fallback, where
5387
+ // content verification can replace the strict post-rename inode check.
5408
5388
  if (params.onRenameIdentityMismatch === "verify-content") {
5409
5389
  return await runPinnedWriteFallback(params);
5410
5390
  }
5411
- if (getFsSafePythonConfig().mode === "off") {
5412
- return await runPinnedWriteFallback(params);
5413
- }
5414
- if (params.input.kind === "stream") {
5415
- try {
5416
- assertPinnedPythonOperationAvailable();
5417
- }
5418
- catch (error) {
5419
- if (canFallbackFromPythonError(error)) {
5420
- return await runPinnedWriteFallback(params);
5421
- }
5422
- throw error;
5423
- }
5424
- }
5425
- const input = params.input.kind === "stream"
5426
- ? { kind: "buffer", data: Buffer.from(await inputToBase64(params.input, params.maxBytes), "base64") }
5427
- : params.input;
5428
- const payload = {
5429
- base64: await inputToBase64(input, params.maxBytes),
5430
- basename: params.basename,
5431
- maxBytes: params.maxBytes ?? -1,
5432
- mkdir: params.mkdir,
5433
- mode: params.mode || 0o600,
5434
- overwrite: params.overwrite !== false,
5435
- relativeParentPath: params.relativeParentPath,
5436
- ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
5437
- };
5438
- try {
5439
- return await runPinnedPythonOperation({
5440
- operation: "write",
5441
- rootPath: params.rootPath,
5442
- payload,
5443
- });
5444
- }
5445
- catch (error) {
5446
- if (canFallbackFromPythonError(error)) {
5447
- return await runPinnedWriteFallback({ ...params, input });
5448
- }
5449
- throw error;
5391
+ const native = getNativeBinding();
5392
+ if (native && params.overwrite === false) {
5393
+ return await runPinnedWriteNative(native, params);
5450
5394
  }
5395
+ return await runPinnedWriteFallback(params);
5451
5396
  }
5452
5397
  async function runPinnedWriteWithRenamePolicy(params) {
5453
5398
  const { targetPath, renameIdentity, ...writeParams } = params;
@@ -5470,34 +5415,19 @@ async function runPinnedWriteWithRenamePolicy(params) {
5470
5415
  onRenameIdentityMismatch: "verify-content",
5471
5416
  }));
5472
5417
  }
5473
- async function runPinnedCopyHelper(params) {
5474
- assertSafeBasename(params.basename);
5475
- validatePinnedOperationPayload({
5476
- relativeParentPath: params.relativeParentPath,
5477
- });
5478
- return await runPinnedPythonOperation({
5479
- operation: "copy",
5480
- rootPath: params.rootPath,
5481
- payload: {
5482
- basename: params.basename,
5483
- maxBytes: params.maxBytes ?? -1,
5484
- mkdir: params.mkdir,
5485
- mode: params.mode || 0o600,
5486
- overwrite: params.overwrite !== false,
5487
- relativeParentPath: params.relativeParentPath,
5488
- ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
5489
- sourceDev: params.sourceIdentity.dev,
5490
- sourceIno: params.sourceIdentity.ino,
5491
- sourcePath: params.sourcePath,
5492
- },
5493
- });
5494
- }
5495
5418
  async function runPinnedWriteFallback(params) {
5496
- const parentPath = params.relativeParentPath
5419
+ let parentPath = params.relativeParentPath
5497
5420
  ? external_node_path_.join(params.rootPath, ...params.relativeParentPath.split("/"))
5498
5421
  : params.rootPath;
5499
5422
  if (params.mkdir) {
5500
- await mkdirPathComponentsWithGuards({ rootReal: params.rootPath, targetPath: parentPath });
5423
+ // mkdirPathComponentsWithGuards may resolve the final component through
5424
+ // an in-root symlink (e.g. a skill-bank layout). Use its returned real
5425
+ // path for the subsequent guard and target path so we don't re-check the
5426
+ // original, possibly-symlinked, lexical path and reject it outright.
5427
+ parentPath = await mkdirPathComponentsWithGuards({
5428
+ rootReal: params.rootPath,
5429
+ targetPath: parentPath,
5430
+ });
5501
5431
  }
5502
5432
  const parentGuard = params.mkdir
5503
5433
  ? await directory_guard_createAsyncDirectoryGuard(parentPath)
@@ -5513,8 +5443,9 @@ async function runPinnedWriteFallback(params) {
5513
5443
  });
5514
5444
  let created = true;
5515
5445
  try {
5446
+ await handle.chmod(params.mode);
5516
5447
  if (params.input.kind === "buffer") {
5517
- assertWithinMaxBytes(pinned_write_byteLength(params.input.data, params.input.encoding), params.maxBytes);
5448
+ pinned_write_assertWithinMaxBytes(pinned_write_byteLength(params.input.data, params.input.encoding), params.maxBytes);
5518
5449
  if (typeof params.input.data === "string") {
5519
5450
  await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
5520
5451
  }
@@ -5552,8 +5483,9 @@ async function runPinnedWriteFallback(params) {
5552
5483
  let renamed = false;
5553
5484
  try {
5554
5485
  handle = await promises_.open(tempPath, tempFlags, params.mode);
5486
+ await handle.chmod(params.mode);
5555
5487
  if (params.input.kind === "buffer") {
5556
- assertWithinMaxBytes(pinned_write_byteLength(params.input.data, params.input.encoding), params.maxBytes);
5488
+ pinned_write_assertWithinMaxBytes(pinned_write_byteLength(params.input.data, params.input.encoding), params.maxBytes);
5557
5489
  if (typeof params.input.data === "string") {
5558
5490
  await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
5559
5491
  }
@@ -5636,12 +5568,85 @@ async function runPinnedWriteFallback(params) {
5636
5568
 
5637
5569
  // EXTERNAL MODULE: external "node:os"
5638
5570
  var external_node_os_ = __webpack_require__(8161);
5571
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path-existing.js
5572
+
5573
+
5574
+
5575
+
5576
+ function isFilesystemRoot(candidate) {
5577
+ return external_node_path_.parse(candidate).root === candidate;
5578
+ }
5579
+ async function root_path_existing_pathExists(targetPath) {
5580
+ try {
5581
+ await promises_.lstat(targetPath);
5582
+ return true;
5583
+ }
5584
+ catch (error) {
5585
+ if (path_isNotFoundPathError(error)) {
5586
+ return false;
5587
+ }
5588
+ throw error;
5589
+ }
5590
+ }
5591
+ async function root_path_existing_resolvePathViaExistingAncestor(targetPath) {
5592
+ const normalized = external_node_path_.resolve(targetPath);
5593
+ let cursor = normalized;
5594
+ const missingSuffix = [];
5595
+ while (!isFilesystemRoot(cursor) && !(await root_path_existing_pathExists(cursor))) {
5596
+ missingSuffix.unshift(external_node_path_.basename(cursor));
5597
+ const parent = external_node_path_.dirname(cursor);
5598
+ if (parent === cursor) {
5599
+ break;
5600
+ }
5601
+ cursor = parent;
5602
+ }
5603
+ if (!(await root_path_existing_pathExists(cursor))) {
5604
+ return normalized;
5605
+ }
5606
+ try {
5607
+ const resolvedAncestor = external_node_path_.resolve(await promises_.realpath(cursor));
5608
+ return missingSuffix.length === 0
5609
+ ? resolvedAncestor
5610
+ : external_node_path_.resolve(resolvedAncestor, ...missingSuffix);
5611
+ }
5612
+ catch {
5613
+ return normalized;
5614
+ }
5615
+ }
5616
+ function root_path_existing_resolvePathViaExistingAncestorSync(targetPath) {
5617
+ const normalized = path.resolve(targetPath);
5618
+ let cursor = normalized;
5619
+ const missingSuffix = [];
5620
+ while (!isFilesystemRoot(cursor) && !fs.existsSync(cursor)) {
5621
+ missingSuffix.unshift(path.basename(cursor));
5622
+ const parent = path.dirname(cursor);
5623
+ if (parent === cursor) {
5624
+ break;
5625
+ }
5626
+ cursor = parent;
5627
+ }
5628
+ if (!fs.existsSync(cursor)) {
5629
+ return normalized;
5630
+ }
5631
+ try {
5632
+ const resolvedAncestor = path.resolve(fs.realpathSync(cursor));
5633
+ return missingSuffix.length === 0
5634
+ ? resolvedAncestor
5635
+ : path.resolve(resolvedAncestor, ...missingSuffix);
5636
+ }
5637
+ catch {
5638
+ return normalized;
5639
+ }
5640
+ }
5641
+
5639
5642
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path.js
5640
5643
 
5641
5644
 
5642
5645
 
5643
5646
 
5644
5647
 
5648
+
5649
+
5645
5650
  const ROOT_PATH_ALIAS_POLICIES = {
5646
5651
  strict: Object.freeze({
5647
5652
  allowFinalSymlinkForUnlink: false,
@@ -5657,7 +5662,7 @@ async function resolveRootPath(params) {
5657
5662
  const absolutePath = external_node_path_.resolve(params.absolutePath);
5658
5663
  const rootCanonicalPath = params.rootCanonicalPath
5659
5664
  ? external_node_path_.resolve(params.rootCanonicalPath)
5660
- : await root_path_resolvePathViaExistingAncestor(rootPath);
5665
+ : await root_path_existing_resolvePathViaExistingAncestor(rootPath);
5661
5666
  const context = createBoundaryResolutionContext({
5662
5667
  resolveParams: params,
5663
5668
  rootPath,
@@ -5719,15 +5724,40 @@ function isPromiseLike(value) {
5719
5724
  typeof value.then === "function");
5720
5725
  }
5721
5726
  function createLexicalTraversalState(params) {
5722
- const relative = external_node_path_.relative(params.rootPath, params.absolutePath);
5727
+ const rawAbsolutePath = params.params.absolutePath;
5728
+ const rawRelativePath = rawPathRelativeToRoot(params.rootPath, rawAbsolutePath);
5729
+ const relative = rawRelativePath ?? external_node_path_.relative(params.rootPath, params.absolutePath);
5723
5730
  return {
5724
- segments: relative.split(external_node_path_.sep).filter(Boolean),
5731
+ segments: splitTraversalSegments(relative),
5725
5732
  allowFinalSymlink: params.params.policy?.allowFinalSymlinkForUnlink === true,
5726
5733
  canonicalCursor: params.rootCanonicalPath,
5727
5734
  lexicalCursor: params.rootPath,
5728
5735
  preserveFinalSymlink: false,
5729
5736
  };
5730
5737
  }
5738
+ function splitTraversalSegments(value) {
5739
+ return value
5740
+ .split(process.platform === "win32" ? /[\\/]+/ : /\/+/)
5741
+ .filter((segment) => Boolean(segment) && segment !== ".");
5742
+ }
5743
+ function rawPathRelativeToRoot(rootPath, candidatePath) {
5744
+ if (!external_node_path_.isAbsolute(candidatePath)) {
5745
+ return undefined;
5746
+ }
5747
+ const root = external_node_path_.resolve(rootPath);
5748
+ const candidate = process.platform === "win32"
5749
+ ? candidatePath.replaceAll("/", external_node_path_.sep)
5750
+ : candidatePath;
5751
+ if (candidate === root) {
5752
+ return "";
5753
+ }
5754
+ const rootWithSep = root.endsWith(external_node_path_.sep) ? root : `${root}${external_node_path_.sep}`;
5755
+ const candidatePrefix = candidate.slice(0, rootWithSep.length);
5756
+ const prefixMatches = process.platform === "win32"
5757
+ ? candidatePrefix.toLowerCase() === rootWithSep.toLowerCase()
5758
+ : candidatePrefix === rootWithSep;
5759
+ return prefixMatches ? candidate.slice(rootWithSep.length) : undefined;
5760
+ }
5731
5761
  function assertLexicalCursorInsideBoundary(params) {
5732
5762
  assertInsideBoundary({
5733
5763
  boundaryLabel: params.params.boundaryLabel,
@@ -5738,13 +5768,15 @@ function assertLexicalCursorInsideBoundary(params) {
5738
5768
  }
5739
5769
  function applyMissingSuffixToCanonicalCursor(params) {
5740
5770
  const missingSuffix = params.state.segments.slice(params.missingFromIndex);
5741
- params.state.canonicalCursor = external_node_path_.resolve(params.state.canonicalCursor, ...missingSuffix);
5742
- assertLexicalCursorInsideBoundary({
5743
- params: params.params,
5744
- rootCanonicalPath: params.rootCanonicalPath,
5745
- candidatePath: params.state.canonicalCursor,
5746
- absolutePath: params.absolutePath,
5747
- });
5771
+ for (const segment of missingSuffix) {
5772
+ advanceCanonicalCursorForSegment({
5773
+ state: params.state,
5774
+ segment,
5775
+ rootCanonicalPath: params.rootCanonicalPath,
5776
+ params: params.params,
5777
+ absolutePath: params.absolutePath,
5778
+ });
5779
+ }
5748
5780
  }
5749
5781
  function advanceCanonicalCursorForSegment(params) {
5750
5782
  params.state.canonicalCursor = external_node_path_.resolve(params.state.canonicalCursor, params.segment);
@@ -5860,11 +5892,20 @@ function resolveAndApplySymlinkHop(params) {
5860
5892
  boundaryLabel: params.boundaryLabel,
5861
5893
  });
5862
5894
  }
5895
+ function applyParentTraversalStep(params) {
5896
+ params.state.lexicalCursor = external_node_path_.resolve(params.state.lexicalCursor, "..");
5897
+ advanceCanonicalCursorForSegment({
5898
+ state: params.state,
5899
+ segment: "..",
5900
+ rootCanonicalPath: params.rootCanonicalPath,
5901
+ params: params.resolveParams,
5902
+ absolutePath: params.absolutePath,
5903
+ });
5904
+ }
5863
5905
  function* iterateLexicalTraversal(state) {
5864
5906
  for (let idx = 0; idx < state.segments.length; idx += 1) {
5865
5907
  const segment = state.segments[idx] ?? "";
5866
5908
  const isLast = idx === state.segments.length - 1;
5867
- state.lexicalCursor = external_node_path_.join(state.lexicalCursor, segment);
5868
5909
  yield { idx, segment, isLast };
5869
5910
  }
5870
5911
  }
@@ -5877,6 +5918,14 @@ async function resolveRootPathLexicalAsync(params) {
5877
5918
  absolutePath: params.absolutePath,
5878
5919
  };
5879
5920
  for (const { idx, segment, isLast } of iterateLexicalTraversal(state)) {
5921
+ if (segment === "..") {
5922
+ applyParentTraversalStep({
5923
+ ...sharedStepParams,
5924
+ resolveParams: params.params,
5925
+ });
5926
+ continue;
5927
+ }
5928
+ state.lexicalCursor = external_node_path_.join(state.lexicalCursor, segment);
5880
5929
  const stat = await readLexicalStat({
5881
5930
  ...sharedStepParams,
5882
5931
  missingFromIndex: idx,
@@ -5904,7 +5953,7 @@ async function resolveRootPathLexicalAsync(params) {
5904
5953
  resolveLinkCanonical: (cursor) => resolveSymlinkHopPath(cursor),
5905
5954
  });
5906
5955
  }
5907
- const kind = await getPathKind(params.absolutePath, state.preserveFinalSymlink);
5956
+ const kind = await getPathKind(state.canonicalCursor, state.preserveFinalSymlink);
5908
5957
  return finalizeLexicalResolution({
5909
5958
  ...params,
5910
5959
  state,
@@ -5916,6 +5965,15 @@ function resolveRootPathLexicalSync(params) {
5916
5965
  for (let idx = 0; idx < state.segments.length; idx += 1) {
5917
5966
  const segment = state.segments[idx] ?? "";
5918
5967
  const isLast = idx === state.segments.length - 1;
5968
+ if (segment === "..") {
5969
+ applyParentTraversalStep({
5970
+ state,
5971
+ rootCanonicalPath: params.rootCanonicalPath,
5972
+ resolveParams: params.params,
5973
+ absolutePath: params.absolutePath,
5974
+ });
5975
+ continue;
5976
+ }
5919
5977
  state.lexicalCursor = path.join(state.lexicalCursor, segment);
5920
5978
  const maybeStat = readLexicalStat({
5921
5979
  state,
@@ -5957,7 +6015,7 @@ function resolveRootPathLexicalSync(params) {
5957
6015
  throw new Error("Unexpected async symlink resolution");
5958
6016
  }
5959
6017
  }
5960
- const kind = getPathKindSync(params.absolutePath, state.preserveFinalSymlink);
6018
+ const kind = getPathKindSync(state.canonicalCursor, state.preserveFinalSymlink);
5961
6019
  return finalizeLexicalResolution({
5962
6020
  ...params,
5963
6021
  state,
@@ -6026,7 +6084,7 @@ async function resolveOutsideLexicalCanonicalPathAsync(params) {
6026
6084
  if (path_isPathInside(params.rootPath, params.absolutePath)) {
6027
6085
  return undefined;
6028
6086
  }
6029
- return await root_path_resolvePathViaExistingAncestor(params.absolutePath);
6087
+ return await root_path_existing_resolvePathViaExistingAncestor(params.absolutePath);
6030
6088
  }
6031
6089
  function resolveOutsideLexicalCanonicalPathSync(params) {
6032
6090
  if (isPathInside(params.rootPath, params.absolutePath)) {
@@ -6073,60 +6131,6 @@ function buildResolvedRootPath(params) {
6073
6131
  kind: params.kind.kind,
6074
6132
  };
6075
6133
  }
6076
- async function root_path_resolvePathViaExistingAncestor(targetPath) {
6077
- const normalized = external_node_path_.resolve(targetPath);
6078
- let cursor = normalized;
6079
- const missingSuffix = [];
6080
- while (!isFilesystemRoot(cursor) && !(await root_path_pathExists(cursor))) {
6081
- missingSuffix.unshift(external_node_path_.basename(cursor));
6082
- const parent = external_node_path_.dirname(cursor);
6083
- if (parent === cursor) {
6084
- break;
6085
- }
6086
- cursor = parent;
6087
- }
6088
- if (!(await root_path_pathExists(cursor))) {
6089
- return normalized;
6090
- }
6091
- try {
6092
- const resolvedAncestor = external_node_path_.resolve(await promises_.realpath(cursor));
6093
- if (missingSuffix.length === 0) {
6094
- return resolvedAncestor;
6095
- }
6096
- return external_node_path_.resolve(resolvedAncestor, ...missingSuffix);
6097
- }
6098
- catch {
6099
- return normalized;
6100
- }
6101
- }
6102
- function resolvePathViaExistingAncestorSync(targetPath) {
6103
- const normalized = path.resolve(targetPath);
6104
- let cursor = normalized;
6105
- const missingSuffix = [];
6106
- while (!isFilesystemRoot(cursor) && !fs.existsSync(cursor)) {
6107
- missingSuffix.unshift(path.basename(cursor));
6108
- const parent = path.dirname(cursor);
6109
- if (parent === cursor) {
6110
- break;
6111
- }
6112
- cursor = parent;
6113
- }
6114
- if (!fs.existsSync(cursor)) {
6115
- return normalized;
6116
- }
6117
- try {
6118
- // Keep sync behavior aligned with async (`fsp.realpath`) to avoid
6119
- // platform-specific canonical alias drift (notably on Windows).
6120
- const resolvedAncestor = path.resolve(fs.realpathSync(cursor));
6121
- if (missingSuffix.length === 0) {
6122
- return resolvedAncestor;
6123
- }
6124
- return path.resolve(resolvedAncestor, ...missingSuffix);
6125
- }
6126
- catch {
6127
- return normalized;
6128
- }
6129
- }
6130
6134
  async function getPathKind(absolutePath, preserveFinalSymlink) {
6131
6135
  try {
6132
6136
  const stat = preserveFinalSymlink
@@ -6194,21 +6198,6 @@ function shortPath(value) {
6194
6198
  }
6195
6199
  return value;
6196
6200
  }
6197
- function isFilesystemRoot(candidate) {
6198
- return external_node_path_.parse(candidate).root === candidate;
6199
- }
6200
- async function root_path_pathExists(targetPath) {
6201
- try {
6202
- await promises_.lstat(targetPath);
6203
- return true;
6204
- }
6205
- catch (error) {
6206
- if (path_isNotFoundPathError(error)) {
6207
- return false;
6208
- }
6209
- throw error;
6210
- }
6211
- }
6212
6201
  async function resolveSymlinkHopPath(symlinkPath) {
6213
6202
  try {
6214
6203
  return external_node_path_.resolve(await promises_.realpath(symlinkPath));
@@ -6219,7 +6208,7 @@ async function resolveSymlinkHopPath(symlinkPath) {
6219
6208
  }
6220
6209
  const linkTarget = await promises_.readlink(symlinkPath);
6221
6210
  const linkAbsolute = external_node_path_.resolve(external_node_path_.dirname(symlinkPath), linkTarget);
6222
- return root_path_resolvePathViaExistingAncestor(linkAbsolute);
6211
+ return root_path_existing_resolvePathViaExistingAncestor(linkAbsolute);
6223
6212
  }
6224
6213
  }
6225
6214
  function resolveSymlinkHopPathSync(symlinkPath) {
@@ -6254,7 +6243,7 @@ async function assertNoPathAliasEscape(params) {
6254
6243
  return;
6255
6244
  }
6256
6245
  await assertNoHardlinkedFinalPath({
6257
- filePath: resolved.absolutePath,
6246
+ filePath: resolved.canonicalPath,
6258
6247
  root: resolved.rootPath,
6259
6248
  boundaryLabel: params.boundaryLabel,
6260
6249
  allowFinalHardlinkForUnlink: params.policy?.allowFinalHardlinkForUnlink,
@@ -6288,8 +6277,80 @@ function path_policy_shortPath(value) {
6288
6277
  return value;
6289
6278
  }
6290
6279
 
6291
- // EXTERNAL MODULE: external "node:url"
6292
- var external_node_url_ = __webpack_require__(3136);
6280
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/string-coerce.js
6281
+ function readStringValue(value) {
6282
+ return typeof value === "string" ? value : undefined;
6283
+ }
6284
+ function normalizeNullableString(value) {
6285
+ if (typeof value !== "string") {
6286
+ return null;
6287
+ }
6288
+ const trimmed = value.trim();
6289
+ return trimmed ? trimmed : null;
6290
+ }
6291
+ function normalizeOptionalString(value) {
6292
+ return normalizeNullableString(value) ?? undefined;
6293
+ }
6294
+ function normalizeStringifiedOptionalString(value) {
6295
+ if (typeof value === "string") {
6296
+ return normalizeOptionalString(value);
6297
+ }
6298
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
6299
+ return normalizeOptionalString(String(value));
6300
+ }
6301
+ return undefined;
6302
+ }
6303
+ function normalizeOptionalLowercaseString(value) {
6304
+ return normalizeOptionalString(value)?.toLowerCase();
6305
+ }
6306
+ function normalizeLowercaseStringOrEmpty(value) {
6307
+ return normalizeOptionalLowercaseString(value) ?? "";
6308
+ }
6309
+ function normalizeFastMode(raw) {
6310
+ if (typeof raw === "boolean") {
6311
+ return raw;
6312
+ }
6313
+ if (!raw) {
6314
+ return undefined;
6315
+ }
6316
+ const key = normalizeLowercaseStringOrEmpty(raw);
6317
+ if (["off", "false", "no", "0", "disable", "disabled", "normal"].includes(key)) {
6318
+ return false;
6319
+ }
6320
+ if (["on", "true", "yes", "1", "enable", "enabled", "fast"].includes(key)) {
6321
+ return true;
6322
+ }
6323
+ return undefined;
6324
+ }
6325
+ function lowercasePreservingWhitespace(value) {
6326
+ return value.toLowerCase();
6327
+ }
6328
+ function localeLowercasePreservingWhitespace(value) {
6329
+ return value.toLocaleLowerCase();
6330
+ }
6331
+ function resolvePrimaryStringValue(value) {
6332
+ if (typeof value === "string") {
6333
+ return normalizeOptionalString(value);
6334
+ }
6335
+ if (!value || typeof value !== "object") {
6336
+ return undefined;
6337
+ }
6338
+ return normalizeOptionalString(value.primary);
6339
+ }
6340
+ function normalizeOptionalThreadValue(value) {
6341
+ if (typeof value === "number") {
6342
+ return Number.isFinite(value) ? Math.trunc(value) : undefined;
6343
+ }
6344
+ return normalizeOptionalString(value);
6345
+ }
6346
+ function normalizeOptionalStringifiedId(value) {
6347
+ const normalized = normalizeOptionalThreadValue(value);
6348
+ return normalized == null ? undefined : String(normalized);
6349
+ }
6350
+ function hasNonEmptyString(value) {
6351
+ return normalizeOptionalString(value) !== undefined;
6352
+ }
6353
+
6293
6354
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/local-file-access.js
6294
6355
 
6295
6356
 
@@ -6421,6 +6482,28 @@ const WINDOWS_RESERVED_DEVICE_NAMES = new Set([
6421
6482
  "LPT²",
6422
6483
  "LPT³",
6423
6484
  ]);
6485
+ const WINDOWS_SEPARATOR_CHAR_CODE = 0x5c;
6486
+ const WINDOWS_IGNORED_SPACE_CHAR_CODE = 0x20;
6487
+ const WINDOWS_IGNORED_DOT_CHAR_CODE = 0x2e;
6488
+ function trimTrailingWindowsSeparators(value) {
6489
+ let end = value.length;
6490
+ while (end > 0 && value.charCodeAt(end - 1) === WINDOWS_SEPARATOR_CHAR_CODE) {
6491
+ end -= 1;
6492
+ }
6493
+ return end === value.length ? value : value.slice(0, end);
6494
+ }
6495
+ function trimTrailingWindowsIgnoredChars(value) {
6496
+ let end = value.length;
6497
+ while (end > 0) {
6498
+ const charCode = value.charCodeAt(end - 1);
6499
+ if (charCode !== WINDOWS_IGNORED_SPACE_CHAR_CODE &&
6500
+ charCode !== WINDOWS_IGNORED_DOT_CHAR_CODE) {
6501
+ break;
6502
+ }
6503
+ end -= 1;
6504
+ }
6505
+ return end === value.length ? value : value.slice(0, end);
6506
+ }
6424
6507
  function candidateReadPaths(filePath) {
6425
6508
  if (!filePath.startsWith("file://")) {
6426
6509
  return [filePath];
@@ -6449,10 +6532,10 @@ function matchPosixDeviceReadPath(filePath, cwd) {
6449
6532
  return undefined;
6450
6533
  }
6451
6534
  function normalizeWindowsDeviceBaseName(filePath) {
6452
- const normalized = filePath.replace(/\//g, "\\").replace(/[\\]+$/g, "");
6535
+ const normalized = trimTrailingWindowsSeparators(filePath.replace(/\//g, "\\"));
6453
6536
  const lastSegment = normalized.split("\\").filter(Boolean).at(-1) ?? normalized;
6454
6537
  const withoutStream = lastSegment.split(":")[0] ?? lastSegment;
6455
- const withoutTrailingIgnoredChars = withoutStream.replace(/[ .]+$/g, "");
6538
+ const withoutTrailingIgnoredChars = trimTrailingWindowsIgnoredChars(withoutStream);
6456
6539
  return (withoutTrailingIgnoredChars.split(".")[0] ?? withoutTrailingIgnoredChars).toUpperCase();
6457
6540
  }
6458
6541
  function matchWindowsDeviceReadPath(filePath) {
@@ -6487,94 +6570,6 @@ function assertNoUnsafeDeviceReadPath(filePath, options) {
6487
6570
  }
6488
6571
  }
6489
6572
 
6490
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/bounded-read.js
6491
-
6492
-
6493
- const READ_CHUNK_BYTES = 64 * 1024;
6494
- function assertMaxBytes(maxBytes) {
6495
- if (maxBytes === Number.POSITIVE_INFINITY) {
6496
- return;
6497
- }
6498
- if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
6499
- throw new RangeError("maxBytes must be a non-negative safe integer or Infinity");
6500
- }
6501
- }
6502
- function createScratchBuffer(maxBytes) {
6503
- const initialReadBytes = Number.isFinite(maxBytes)
6504
- ? Math.min(READ_CHUNK_BYTES, maxBytes + 1)
6505
- : READ_CHUNK_BYTES;
6506
- return Buffer.allocUnsafe(Math.max(1, initialReadBytes));
6507
- }
6508
- function nextReadLength(total, maxBytes, capacity) {
6509
- return Number.isFinite(maxBytes)
6510
- ? Math.min(capacity, maxBytes - total + 1)
6511
- : capacity;
6512
- }
6513
- function appendChunk(params) {
6514
- const total = params.total + params.bytesRead;
6515
- if (total > params.maxBytes) {
6516
- throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got at least ${total})`);
6517
- }
6518
- params.chunks.push(Buffer.from(params.scratch.subarray(0, params.bytesRead)));
6519
- return total;
6520
- }
6521
- async function readBoundedAsync(maxBytes, readChunk) {
6522
- assertMaxBytes(maxBytes);
6523
- const chunks = [];
6524
- const scratch = createScratchBuffer(maxBytes);
6525
- let total = 0;
6526
- while (true) {
6527
- const length = nextReadLength(total, maxBytes, scratch.length);
6528
- const bytesRead = await readChunk(scratch, length);
6529
- if (bytesRead === 0) {
6530
- return Buffer.concat(chunks, total);
6531
- }
6532
- total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
6533
- }
6534
- }
6535
- /**
6536
- * Reads from the handle's current offset without closing it. A bounded read
6537
- * consumes at most maxBytes + 1 bytes so growth after an earlier stat cannot
6538
- * force an unbounded allocation.
6539
- */
6540
- async function readFileHandleBounded(handle, maxBytes) {
6541
- return await readBoundedAsync(maxBytes, async (scratch, length) => {
6542
- return (await handle.read(scratch, 0, length, null)).bytesRead;
6543
- });
6544
- }
6545
- function readDescriptorChunk(fd, scratch, length) {
6546
- return new Promise((resolve, reject) => {
6547
- fs.read(fd, scratch, 0, length, null, (error, bytesRead) => {
6548
- if (error) {
6549
- reject(error);
6550
- return;
6551
- }
6552
- resolve(bytesRead);
6553
- });
6554
- });
6555
- }
6556
- /** Async bounded read from a numeric descriptor. The caller owns the descriptor. */
6557
- async function readFileDescriptorBounded(fd, maxBytes) {
6558
- return await readBoundedAsync(maxBytes, async (scratch, length) => {
6559
- return await readDescriptorChunk(fd, scratch, length);
6560
- });
6561
- }
6562
- /** Sync bounded read from a numeric descriptor. The caller owns the descriptor. */
6563
- function readFileDescriptorBoundedSync(fd, maxBytes) {
6564
- assertMaxBytes(maxBytes);
6565
- const chunks = [];
6566
- const scratch = createScratchBuffer(maxBytes);
6567
- let total = 0;
6568
- while (true) {
6569
- const length = nextReadLength(total, maxBytes, scratch.length);
6570
- const bytesRead = fs.readSync(fd, scratch, 0, length, null);
6571
- if (bytesRead === 0) {
6572
- return Buffer.concat(chunks, total);
6573
- }
6574
- total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
6575
- }
6576
- }
6577
-
6578
6573
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/read-opened-file.js
6579
6574
 
6580
6575
 
@@ -6587,6 +6582,7 @@ async function read_opened_file_readOpenedFileSafely(params) {
6587
6582
  : await readFileHandleBounded(params.opened.handle, params.maxBytes);
6588
6583
  return {
6589
6584
  buffer,
6585
+ containment: params.opened.containment,
6590
6586
  realPath: params.opened.realPath,
6591
6587
  stat: params.opened.stat,
6592
6588
  };
@@ -6730,6 +6726,7 @@ function resolveOsHomeRelativePath(input, opts) {
6730
6726
 
6731
6727
 
6732
6728
 
6729
+
6733
6730
  const ensureTrailingSep = (value) => value.endsWith(external_node_path_.sep) ? value : value + external_node_path_.sep;
6734
6731
  function assertValidRootRelativePath(relativePath) {
6735
6732
  path_assertNoNulPathInput(relativePath, "relative path contains a NUL byte");
@@ -6774,13 +6771,31 @@ async function resolveRootContext(rootDir) {
6774
6771
  rootWithSep: ensureTrailingSep(rootReal),
6775
6772
  };
6776
6773
  }
6777
- async function resolvePathInRoot(root, relativePath) {
6774
+ async function resolvePathInRoot(root, relativePath, options) {
6778
6775
  assertValidRootRelativePath(relativePath);
6779
6776
  const expanded = await expandRelativePathWithHome(relativePath);
6780
6777
  const resolved = external_node_path_.resolve(root.rootWithSep, expanded);
6781
6778
  if (!path_isPathInside(root.rootWithSep, resolved)) {
6782
6779
  throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
6783
6780
  }
6781
+ const rawAbsolutePath = external_node_path_.isAbsolute(expanded)
6782
+ ? expanded
6783
+ : `${root.rootWithSep}${expanded}`;
6784
+ try {
6785
+ await resolveRootPath({
6786
+ absolutePath: rawAbsolutePath,
6787
+ rootPath: root.rootReal,
6788
+ rootCanonicalPath: root.rootReal,
6789
+ boundaryLabel: "root",
6790
+ policy: options?.allowFinalSymlink ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
6791
+ });
6792
+ }
6793
+ catch (error) {
6794
+ const code = options?.aliasErrorCode ?? "outside-workspace";
6795
+ throw new errors_FsSafeError(code, code === "path-alias" ? "path alias escape blocked" : "file is outside workspace root", {
6796
+ cause: error instanceof Error ? error : undefined,
6797
+ });
6798
+ }
6784
6799
  return { rootReal: root.rootReal, rootWithSep: root.rootWithSep, resolved };
6785
6800
  }
6786
6801
  async function resolvePathWithinRoot(params) {
@@ -6819,14 +6834,143 @@ function stringifyJsonDocument(value, replacer, space) {
6819
6834
  return text;
6820
6835
  }
6821
6836
 
6837
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-walk.js
6838
+
6839
+
6840
+
6841
+ function validateBudget(name, value) {
6842
+ if (value === undefined)
6843
+ return Number.POSITIVE_INFINITY;
6844
+ if (!Number.isSafeInteger(value) || value < 0) {
6845
+ throw new RangeError(`${name} must be a non-negative safe integer`);
6846
+ }
6847
+ return value;
6848
+ }
6849
+ function entryKind(entry) {
6850
+ if (entry.isSymbolicLink)
6851
+ return "symlink";
6852
+ if (entry.isDirectory)
6853
+ return "directory";
6854
+ if (entry.isFile)
6855
+ return "file";
6856
+ return "other";
6857
+ }
6858
+ function limitEntry(relativePath) {
6859
+ return { relativePath, kind: "truncated", size: 0 };
6860
+ }
6861
+ async function* walkRoot(root, relativePath, options) {
6862
+ const maxDepth = validateBudget("maxDepth", options.maxDepth);
6863
+ const maxEntries = validateBudget("maxEntries", options.maxEntries);
6864
+ const visitedDirectories = new Set();
6865
+ let examined = 0;
6866
+ const onLimit = (atPath) => {
6867
+ if ((options.limitBehavior ?? "truncate") === "throw") {
6868
+ throw new errors_FsSafeError("too-large", `root walk budget exceeded at ${atPath || "."}`);
6869
+ }
6870
+ return limitEntry(atPath);
6871
+ };
6872
+ async function* visit(directory, depth) {
6873
+ options.signal?.throwIfAborted();
6874
+ let entries;
6875
+ try {
6876
+ const resolvedDirectory = await resolveRootPath({
6877
+ absolutePath: external_node_path_.resolve(root.rootReal, directory),
6878
+ rootPath: root.rootReal,
6879
+ rootCanonicalPath: root.rootReal,
6880
+ boundaryLabel: "root walk",
6881
+ });
6882
+ if (!resolvedDirectory.exists || resolvedDirectory.kind !== "directory") {
6883
+ throw new errors_FsSafeError("not-file", `root walk path is not a directory: ${directory || "."}`);
6884
+ }
6885
+ if (visitedDirectories.has(resolvedDirectory.canonicalPath)) {
6886
+ return;
6887
+ }
6888
+ visitedDirectories.add(resolvedDirectory.canonicalPath);
6889
+ const listingDirectory = external_node_path_.relative(root.rootReal, resolvedDirectory.canonicalPath)
6890
+ .split(external_node_path_.sep)
6891
+ .join(external_node_path_.posix.sep);
6892
+ entries = await root.list(listingDirectory, { withFileTypes: true });
6893
+ }
6894
+ catch (error) {
6895
+ if ((options.onDirectoryError ?? "throw") === "throw")
6896
+ throw error;
6897
+ yield { relativePath: directory, kind: "directory-error", size: 0, error };
6898
+ return;
6899
+ }
6900
+ for (const entry of entries) {
6901
+ options.signal?.throwIfAborted();
6902
+ const child = directory
6903
+ ? external_node_path_.posix.join(directory.split(external_node_path_.sep).join(external_node_path_.posix.sep), entry.name)
6904
+ : entry.name;
6905
+ if (examined >= maxEntries) {
6906
+ yield onLimit(child);
6907
+ return;
6908
+ }
6909
+ examined += 1;
6910
+ let kind = entryKind(entry);
6911
+ let size = entry.size;
6912
+ if (kind === "symlink") {
6913
+ if (options.symlinkPolicy === "skip") {
6914
+ continue;
6915
+ }
6916
+ const resolved = await resolveRootPath({
6917
+ absolutePath: external_node_path_.resolve(root.rootReal, child),
6918
+ rootPath: root.rootReal,
6919
+ rootCanonicalPath: root.rootReal,
6920
+ boundaryLabel: "root walk",
6921
+ });
6922
+ if (!resolved.exists) {
6923
+ continue;
6924
+ }
6925
+ kind = resolved.kind === "directory" ? "directory" : resolved.kind === "file" ? "file" : "other";
6926
+ size = entry.size;
6927
+ }
6928
+ const walkEntry = { relativePath: child, kind, size };
6929
+ const filterResult = options.entryFilter?.(walkEntry) ?? "include";
6930
+ if (!["include", "skip", "skip-subtree"].includes(filterResult)) {
6931
+ throw new TypeError(`invalid root walk entryFilter result: ${String(filterResult)}`);
6932
+ }
6933
+ if (filterResult === "include") {
6934
+ yield walkEntry;
6935
+ }
6936
+ if (kind !== "directory") {
6937
+ continue;
6938
+ }
6939
+ if (filterResult === "skip-subtree") {
6940
+ continue;
6941
+ }
6942
+ if (depth >= maxDepth) {
6943
+ yield onLimit(child);
6944
+ return;
6945
+ }
6946
+ yield* visit(child, depth + 1);
6947
+ }
6948
+ }
6949
+ yield* visit(relativePath, 0);
6950
+ }
6951
+
6822
6952
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/temp-cleanup.js
6823
6953
 
6954
+
6824
6955
  const tempCleanupEntries = new Map();
6825
6956
  let cleanupRegistered = false;
6957
+ function pathStillMatchesReceipt(entry) {
6958
+ if (!entry.identity) {
6959
+ return false;
6960
+ }
6961
+ try {
6962
+ return file_identity_sameFileIdentity(external_node_fs_.lstatSync(entry.path), entry.identity);
6963
+ }
6964
+ catch (error) {
6965
+ return error.code === "ENOENT";
6966
+ }
6967
+ }
6826
6968
  function cleanupRegisteredTempPathsSync() {
6827
6969
  for (const entry of tempCleanupEntries.values()) {
6828
6970
  try {
6829
- external_node_fs_.rmSync(entry.path, { force: true, recursive: entry.recursive });
6971
+ if (pathStillMatchesReceipt(entry)) {
6972
+ external_node_fs_.rmSync(entry.path, { force: true, recursive: entry.recursive });
6973
+ }
6830
6974
  }
6831
6975
  catch {
6832
6976
  // Process-exit cleanup is best-effort.
@@ -6839,13 +6983,27 @@ function registerTempPathForExit(tempPath, options) {
6839
6983
  cleanupRegistered = true;
6840
6984
  process.once("exit", cleanupRegisteredTempPathsSync);
6841
6985
  }
6842
- tempCleanupEntries.set(tempPath, {
6986
+ const entry = {
6843
6987
  path: tempPath,
6844
6988
  recursive: options?.recursive === true,
6845
- });
6846
- return () => {
6989
+ identity: options?.identity,
6990
+ };
6991
+ if (!entry.identity) {
6992
+ try {
6993
+ entry.identity = external_node_fs_.lstatSync(tempPath);
6994
+ }
6995
+ catch {
6996
+ // Callers that register before creation set the identity after opening.
6997
+ }
6998
+ }
6999
+ tempCleanupEntries.set(tempPath, entry);
7000
+ const unregister = (() => {
6847
7001
  tempCleanupEntries.delete(tempPath);
7002
+ });
7003
+ unregister.setIdentity = (identity) => {
7004
+ entry.identity = identity;
6848
7005
  };
7006
+ return unregister;
6849
7007
  }
6850
7008
  function __cleanupRegisteredTempPathsForTest() {
6851
7009
  cleanupRegisteredTempPathsSync();
@@ -6856,7 +7014,9 @@ function __cleanupRegisteredTempPathForTest(tempPath) {
6856
7014
  return;
6857
7015
  }
6858
7016
  try {
6859
- fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
7017
+ if (pathStillMatchesReceipt(entry)) {
7018
+ fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
7019
+ }
6860
7020
  }
6861
7021
  finally {
6862
7022
  tempCleanupEntries.delete(tempPath);
@@ -6907,9 +7067,6 @@ async function serializePathWrite(key, run) {
6907
7067
 
6908
7068
 
6909
7069
 
6910
-
6911
-
6912
-
6913
7070
 
6914
7071
 
6915
7072
 
@@ -6940,6 +7097,7 @@ const DEFAULT_ROOT_MAX_BYTES = 16 * 1024 * 1024;
6940
7097
  function openResult(params) {
6941
7098
  return {
6942
7099
  handle: params.handle,
7100
+ containment: "best-effort",
6943
7101
  realPath: params.realPath,
6944
7102
  stat: params.stat,
6945
7103
  [Symbol.asyncDispose]: () => params.handle.close().catch(() => undefined),
@@ -7058,7 +7216,7 @@ class RootHandle {
7058
7216
  };
7059
7217
  }
7060
7218
  async resolve(relativePath) {
7061
- return (await resolvePathInRoot(this.context, relativePath)).resolved;
7219
+ return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true })).resolved;
7062
7220
  }
7063
7221
  async open(relativePath, options = {}) {
7064
7222
  return await openFileInRoot(this.context, {
@@ -7197,58 +7355,34 @@ class RootHandle {
7197
7355
  }
7198
7356
  async stat(relativePath) {
7199
7357
  assertValidRootRelativePath(relativePath);
7200
- try {
7201
- return await helperStat(this.rootReal, relativePath);
7202
- }
7203
- catch (error) {
7204
- if (canFallbackFromPythonError(error)) {
7205
- return await statPathFallback(this.context, relativePath);
7206
- }
7207
- throw error;
7208
- }
7358
+ validatePinnedOperationPayload({ relativePath });
7359
+ return await statPathFallback(this.context, relativePath);
7209
7360
  }
7210
7361
  async list(relativePath, options = {}) {
7211
7362
  assertValidRootRelativePath(relativePath);
7212
- try {
7213
- return options.withFileTypes === true
7214
- ? await helperReaddir(this.rootReal, relativePath, true)
7215
- : await helperReaddir(this.rootReal, relativePath, false);
7216
- }
7217
- catch (error) {
7218
- if (canFallbackFromPythonError(error)) {
7219
- return await listPathFallback(this.context, relativePath, options.withFileTypes === true);
7220
- }
7221
- throw error;
7222
- }
7363
+ validatePinnedOperationPayload({ relativePath });
7364
+ return await listPathFallback(this.context, relativePath, options.withFileTypes === true);
7223
7365
  }
7224
7366
  async move(fromRelative, toRelative, options = {}) {
7225
7367
  assertValidRootRelativePath(fromRelative);
7226
7368
  assertValidRootRelativePath(toRelative);
7369
+ validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
7227
7370
  const denyMutations = mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations);
7228
7371
  await assertMoveMutationAllowed(this.context, {
7229
7372
  fromRelative,
7230
7373
  toRelative,
7231
7374
  denyMutations,
7232
7375
  });
7233
- try {
7234
- await runPinnedHelper("rename", this.rootReal, {
7235
- from: fromRelative,
7236
- overwrite: options.overwrite ?? false,
7237
- to: toRelative,
7238
- });
7239
- }
7240
- catch (error) {
7241
- if (canFallbackFromPythonError(error)) {
7242
- await movePathFallback(this.context, {
7243
- fromRelative,
7244
- denyMutations,
7245
- overwrite: options.overwrite ?? false,
7246
- toRelative,
7247
- });
7248
- return;
7249
- }
7250
- throw error;
7251
- }
7376
+ await movePathFallback(this.context, {
7377
+ fromRelative,
7378
+ denyMutations,
7379
+ overwrite: options.overwrite ?? false,
7380
+ toRelative,
7381
+ });
7382
+ }
7383
+ walk(relativePath, options) {
7384
+ assertValidRootRelativePath(relativePath);
7385
+ return walkRoot(this, relativePath, options);
7252
7386
  }
7253
7387
  }
7254
7388
  function readDefaults(defaults) {
@@ -7263,7 +7397,9 @@ async function root_impl_root(rootDir, defaults = {}) {
7263
7397
  return new RootHandle(await resolveRootContext(rootDir), defaults);
7264
7398
  }
7265
7399
  async function openFileInRoot(root, params) {
7266
- const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath);
7400
+ const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, {
7401
+ allowFinalSymlink: true,
7402
+ });
7267
7403
  let opened;
7268
7404
  try {
7269
7405
  opened = await openVerifiedLocalFile(resolved, {
@@ -7334,6 +7470,15 @@ function buildAtomicWriteTempPath(targetPath) {
7334
7470
  function rootWriteQueueKey(root, relativePath) {
7335
7471
  return `${root.rootReal}\0${relativePath}`;
7336
7472
  }
7473
+ async function prepareRootWriteTarget(rootReal, targetPath) {
7474
+ const parentPath = await mkdirPathComponentsWithGuards({
7475
+ rootReal,
7476
+ targetPath: external_node_path_.dirname(targetPath),
7477
+ });
7478
+ // Continue through the guarded walk's real parent instead of re-entering
7479
+ // the original path through a symlinked component.
7480
+ return external_node_path_.join(parentPath, external_node_path_.basename(targetPath));
7481
+ }
7337
7482
  async function writeTempFileForAtomicReplace(params) {
7338
7483
  const tempHandle = await promises_.open(params.tempPath, OPEN_WRITE_CREATE_FLAGS, params.mode);
7339
7484
  try {
@@ -7364,7 +7509,7 @@ async function verifyAtomicWriteResult(params) {
7364
7509
  }
7365
7510
  }
7366
7511
  async function openWritableFileInRoot(root, params) {
7367
- const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath);
7512
+ const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, { aliasErrorCode: "path-alias" });
7368
7513
  await assertMutationNotDenied(resolved, params.denyMutations);
7369
7514
  try {
7370
7515
  await assertNoPathAliasEscape({
@@ -7376,15 +7521,11 @@ async function openWritableFileInRoot(root, params) {
7376
7521
  catch (err) {
7377
7522
  throw new errors_FsSafeError("path-alias", "path alias escape blocked", { cause: err });
7378
7523
  }
7379
- if (params.mkdir !== false) {
7380
- const parentGuard = await directory_guard_createNearestExistingDirectoryGuard(rootReal, external_node_path_.dirname(resolved));
7381
- await withAsyncDirectoryGuards([parentGuard], async () => {
7382
- await promises_.mkdir(external_node_path_.dirname(resolved), { recursive: true });
7383
- });
7384
- }
7385
- let ioPath = resolved;
7524
+ let ioPath = params.mkdir === false
7525
+ ? resolved
7526
+ : await prepareRootWriteTarget(rootReal, resolved);
7386
7527
  try {
7387
- const resolvedRealPath = await promises_.realpath(resolved);
7528
+ const resolvedRealPath = await promises_.realpath(ioPath);
7388
7529
  if (!path_isPathInside(rootWithSep, resolvedRealPath)) {
7389
7530
  throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
7390
7531
  }
@@ -7469,6 +7610,7 @@ async function openWritableFileInRoot(root, params) {
7469
7610
  }
7470
7611
  return {
7471
7612
  handle,
7613
+ containment: "best-effort",
7472
7614
  createdForWrite,
7473
7615
  realPath,
7474
7616
  stat,
@@ -7524,44 +7666,22 @@ async function appendFileInRoot(root, params) {
7524
7666
  }
7525
7667
  }
7526
7668
  async function removePathInRoot(root, params) {
7669
+ validatePinnedOperationPayload({ relativePath: params.relativePath });
7527
7670
  const resolved = await resolvePinnedRemovePathInRoot(root, params.relativePath, params.denyMutations);
7528
- if (process.platform === "win32") {
7529
- await removePathFallback(resolved);
7530
- return;
7531
- }
7532
7671
  try {
7533
- await runPinnedPathHelper({
7534
- operation: "remove",
7535
- rootPath: resolved.rootReal,
7536
- relativePath: resolved.relativePosix,
7537
- });
7672
+ await removePathFallback(resolved);
7538
7673
  }
7539
7674
  catch (error) {
7540
- if (isPinnedPathHelperSpawnError(error)) {
7541
- await removePathFallback(resolved);
7542
- return;
7543
- }
7544
7675
  throw normalizePinnedPathError(error);
7545
7676
  }
7546
7677
  }
7547
7678
  async function mkdirPathInRoot(root, params) {
7679
+ validatePinnedOperationPayload({ relativePath: params.relativePath });
7548
7680
  const resolved = await resolvePinnedPathInRoot(root, params);
7549
- if (process.platform === "win32") {
7550
- await mkdirPathFallback(resolved);
7551
- return;
7552
- }
7553
7681
  try {
7554
- await runPinnedPathHelper({
7555
- operation: "mkdirp",
7556
- rootPath: resolved.rootReal,
7557
- relativePath: resolved.relativePosix,
7558
- });
7682
+ await mkdirPathFallback(resolved);
7559
7683
  }
7560
7684
  catch (error) {
7561
- if (isPinnedPathHelperSpawnError(error)) {
7562
- await mkdirPathFallback(resolved);
7563
- return;
7564
- }
7565
7685
  throw normalizePinnedPathError(error);
7566
7686
  }
7567
7687
  }
@@ -7627,49 +7747,25 @@ async function copyFileInRoot(root, params) {
7627
7747
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
7628
7748
  }
7629
7749
  try {
7630
- if (process.platform === "win32") {
7631
- await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
7632
- await copyFileFallback(root, params, source);
7633
- });
7634
- return;
7635
- }
7636
7750
  const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
7637
7751
  await serializePathWrite(pinned.targetPath, async () => {
7638
- let identity;
7752
+ await assertCopySourceCurrent(source);
7753
+ const identity = await runPinnedWriteHelper({
7754
+ rootPath: pinned.rootReal,
7755
+ relativeParentPath: pinned.relativeParentPath,
7756
+ basename: pinned.basename,
7757
+ mkdir: params.mkdir !== false,
7758
+ mode: pinned.mode,
7759
+ overwrite: true,
7760
+ maxBytes: params.maxBytes,
7761
+ input: { kind: "stream", stream: source.handle.createReadStream() },
7762
+ });
7639
7763
  try {
7640
- if (getFsSafePythonConfig().mode === "off") {
7641
- await copyFileFallback(root, params, source);
7642
- return;
7643
- }
7644
- identity = await runPinnedCopyHelper({
7645
- rootPath: pinned.rootReal,
7646
- relativeParentPath: pinned.relativeParentPath,
7647
- basename: pinned.basename,
7648
- mkdir: params.mkdir !== false,
7649
- mode: pinned.mode,
7650
- overwrite: true,
7651
- maxBytes: params.maxBytes,
7652
- sourcePath: source.realPath,
7653
- sourceIdentity: { dev: source.stat.dev, ino: source.stat.ino },
7654
- });
7764
+ await assertCopySourcePathCurrent(source);
7655
7765
  }
7656
7766
  catch (error) {
7657
- if (canFallbackFromPythonError(error)) {
7658
- await copyFileFallback(root, params, source);
7659
- return;
7660
- }
7661
- throw normalizePinnedWriteError(error);
7662
- }
7663
- try {
7664
- await verifyAtomicWriteResult({
7665
- root,
7666
- targetPath: pinned.targetPath,
7667
- expectedIdentity: identity,
7668
- });
7669
- }
7670
- catch (err) {
7671
- emitWriteBoundaryWarning(`post-copy verification failed: ${String(err)}`);
7672
- throw err;
7767
+ await removeCopyTargetIfUnchanged(pinned.targetPath, identity).catch(() => undefined);
7768
+ throw error;
7673
7769
  }
7674
7770
  });
7675
7771
  }
@@ -7677,8 +7773,33 @@ async function copyFileInRoot(root, params) {
7677
7773
  await source.handle.close().catch(() => { });
7678
7774
  }
7679
7775
  }
7776
+ async function assertCopySourceCurrent(source) {
7777
+ const opened = await source.handle.stat();
7778
+ if (!file_identity_sameFileIdentity(opened, source.stat)) {
7779
+ throw new errors_FsSafeError("path-mismatch", "copy source descriptor changed");
7780
+ }
7781
+ await assertCopySourcePathCurrent(source);
7782
+ }
7783
+ async function assertCopySourcePathCurrent(source) {
7784
+ const current = await promises_.lstat(source.realPath);
7785
+ if (current.isSymbolicLink() || !current.isFile() || !file_identity_sameFileIdentity(current, source.stat)) {
7786
+ throw new errors_FsSafeError("path-mismatch", "copy source path changed");
7787
+ }
7788
+ }
7789
+ async function removeCopyTargetIfUnchanged(targetPath, identity) {
7790
+ const parentGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_.dirname(targetPath));
7791
+ const current = await promises_.lstat(targetPath);
7792
+ if (current.isSymbolicLink() || !file_identity_sameFileIdentity(current, identity)) {
7793
+ return;
7794
+ }
7795
+ await withAsyncDirectoryGuards([parentGuard], async () => {
7796
+ await promises_.rm(targetPath);
7797
+ });
7798
+ }
7680
7799
  async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations) {
7681
- const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, relativePath);
7800
+ const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, relativePath, {
7801
+ aliasErrorCode: "path-alias",
7802
+ });
7682
7803
  await assertMutationNotDenied(resolved, denyMutations);
7683
7804
  try {
7684
7805
  await assertNoPathAliasEscape({
@@ -7780,8 +7901,11 @@ async function resolvePinnedRootPathInRoot(root, params) {
7780
7901
  const rootReal = root.rootReal;
7781
7902
  let resolved;
7782
7903
  try {
7904
+ const expandedPath = await expandRelativePathWithHome(params.relativePath);
7783
7905
  resolved = await resolveRootPath({
7784
- absolutePath: external_node_path_.resolve(rootReal, await expandRelativePathWithHome(params.relativePath)),
7906
+ absolutePath: external_node_path_.isAbsolute(expandedPath)
7907
+ ? expandedPath
7908
+ : `${ensureTrailingSep(rootReal)}${expandedPath}`,
7785
7909
  rootPath: rootReal,
7786
7910
  rootCanonicalPath: rootReal,
7787
7911
  boundaryLabel: "root",
@@ -7852,35 +7976,51 @@ async function listPathFallback(root, relativePath, withFileTypes) {
7852
7976
  }
7853
7977
  }
7854
7978
  async function assertMoveMutationAllowed(root, params) {
7855
- const source = await resolvePathInRoot(root, params.fromRelative);
7979
+ const source = await resolvePathInRoot(root, params.fromRelative, {
7980
+ aliasErrorCode: "path-alias",
7981
+ allowFinalSymlink: true,
7982
+ });
7856
7983
  await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
7857
- const target = await resolvePathInRoot(root, params.toRelative);
7984
+ const target = await resolvePathInRoot(root, params.toRelative, {
7985
+ aliasErrorCode: "path-alias",
7986
+ allowFinalSymlink: true,
7987
+ });
7858
7988
  await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
7859
7989
  }
7860
7990
  async function movePathFallback(root, params) {
7861
- const source = await resolvePathInRoot(root, params.fromRelative);
7991
+ const source = await resolvePathInRoot(root, params.fromRelative, {
7992
+ aliasErrorCode: "path-alias",
7993
+ allowFinalSymlink: true,
7994
+ });
7862
7995
  await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
7863
7996
  await resolvePinnedRootPathInRoot(root, {
7864
7997
  relativePath: params.fromRelative,
7865
7998
  policy: PATH_ALIAS_POLICIES.strict,
7866
7999
  });
7867
- const target = await resolvePathInRoot(root, params.toRelative);
8000
+ const target = await resolvePathInRoot(root, params.toRelative, {
8001
+ aliasErrorCode: "path-alias",
8002
+ allowFinalSymlink: true,
8003
+ });
7868
8004
  await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
7869
8005
  await resolvePinnedRootPathInRoot(root, {
7870
8006
  relativePath: params.toRelative,
7871
8007
  policy: PATH_ALIAS_POLICIES.unlinkTarget,
7872
8008
  });
7873
- try {
7874
- await assertNoPathAliasEscape({
7875
- absolutePath: target.resolved,
7876
- rootPath: target.rootReal,
7877
- boundaryLabel: "root",
7878
- });
7879
- }
7880
- catch (error) {
7881
- throw new errors_FsSafeError("path-alias", "path alias escape blocked", {
7882
- cause: error instanceof Error ? error : undefined,
7883
- });
8009
+ const targetStat = await promises_.lstat(target.resolved).catch(() => undefined);
8010
+ const replacesFinalSymlink = process.platform !== "win32" && params.overwrite && targetStat?.isSymbolicLink() === true;
8011
+ if (!replacesFinalSymlink) {
8012
+ try {
8013
+ await assertNoPathAliasEscape({
8014
+ absolutePath: target.resolved,
8015
+ rootPath: target.rootReal,
8016
+ boundaryLabel: "root",
8017
+ });
8018
+ }
8019
+ catch (error) {
8020
+ throw new errors_FsSafeError("path-alias", "path alias escape blocked", {
8021
+ cause: error instanceof Error ? error : undefined,
8022
+ });
8023
+ }
7884
8024
  }
7885
8025
  let sourceStat;
7886
8026
  try {
@@ -7967,6 +8107,7 @@ async function writeFileFallback(root, params) {
7967
8107
  encoding: params.encoding,
7968
8108
  mode: mode || 0o600,
7969
8109
  });
8110
+ unregisterTempPath.setIdentity(writtenStat);
7970
8111
  const commitTempPath = tempPath;
7971
8112
  await withAsyncDirectoryGuards([destinationGuard], async () => {
7972
8113
  await promises_.rename(commitTempPath, destinationPath);
@@ -7994,7 +8135,9 @@ async function writeFileFallback(root, params) {
7994
8135
  }
7995
8136
  }
7996
8137
  async function writeMissingFileFallback(root, params) {
7997
- const { rootReal, resolved } = await resolvePathInRoot(root, params.relativePath);
8138
+ const { rootReal, resolved } = await resolvePathInRoot(root, params.relativePath, {
8139
+ aliasErrorCode: "path-alias",
8140
+ });
7998
8141
  await assertMutationNotDenied(resolved, params.denyMutations);
7999
8142
  try {
8000
8143
  await assertNoPathAliasEscape({
@@ -8006,14 +8149,14 @@ async function writeMissingFileFallback(root, params) {
8006
8149
  catch (err) {
8007
8150
  throw new errors_FsSafeError("path-alias", "path alias escape blocked", { cause: err });
8008
8151
  }
8009
- if (params.mkdir !== false) {
8010
- await promises_.mkdir(external_node_path_.dirname(resolved), { recursive: true });
8011
- }
8012
- const parentGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_.dirname(resolved));
8152
+ const targetPath = params.mkdir === false
8153
+ ? resolved
8154
+ : await prepareRootWriteTarget(rootReal, resolved);
8155
+ const parentGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_.dirname(targetPath));
8013
8156
  let created = false;
8014
8157
  try {
8015
8158
  const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
8016
- const handle = await promises_.open(resolved, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
8159
+ const handle = await promises_.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
8017
8160
  created = true;
8018
8161
  try {
8019
8162
  if (typeof params.data === "string") {
@@ -8037,7 +8180,7 @@ async function writeMissingFileFallback(root, params) {
8037
8180
  await handle.close();
8038
8181
  await verifyAtomicWriteResult({
8039
8182
  root,
8040
- targetPath: resolved,
8183
+ targetPath,
8041
8184
  expectedIdentity: writtenStat,
8042
8185
  });
8043
8186
  created = false;
@@ -8052,87 +8195,102 @@ async function writeMissingFileFallback(root, params) {
8052
8195
  }
8053
8196
  finally {
8054
8197
  if (created) {
8055
- await promises_.rm(resolved, { force: true }).catch(() => undefined);
8198
+ await promises_.rm(targetPath, { force: true }).catch(() => undefined);
8056
8199
  }
8057
8200
  }
8058
8201
  }
8059
- async function copyFileFallback(root, params, source) {
8060
- let target = null;
8061
- let sourceClosedByStream = false;
8062
- let targetClosedByUs = false;
8063
- let tempHandle = null;
8064
- let tempPath = null;
8065
- let unregisterTempPath = null;
8066
- let tempClosedByStream = false;
8067
- try {
8068
- target = await openWritableFileInRoot(root, {
8069
- relativePath: params.relativePath,
8070
- mkdir: params.mkdir,
8071
- mode: params.mode,
8072
- denyMutations: params.denyMutations,
8073
- truncateExisting: false,
8074
- });
8075
- const destinationPath = target.realPath;
8076
- const mode = params.mode ?? (target.stat.mode & 0o777);
8077
- await target.handle.close().catch(() => { });
8078
- targetClosedByUs = true;
8079
- const destinationGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_.dirname(destinationPath));
8080
- tempPath = buildAtomicWriteTempPath(destinationPath);
8081
- unregisterTempPath = registerTempPathForExit(tempPath);
8082
- tempHandle = await promises_.open(tempPath, OPEN_WRITE_CREATE_FLAGS, mode || 0o600);
8083
- const sourceStream = createBoundedReadStream(source, params.maxBytes);
8084
- const targetStream = tempHandle.createWriteStream();
8085
- sourceStream.once("close", () => {
8086
- sourceClosedByStream = true;
8087
- });
8088
- targetStream.once("close", () => {
8089
- tempClosedByStream = true;
8090
- });
8091
- await (0,external_node_stream_promises_.pipeline)(sourceStream, targetStream);
8092
- const writtenStat = await promises_.stat(tempPath);
8093
- if (!tempClosedByStream) {
8094
- await tempHandle.close().catch(() => { });
8095
- tempClosedByStream = true;
8096
- }
8097
- tempHandle = null;
8098
- const commitTempPath = tempPath;
8099
- await withAsyncDirectoryGuards([destinationGuard], async () => {
8100
- await promises_.rename(commitTempPath, destinationPath);
8101
- });
8102
- tempPath = null;
8103
- unregisterTempPath();
8104
- unregisterTempPath = null;
8105
- try {
8106
- await verifyAtomicWriteResult({
8107
- root,
8108
- targetPath: destinationPath,
8109
- expectedIdentity: writtenStat,
8110
- });
8202
+
8203
+ ;// CONCATENATED MODULE: ../core/src/gateways/read-file-no-follow.ts
8204
+ /**
8205
+ * Atomic file read with symlink and size protection.
8206
+ */
8207
+
8208
+ function isEloopError(error) {
8209
+ return error instanceof Error && "code" in error && error.code === "ELOOP";
8210
+ }
8211
+ async function openWithoutFollowingSymlinks(filePath, safePath) {
8212
+ const hasNoFollow = typeof external_node_fs_.constants.O_NOFOLLOW === "number" && external_node_fs_.constants.O_NOFOLLOW !== 0;
8213
+ if (!hasNoFollow) {
8214
+ const stats = await (0,promises_.lstat)(filePath);
8215
+ if (stats.isSymbolicLink()) {
8216
+ throw new Error(`Symlinks are not allowed: ${safePath}`);
8111
8217
  }
8112
- catch (err) {
8113
- emitWriteBoundaryWarning(`post-copy verification failed: ${String(err)}`);
8114
- throw err;
8218
+ return (0,promises_.open)(filePath, external_node_fs_.constants.O_RDONLY);
8219
+ }
8220
+ try {
8221
+ return await (0,promises_.open)(filePath, external_node_fs_.constants.O_RDONLY | external_node_fs_.constants.O_NOFOLLOW);
8222
+ } catch (error) {
8223
+ if (isEloopError(error)) {
8224
+ throw new Error(`Symlinks are not allowed: ${safePath}`);
8115
8225
  }
8226
+ throw error;
8116
8227
  }
8117
- catch (err) {
8118
- if (target?.createdForWrite) {
8119
- await promises_.rm(target.realPath, { force: true }).catch(() => { });
8228
+ }
8229
+ /**
8230
+ * Reads a file atomically with symlink and size protection.
8231
+ *
8232
+ * Uses `O_NOFOLLOW` (where available) to atomically reject symlinks at
8233
+ * the kernel level, eliminating the TOCTOU window between `lstat()` and
8234
+ * `readFile()`. Falls back to `lstat()` on platforms without `O_NOFOLLOW`.
8235
+ * Validates file size via `fstat()` on the opened file descriptor so the
8236
+ * size check and the read operate on the same inode.
8237
+ */ async function readFileNoFollow(filePath, maxBytes) {
8238
+ const safePath = JSON.stringify(filePath);
8239
+ const fh = await openWithoutFollowingSymlinks(filePath, safePath);
8240
+ try {
8241
+ const fdStats = await fh.stat();
8242
+ if (fdStats.size > maxBytes) {
8243
+ throw new Error(`File ${safePath} exceeds size limit (${fdStats.size} bytes > ${maxBytes} bytes)`);
8120
8244
  }
8121
- throw err;
8245
+ return await fh.readFile("utf-8");
8246
+ } finally{
8247
+ await fh.close();
8122
8248
  }
8123
- finally {
8124
- if (!sourceClosedByStream) {
8125
- await source.handle.close().catch(() => { });
8249
+ }
8250
+
8251
+ ;// CONCATENATED MODULE: ../core/src/gateways/symlink-path-guard.ts
8252
+ /**
8253
+ * Path-segment symlink rejection helpers for root-bounded FS ops.
8254
+ */
8255
+
8256
+
8257
+ function isEnoentError(error) {
8258
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
8259
+ }
8260
+ function symlinkNotAllowedError(relativePath) {
8261
+ return new Error(`Symlinks are not allowed: path contains symbolic link: ${JSON.stringify(relativePath)}`, {
8262
+ cause: new errors_FsSafeError("symlink", "symlink not allowed")
8263
+ });
8264
+ }
8265
+ async function lstatIfExists(path) {
8266
+ try {
8267
+ return await (0,promises_.lstat)(path);
8268
+ } catch (error) {
8269
+ if (isEnoentError(error)) {
8270
+ return undefined;
8126
8271
  }
8127
- if (tempHandle && !tempClosedByStream) {
8128
- await tempHandle.close().catch(() => { });
8272
+ throw error;
8273
+ }
8274
+ }
8275
+ /**
8276
+ * Walk path segments under rootPath and reject any symbolic-link component.
8277
+ * Stops at the first missing segment (ENOENT).
8278
+ */ async function rejectSymlinkSegments(rootPath, relativeFromRoot, onSymlink) {
8279
+ if (!relativeFromRoot) {
8280
+ return;
8281
+ }
8282
+ let currentPath = rootPath;
8283
+ for (const segment of relativeFromRoot.split(external_node_path_.sep)){
8284
+ if (!segment || segment === ".") {
8285
+ continue;
8129
8286
  }
8130
- if (tempPath) {
8131
- await promises_.rm(tempPath, { force: true }).catch(() => { });
8287
+ currentPath = (0,external_node_path_.join)(currentPath, segment);
8288
+ const stats = await lstatIfExists(currentPath);
8289
+ if (stats === undefined) {
8290
+ return;
8132
8291
  }
8133
- unregisterTempPath?.();
8134
- if (target && !targetClosedByUs) {
8135
- await target.handle.close().catch(() => { });
8292
+ if (stats.isSymbolicLink()) {
8293
+ throw onSymlink(currentPath);
8136
8294
  }
8137
8295
  }
8138
8296
  }
@@ -8144,6 +8302,8 @@ async function copyFileFallback(root, params, source) {
8144
8302
 
8145
8303
 
8146
8304
 
8305
+
8306
+
8147
8307
  /**
8148
8308
  * Checks if a file or directory exists.
8149
8309
  */ async function fileExists(path) {
@@ -8155,28 +8315,34 @@ async function copyFileFallback(root, params, source) {
8155
8315
  }
8156
8316
  }
8157
8317
  function isPathWithinRoot(rootPath, candidatePath) {
8158
- return candidatePath === rootPath || candidatePath.startsWith(`${rootPath}${external_node_path_.sep}`);
8318
+ if (candidatePath === rootPath) {
8319
+ return true;
8320
+ }
8321
+ // When root is `/` (or `C:\`), appending sep would yield `//` / `C:\\` and
8322
+ // incorrectly reject every in-root path. Use root as the prefix when it
8323
+ // already ends with the separator.
8324
+ const prefix = rootPath.endsWith(external_node_path_.sep) ? rootPath : `${rootPath}${external_node_path_.sep}`;
8325
+ return candidatePath.startsWith(prefix);
8159
8326
  }
8160
8327
  function mapFsSafeError(error, relativePath) {
8161
- if (error instanceof errors_FsSafeError) {
8162
- switch(error.code){
8163
- case "outside-workspace":
8164
- case "invalid-path":
8165
- throw new Error(`Resolved path is outside target directory: ${relativePath}`, {
8166
- cause: error
8167
- });
8168
- case "path-alias":
8169
- case "symlink":
8170
- throw new Error(`Symlinks are not allowed: path contains symbolic link: ${relativePath}`, {
8171
- cause: error
8172
- });
8173
- case "too-large":
8174
- throw new Error(`File ${relativePath} exceeds size limit: ${error.message}`, {
8175
- cause: error
8176
- });
8177
- default:
8178
- throw error;
8179
- }
8328
+ if (!(error instanceof errors_FsSafeError)) {
8329
+ throw error;
8330
+ }
8331
+ const safePath = JSON.stringify(relativePath);
8332
+ if (error.code === "outside-workspace" || error.code === "invalid-path") {
8333
+ throw new Error(`Resolved path is outside target directory: ${safePath}`, {
8334
+ cause: error
8335
+ });
8336
+ }
8337
+ if (error.code === "path-alias" || error.code === "symlink") {
8338
+ throw new Error(`Symlinks are not allowed: path contains symbolic link: ${safePath}`, {
8339
+ cause: error
8340
+ });
8341
+ }
8342
+ if (error.code === "too-large") {
8343
+ throw new Error(`File ${safePath} exceeds size limit: ${error.message}`, {
8344
+ cause: error
8345
+ });
8180
8346
  }
8181
8347
  throw error;
8182
8348
  }
@@ -8187,8 +8353,20 @@ async function createSafeRoot(targetDir, maxBytes) {
8187
8353
  symlinks: "reject"
8188
8354
  });
8189
8355
  }
8356
+ /**
8357
+ * fs-safe 0.5 exists/stat/list canonicalize through in-root symlinks; open/read
8358
+ * still honor symlinks:"reject". Enforce reject on every root-bounded op.
8359
+ */ async function assertNoSymlinksWithinRoot(targetDir, relativePath) {
8360
+ if (!relativePath) {
8361
+ return;
8362
+ }
8363
+ const absolutePath = await file_system_utils_resolvePathWithinRoot(targetDir, relativePath);
8364
+ const rootPath = await (0,promises_.realpath)(targetDir);
8365
+ await rejectSymlinkSegments(rootPath, (0,external_node_path_.relative)(rootPath, absolutePath), ()=>symlinkNotAllowedError(relativePath));
8366
+ }
8190
8367
  async function file_system_utils_readTextWithinRoot(targetDir, relativePath, maxBytes) {
8191
8368
  try {
8369
+ await assertNoSymlinksWithinRoot(targetDir, relativePath);
8192
8370
  const safeRoot = await createSafeRoot(targetDir, maxBytes);
8193
8371
  return await safeRoot.readText(relativePath, {
8194
8372
  maxBytes
@@ -8199,6 +8377,7 @@ async function file_system_utils_readTextWithinRoot(targetDir, relativePath, max
8199
8377
  }
8200
8378
  async function file_system_utils_listDirectoryWithinRoot(targetDir, relativePath) {
8201
8379
  try {
8380
+ await assertNoSymlinksWithinRoot(targetDir, relativePath);
8202
8381
  const safeRoot = await createSafeRoot(targetDir);
8203
8382
  const entries = await safeRoot.list(relativePath, {
8204
8383
  withFileTypes: true
@@ -8218,6 +8397,7 @@ function toSafeDirEntry(entry) {
8218
8397
  }
8219
8398
  async function file_system_utils_pathExistsWithinRoot(targetDir, relativePath) {
8220
8399
  try {
8400
+ await assertNoSymlinksWithinRoot(targetDir, relativePath);
8221
8401
  const safeRoot = await createSafeRoot(targetDir);
8222
8402
  return await safeRoot.exists(relativePath);
8223
8403
  } catch (error) {
@@ -8234,6 +8414,7 @@ async function file_system_utils_pathExistsWithinRoot(targetDir, relativePath) {
8234
8414
  }
8235
8415
  async function statWithinRoot(targetDir, relativePath) {
8236
8416
  try {
8417
+ await assertNoSymlinksWithinRoot(targetDir, relativePath);
8237
8418
  const safeRoot = await createSafeRoot(targetDir);
8238
8419
  return await safeRoot.stat(relativePath);
8239
8420
  } catch (error) {
@@ -8260,26 +8441,7 @@ async function statWithinRoot(targetDir, relativePath) {
8260
8441
  if (!isPathWithinRoot(rootPath, absolutePath)) {
8261
8442
  throw new Error(`Resolved path is outside target directory: ${absolutePath}`);
8262
8443
  }
8263
- const relativePath = (0,external_node_path_.relative)(rootPath, absolutePath);
8264
- if (!relativePath || relativePath === ".") {
8265
- return;
8266
- }
8267
- const segments = relativePath.split(external_node_path_.sep);
8268
- let currentPath = rootPath;
8269
- for (const segment of segments){
8270
- currentPath = (0,external_node_path_.join)(currentPath, segment);
8271
- try {
8272
- const stats = await (0,promises_.lstat)(currentPath);
8273
- if (stats.isSymbolicLink()) {
8274
- throw new Error(`Path contains symbolic link: ${currentPath}`);
8275
- }
8276
- } catch (error) {
8277
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
8278
- return;
8279
- }
8280
- throw error;
8281
- }
8282
- }
8444
+ await rejectSymlinkSegments(rootPath, (0,external_node_path_.relative)(rootPath, absolutePath), (segmentPath)=>new Error(`Path contains symbolic link: ${segmentPath}`));
8283
8445
  }
8284
8446
  /**
8285
8447
  * Resolves a relative path under targetDir and validates it does not pass through symlinks.
@@ -8296,44 +8458,6 @@ async function statWithinRoot(targetDir, relativePath) {
8296
8458
  throw new Error(`${context} exceeds size limit (${fileStats.size} bytes > ${maxBytes} bytes)`);
8297
8459
  }
8298
8460
  }
8299
- /**
8300
- * Reads a file atomically with symlink and size protection.
8301
- *
8302
- * Uses `O_NOFOLLOW` (where available) to atomically reject symlinks at
8303
- * the kernel level, eliminating the TOCTOU window between `lstat()` and
8304
- * `readFile()`. Falls back to `lstat()` on platforms without `O_NOFOLLOW`.
8305
- * Validates file size via `fstat()` on the opened file descriptor so the
8306
- * size check and the read operate on the same inode.
8307
- */ async function readFileNoFollow(filePath, maxBytes) {
8308
- const hasNoFollow = typeof external_node_fs_.constants.O_NOFOLLOW === "number" && external_node_fs_.constants.O_NOFOLLOW !== 0;
8309
- const safePath = JSON.stringify(filePath);
8310
- let fh;
8311
- if (hasNoFollow) {
8312
- try {
8313
- fh = await (0,promises_.open)(filePath, external_node_fs_.constants.O_RDONLY | external_node_fs_.constants.O_NOFOLLOW);
8314
- } catch (error) {
8315
- if (error instanceof Error && "code" in error && error.code === "ELOOP") {
8316
- throw new Error(`Symlinks are not allowed: ${safePath}`);
8317
- }
8318
- throw error;
8319
- }
8320
- } else {
8321
- const stats = await (0,promises_.lstat)(filePath);
8322
- if (stats.isSymbolicLink()) {
8323
- throw new Error(`Symlinks are not allowed: ${safePath}`);
8324
- }
8325
- fh = await (0,promises_.open)(filePath, external_node_fs_.constants.O_RDONLY);
8326
- }
8327
- try {
8328
- const fdStats = await fh.stat();
8329
- if (fdStats.size > maxBytes) {
8330
- throw new Error(`File ${safePath} exceeds size limit (${fdStats.size} bytes > ${maxBytes} bytes)`);
8331
- }
8332
- return await fh.readFile("utf-8");
8333
- } finally{
8334
- await fh.close();
8335
- }
8336
- }
8337
8461
 
8338
8462
  ;// CONCATENATED MODULE: ../core/src/gateways/init-hooks-config-gateway.ts
8339
8463
 
@@ -16687,7 +16811,9 @@ class LessonContextUseCase {
16687
16811
  // the second, causing incorrect matching).
16688
16812
  const globMatchers = result.lessons.map((p)=>{
16689
16813
  const { paths } = p.lesson.triggers;
16690
- return paths.length > 0 ? picomatch_default()(paths, PICOMATCH_OPTIONS) : null;
16814
+ return paths.length > 0 ? picomatch_default()([
16815
+ ...paths
16816
+ ], PICOMATCH_OPTIONS) : null;
16691
16817
  });
16692
16818
  matched = result.lessons.filter((p, i)=>matchesLesson(p.lesson, filePaths, fileContents, segments, normalizedPaths, globMatchers[i] ?? null));
16693
16819
  }
@@ -19115,19 +19241,20 @@ function buildPermissionDecisionResponse(payload) {
19115
19241
  /** Parses a comma-separated --files string into trimmed, non-empty path tokens. */ function parseExplicitFiles(rawFiles) {
19116
19242
  return rawFiles.split(",").map((f)=>f.trim()).filter(Boolean);
19117
19243
  }
19244
+ const contextArgs = {
19245
+ files: {
19246
+ type: "string",
19247
+ description: "Comma-separated list of file paths to match lessons against (PreToolUse dispatch; for debug use — real hook invocations pass paths via stdin)",
19248
+ default: ""
19249
+ }
19250
+ };
19118
19251
  function createContextCommand(useCase) {
19119
19252
  return defineCommand({
19120
19253
  meta: {
19121
19254
  name: "context",
19122
19255
  description: "Inject relevant lessons into Claude hooks as additionalContext"
19123
19256
  },
19124
- args: {
19125
- files: {
19126
- type: "string",
19127
- description: "Comma-separated list of file paths to match lessons against (PreToolUse dispatch; for debug use — real hook invocations pass paths via stdin)",
19128
- default: ""
19129
- }
19130
- },
19257
+ args: contextArgs,
19131
19258
  run: async (context)=>{
19132
19259
  const rootDir = typeof context.data?.targetDir === "string" ? context.data.targetDir : process.cwd();
19133
19260
  const rawFiles = typeof context.args?.files === "string" ? context.args.files : "";
@@ -20709,8 +20836,6 @@ function _parseInput(input) {
20709
20836
  }
20710
20837
 
20711
20838
 
20712
- // EXTERNAL MODULE: external "node:module"
20713
- var external_node_module_ = __webpack_require__(8995);
20714
20839
  // EXTERNAL MODULE: ../../node_modules/jiti/dist/jiti.cjs
20715
20840
  var jiti = __webpack_require__(3273);
20716
20841
  ;// CONCATENATED MODULE: ../../node_modules/jiti/lib/jiti.mjs
@@ -22355,8 +22480,9 @@ async function readVersionFileContent(targetDir, relativePath) {
22355
22480
  continue;
22356
22481
  }
22357
22482
  if (await file_system_utils_pathExistsWithinRoot(targetDir, pmConfig.manifestFile)) {
22358
- const lockfilePath = pmConfig.lockfile ? await file_system_utils_resolveSafePath(targetDir, pmConfig.lockfile) : undefined;
22359
- const hasLockfile = lockfilePath ? await file_system_utils_pathExistsWithinRoot(targetDir, pmConfig.lockfile) : false;
22483
+ const { lockfile } = pmConfig;
22484
+ const lockfilePath = lockfile ? await file_system_utils_resolveSafePath(targetDir, lockfile) : undefined;
22485
+ const hasLockfile = lockfilePath && lockfile ? await file_system_utils_pathExistsWithinRoot(targetDir, lockfile) : false;
22360
22486
  if (pmConfig.requiresLockfile && !hasLockfile) {
22361
22487
  continue;
22362
22488
  }
@@ -22373,8 +22499,9 @@ async function readVersionFileContent(targetDir, relativePath) {
22373
22499
  const packageManagers = [];
22374
22500
  for (const pmConfig of otherPackageManagers){
22375
22501
  if (await file_system_utils_pathExistsWithinRoot(targetDir, pmConfig.manifestFile)) {
22376
- const lockfilePath = pmConfig.lockfile ? await file_system_utils_resolveSafePath(targetDir, pmConfig.lockfile) : undefined;
22377
- const hasLockfile = lockfilePath ? await file_system_utils_pathExistsWithinRoot(targetDir, pmConfig.lockfile) : false;
22502
+ const { lockfile } = pmConfig;
22503
+ const lockfilePath = lockfile ? await file_system_utils_resolveSafePath(targetDir, lockfile) : undefined;
22504
+ const hasLockfile = lockfilePath && lockfile ? await file_system_utils_pathExistsWithinRoot(targetDir, lockfile) : false;
22378
22505
  if (pmConfig.requiresLockfile && !hasLockfile) {
22379
22506
  continue;
22380
22507
  }
@@ -22395,6 +22522,8 @@ async function readVersionFileContent(targetDir, relativePath) {
22395
22522
  return new FileSystemEnvironmentGateway(cwd);
22396
22523
  }
22397
22524
 
22525
+ // EXTERNAL MODULE: external "node:child_process"
22526
+ var external_node_child_process_ = __webpack_require__(1421);
22398
22527
  ;// CONCATENATED MODULE: ../../node_modules/universal-user-agent/index.js
22399
22528
  function getUserAgent() {
22400
22529
  if (typeof navigator === "object" && "userAgent" in navigator) {
@@ -27103,11 +27232,18 @@ function defaultExec(command, args, options) {
27103
27232
  throw new Error("Not authenticated");
27104
27233
  }
27105
27234
  try {
27106
- await this.octokit.rest.repos.createRepoRuleset({
27235
+ const params = {
27107
27236
  owner,
27108
27237
  repo,
27109
- ...payload
27110
- });
27238
+ name: payload.name,
27239
+ enforcement: payload.enforcement,
27240
+ target: payload.target,
27241
+ // biome-ignore lint/style/useNamingConvention: GitHub API schema requires snake_case
27242
+ bypass_actors: payload.bypass_actors,
27243
+ conditions: payload.conditions,
27244
+ rules: payload.rules
27245
+ };
27246
+ await this.octokit.rest.repos.createRepoRuleset(params);
27111
27247
  } catch (error) {
27112
27248
  const details = formatOctokitError(error);
27113
27249
  throw new Error(`Failed to create ruleset for ${owner}/${repo}: ${details}`);
@@ -32548,24 +32684,25 @@ const initCommand = defineCommand({
32548
32684
  ;// CONCATENATED MODULE: ./src/commands/init-hooks.ts
32549
32685
 
32550
32686
 
32687
+ const initHooksArgs = {
32688
+ force: {
32689
+ type: "boolean",
32690
+ description: "Overwrite existing hook configuration even if already present",
32691
+ default: false
32692
+ },
32693
+ "no-session-start": {
32694
+ type: "boolean",
32695
+ description: "Disable the SessionStart hook for invariant lesson injection (enabled by default)",
32696
+ default: false
32697
+ }
32698
+ };
32551
32699
  function createInitHooksCommand(useCase) {
32552
32700
  return defineCommand({
32553
32701
  meta: {
32554
32702
  name: "init-hooks",
32555
32703
  description: "Initialize Claude Code hook configuration for lousy-agents context injection"
32556
32704
  },
32557
- args: {
32558
- force: {
32559
- type: "boolean",
32560
- description: "Overwrite existing hook configuration even if already present",
32561
- default: false
32562
- },
32563
- "no-session-start": {
32564
- type: "boolean",
32565
- description: "Disable the SessionStart hook for invariant lesson injection (enabled by default)",
32566
- default: false
32567
- }
32568
- },
32705
+ args: initHooksArgs,
32569
32706
  run: async (context)=>{
32570
32707
  const rootDir = typeof context.data?.targetDir === "string" ? context.data.targetDir : process.cwd();
32571
32708
  const force = context.args?.force === true;
@@ -51781,227 +51918,6 @@ function lib_isUint8Array(value) {
51781
51918
  */
51782
51919
  const remark = unified().use(remarkParse).use(remarkStringify).freeze()
51783
51920
 
51784
- ;// CONCATENATED MODULE: ../core/src/gateways/markdown-ast-gateway.ts
51785
- /**
51786
- * Gateway for parsing Markdown files into AST using remark.
51787
- * Extracts structural features like headings, code blocks, and paragraphs.
51788
- */
51789
-
51790
-
51791
- /** Maximum instruction file size: 1 MB */ const markdown_ast_gateway_MAX_INSTRUCTION_FILE_BYTES = 1_048_576;
51792
- /**
51793
- * Remark-based implementation of the Markdown AST gateway.
51794
- */ class RemarkMarkdownAstGateway {
51795
- async parseFile(filePath) {
51796
- const content = await readFileNoFollow(filePath, markdown_ast_gateway_MAX_INSTRUCTION_FILE_BYTES);
51797
- return this.parseContent(content);
51798
- }
51799
- parseContent(content) {
51800
- const processor = remark();
51801
- const ast = processor.parse(content);
51802
- const headings = [];
51803
- const codeBlocks = [];
51804
- const inlineCodes = [];
51805
- lib_visit(ast, "heading", (node)=>{
51806
- const text = this.extractHeadingText(node);
51807
- headings.push({
51808
- text,
51809
- depth: node.depth,
51810
- position: {
51811
- line: node.position?.start.line ?? 0
51812
- }
51813
- });
51814
- });
51815
- // Track code blocks with their node index in root children
51816
- for(let i = 0; i < ast.children.length; i++){
51817
- const child = ast.children[i];
51818
- if (child.type === "code") {
51819
- const codeNode = child;
51820
- codeBlocks.push({
51821
- value: codeNode.value,
51822
- lang: codeNode.lang ?? undefined,
51823
- position: {
51824
- line: codeNode.position?.start.line ?? 0
51825
- },
51826
- nodeIndex: i
51827
- });
51828
- }
51829
- }
51830
- lib_visit(ast, "inlineCode", (node)=>{
51831
- inlineCodes.push({
51832
- value: node.value,
51833
- position: {
51834
- line: node.position?.start.line ?? 0
51835
- }
51836
- });
51837
- });
51838
- return {
51839
- headings,
51840
- codeBlocks,
51841
- inlineCodes,
51842
- ast
51843
- };
51844
- }
51845
- findConditionalKeywordsInProximity(structure, codeBlockNodeIndex, proximityWindow, keywords) {
51846
- const ast = structure.ast;
51847
- const endIndex = Math.min(ast.children.length, codeBlockNodeIndex + 1 + proximityWindow);
51848
- for(let i = codeBlockNodeIndex + 1; i < endIndex; i++){
51849
- const sibling = ast.children[i];
51850
- const text = extractTextFromNode(sibling);
51851
- const lowerText = text.toLowerCase();
51852
- for (const keyword of keywords){
51853
- if (containsWordBoundary(lowerText, keyword.toLowerCase())) {
51854
- return true;
51855
- }
51856
- }
51857
- }
51858
- return false;
51859
- }
51860
- extractHeadingText(node) {
51861
- const parts = [];
51862
- lib_visit(node, "text", (textNode)=>{
51863
- parts.push(textNode.value);
51864
- });
51865
- return parts.join("");
51866
- }
51867
- }
51868
- /**
51869
- * Checks if text contains a keyword at a word boundary (safe string matching, no regex).
51870
- */ function containsWordBoundary(text, keyword) {
51871
- const index = text.indexOf(keyword);
51872
- if (index === -1) {
51873
- return false;
51874
- }
51875
- const charBefore = index > 0 ? text[index - 1] : " ";
51876
- const charAfter = index + keyword.length < text.length ? text[index + keyword.length] : " ";
51877
- const isWordBoundaryBefore = !/[a-z0-9]/i.test(charBefore);
51878
- const isWordBoundaryAfter = !/[a-z0-9]/i.test(charAfter);
51879
- return isWordBoundaryBefore && isWordBoundaryAfter;
51880
- }
51881
- /**
51882
- * Extracts plain text content from any AST node recursively.
51883
- */ function extractTextFromNode(node) {
51884
- if (node.value && typeof node.value === "string") {
51885
- return node.value;
51886
- }
51887
- if (Array.isArray(node.children)) {
51888
- return node.children.map((child)=>extractTextFromNode(child)).join(" ");
51889
- }
51890
- return "";
51891
- }
51892
- /**
51893
- * Creates and returns the default Markdown AST gateway.
51894
- */ function createMarkdownAstGateway() {
51895
- return new RemarkMarkdownAstGateway();
51896
- }
51897
-
51898
- ;// CONCATENATED MODULE: ../core/src/entities/lint-rules.ts
51899
- /**
51900
- * Lint rule registry entity.
51901
- * Defines all known lint rule IDs with their default severities, organized by target.
51902
- */ /** Valid severity values for rule configuration */ /** Default severity levels for all known lint rules */ const DEFAULT_LINT_RULES = {
51903
- agents: {
51904
- "agent/missing-frontmatter": "error",
51905
- "agent/invalid-frontmatter": "error",
51906
- "agent/missing-name": "error",
51907
- "agent/invalid-name-format": "error",
51908
- "agent/name-mismatch": "error",
51909
- "agent/missing-description": "error",
51910
- "agent/invalid-description": "error",
51911
- "agent/invalid-field": "warn"
51912
- },
51913
- hooks: {
51914
- "hook/invalid-json": "error",
51915
- "hook/invalid-config": "error",
51916
- "hook/missing-command": "error",
51917
- "hook/missing-matcher": "warn",
51918
- "hook/missing-timeout": "warn"
51919
- },
51920
- instructions: {
51921
- "instruction/parse-error": "warn",
51922
- "instruction/command-not-in-code-block": "warn",
51923
- "instruction/command-outside-section": "warn",
51924
- "instruction/missing-error-handling": "warn",
51925
- "instruction/missing-structural-heading": "warn"
51926
- },
51927
- skills: {
51928
- "skill/invalid-frontmatter": "error",
51929
- "skill/missing-frontmatter": "error",
51930
- "skill/missing-name": "error",
51931
- "skill/invalid-name-format": "error",
51932
- "skill/name-mismatch": "error",
51933
- "skill/missing-description": "error",
51934
- "skill/invalid-description": "error",
51935
- "skill/missing-allowed-tools": "warn",
51936
- "skill/missing-argument-hint": "warn"
51937
- }
51938
- };
51939
-
51940
- ;// CONCATENATED MODULE: ../core/src/lib/lint-config.ts
51941
- /**
51942
- * Lint configuration loader.
51943
- * Loads lint rule severity overrides from c12 config and merges with defaults.
51944
- */
51945
-
51946
-
51947
- /** Zod schema for a rule config map: rule IDs validated with regex to prevent prototype pollution */ const RuleConfigMapSchema = schemas_record(schemas_string().regex(/^[a-z]+\/[a-z]+(?:-[a-z]+)*$/), schemas_enum([
51948
- "error",
51949
- "warn",
51950
- "off"
51951
- ]));
51952
- /** Zod schema for the lint.rules section of the config */ const LintRulesConfigSchema = schemas_object({
51953
- agents: RuleConfigMapSchema.optional(),
51954
- hooks: RuleConfigMapSchema.optional(),
51955
- instructions: RuleConfigMapSchema.optional(),
51956
- skills: RuleConfigMapSchema.optional()
51957
- });
51958
- /** Zod schema for the lint section of the config */ const LintConfigSchema = schemas_object({
51959
- lint: schemas_object({
51960
- rules: LintRulesConfigSchema.optional()
51961
- }).optional()
51962
- });
51963
- /**
51964
- * Merges user overrides with defaults for a single target.
51965
- * Only known rule IDs (present in defaults) are applied; unknown IDs are discarded.
51966
- */ function mergeTargetRules(defaults, overrides) {
51967
- if (!overrides) {
51968
- return defaults;
51969
- }
51970
- const merged = {
51971
- ...defaults
51972
- };
51973
- for (const [ruleId, severity] of Object.entries(overrides)){
51974
- if (Object.hasOwn(defaults, ruleId)) {
51975
- merged[ruleId] = severity;
51976
- }
51977
- }
51978
- return merged;
51979
- }
51980
- /**
51981
- * Loads lint configuration from the target directory using c12.
51982
- * Merges user overrides with default rule severities.
51983
- * Throws on config load failures (syntax errors, permission denied, validation errors).
51984
- */ async function loadLintConfig(targetDir) {
51985
- const { config } = await loadConfig({
51986
- name: "lousy-agents",
51987
- cwd: targetDir
51988
- });
51989
- if (!config) {
51990
- return DEFAULT_LINT_RULES;
51991
- }
51992
- const parsed = LintConfigSchema.parse(config);
51993
- const rules = parsed.lint?.rules;
51994
- if (!rules) {
51995
- return DEFAULT_LINT_RULES;
51996
- }
51997
- return {
51998
- agents: mergeTargetRules(DEFAULT_LINT_RULES.agents, rules.agents),
51999
- hooks: mergeTargetRules(DEFAULT_LINT_RULES.hooks, rules.hooks),
52000
- instructions: mergeTargetRules(DEFAULT_LINT_RULES.instructions, rules.instructions),
52001
- skills: mergeTargetRules(DEFAULT_LINT_RULES.skills, rules.skills)
52002
- };
52003
- }
52004
-
52005
51921
  ;// CONCATENATED MODULE: ../core/src/entities/instruction-quality.ts
52006
51922
  /**
52007
51923
  * Core domain entities for instruction quality analysis.
@@ -52053,6 +51969,9 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
52053
51969
  * Orchestrates discovery, AST parsing, and scoring of feedback loop documentation.
52054
51970
  */
52055
51971
 
51972
+ /** Type guard narrowing an `unknown` AST child to {@link AstNodeLike}. */ function isAstNodeLike(node) {
51973
+ return typeof node === "object" && node !== null && "type" in node && typeof node.type === "string";
51974
+ }
52056
51975
  /** Maximum number of raw heading pattern entries accepted before deduplication. */ const MAX_RAW_HEADING_PATTERNS = 1000;
52057
51976
  /** Maximum number of heading patterns accepted in a single execute() call. */ const MAX_HEADING_PATTERNS = 50;
52058
51977
  /** Maximum character length for a single heading pattern. */ const analyze_instruction_quality_MAX_PATTERN_LENGTH = 200;
@@ -52408,6 +52327,9 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
52408
52327
  return parts.join(" ");
52409
52328
  }
52410
52329
  collectText(node, parts) {
52330
+ if (!isAstNodeLike(node)) {
52331
+ return;
52332
+ }
52411
52333
  if (node.type === "text" && typeof node.value === "string") {
52412
52334
  parts.push(node.value);
52413
52335
  }
@@ -52554,6 +52476,231 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
52554
52476
  }
52555
52477
  }
52556
52478
 
52479
+ ;// CONCATENATED MODULE: ../core/src/gateways/markdown-ast-gateway.ts
52480
+ /**
52481
+ * Gateway for parsing Markdown files into AST using remark.
52482
+ * Extracts structural features like headings, code blocks, and paragraphs.
52483
+ */
52484
+
52485
+
52486
+
52487
+ /** Maximum instruction file size: 1 MB */ const markdown_ast_gateway_MAX_INSTRUCTION_FILE_BYTES = 1_048_576;
52488
+ /**
52489
+ * Remark-based implementation of the Markdown AST gateway.
52490
+ */ class RemarkMarkdownAstGateway {
52491
+ async parseFile(filePath) {
52492
+ const content = await readFileNoFollow(filePath, markdown_ast_gateway_MAX_INSTRUCTION_FILE_BYTES);
52493
+ return this.parseContent(content);
52494
+ }
52495
+ parseContent(content) {
52496
+ const processor = remark();
52497
+ const ast = processor.parse(content);
52498
+ const headings = [];
52499
+ const codeBlocks = [];
52500
+ const inlineCodes = [];
52501
+ lib_visit(ast, "heading", (node)=>{
52502
+ const text = this.extractHeadingText(node);
52503
+ headings.push({
52504
+ text,
52505
+ depth: node.depth,
52506
+ position: {
52507
+ line: node.position?.start.line ?? 0
52508
+ }
52509
+ });
52510
+ });
52511
+ // Track code blocks with their node index in root children
52512
+ for(let i = 0; i < ast.children.length; i++){
52513
+ const child = ast.children[i];
52514
+ if (child.type === "code") {
52515
+ const codeNode = child;
52516
+ codeBlocks.push({
52517
+ value: codeNode.value,
52518
+ lang: codeNode.lang ?? undefined,
52519
+ position: {
52520
+ line: codeNode.position?.start.line ?? 0
52521
+ },
52522
+ nodeIndex: i
52523
+ });
52524
+ }
52525
+ }
52526
+ lib_visit(ast, "inlineCode", (node)=>{
52527
+ inlineCodes.push({
52528
+ value: node.value,
52529
+ position: {
52530
+ line: node.position?.start.line ?? 0
52531
+ }
52532
+ });
52533
+ });
52534
+ return {
52535
+ headings,
52536
+ codeBlocks,
52537
+ inlineCodes,
52538
+ ast
52539
+ };
52540
+ }
52541
+ findConditionalKeywordsInProximity(structure, codeBlockNodeIndex, proximityWindow, keywords) {
52542
+ const ast = structure.ast;
52543
+ const endIndex = Math.min(ast.children.length, codeBlockNodeIndex + 1 + proximityWindow);
52544
+ for(let i = codeBlockNodeIndex + 1; i < endIndex; i++){
52545
+ const sibling = ast.children[i];
52546
+ const text = extractTextFromNode(sibling);
52547
+ const lowerText = text.toLowerCase();
52548
+ for (const keyword of keywords){
52549
+ if (containsWordBoundary(lowerText, keyword.toLowerCase())) {
52550
+ return true;
52551
+ }
52552
+ }
52553
+ }
52554
+ return false;
52555
+ }
52556
+ extractHeadingText(node) {
52557
+ const parts = [];
52558
+ lib_visit(node, "text", (textNode)=>{
52559
+ parts.push(textNode.value);
52560
+ });
52561
+ return parts.join("");
52562
+ }
52563
+ }
52564
+ /**
52565
+ * Checks if text contains a keyword at a word boundary (safe string matching, no regex).
52566
+ */ function containsWordBoundary(text, keyword) {
52567
+ const index = text.indexOf(keyword);
52568
+ if (index === -1) {
52569
+ return false;
52570
+ }
52571
+ const charBefore = index > 0 ? text[index - 1] : " ";
52572
+ const charAfter = index + keyword.length < text.length ? text[index + keyword.length] : " ";
52573
+ const isWordBoundaryBefore = !/[a-z0-9]/i.test(charBefore);
52574
+ const isWordBoundaryAfter = !/[a-z0-9]/i.test(charAfter);
52575
+ return isWordBoundaryBefore && isWordBoundaryAfter;
52576
+ }
52577
+ /**
52578
+ * Extracts plain text content from any AST node recursively.
52579
+ */ function extractTextFromNode(node) {
52580
+ if (!isAstNodeLike(node)) {
52581
+ return "";
52582
+ }
52583
+ if (node.value && typeof node.value === "string") {
52584
+ return node.value;
52585
+ }
52586
+ if (Array.isArray(node.children)) {
52587
+ return node.children.map((child)=>extractTextFromNode(child)).join(" ");
52588
+ }
52589
+ return "";
52590
+ }
52591
+ /**
52592
+ * Creates and returns the default Markdown AST gateway.
52593
+ */ function createMarkdownAstGateway() {
52594
+ return new RemarkMarkdownAstGateway();
52595
+ }
52596
+
52597
+ ;// CONCATENATED MODULE: ../core/src/entities/lint-rules.ts
52598
+ /**
52599
+ * Lint rule registry entity.
52600
+ * Defines all known lint rule IDs with their default severities, organized by target.
52601
+ */ /** Valid severity values for rule configuration */ /** Default severity levels for all known lint rules */ const DEFAULT_LINT_RULES = {
52602
+ agents: {
52603
+ "agent/missing-frontmatter": "error",
52604
+ "agent/invalid-frontmatter": "error",
52605
+ "agent/missing-name": "error",
52606
+ "agent/invalid-name-format": "error",
52607
+ "agent/name-mismatch": "error",
52608
+ "agent/missing-description": "error",
52609
+ "agent/invalid-description": "error",
52610
+ "agent/invalid-field": "warn"
52611
+ },
52612
+ hooks: {
52613
+ "hook/invalid-json": "error",
52614
+ "hook/invalid-config": "error",
52615
+ "hook/missing-command": "error",
52616
+ "hook/missing-matcher": "warn",
52617
+ "hook/missing-timeout": "warn"
52618
+ },
52619
+ instructions: {
52620
+ "instruction/parse-error": "warn",
52621
+ "instruction/command-not-in-code-block": "warn",
52622
+ "instruction/command-outside-section": "warn",
52623
+ "instruction/missing-error-handling": "warn",
52624
+ "instruction/missing-structural-heading": "warn"
52625
+ },
52626
+ skills: {
52627
+ "skill/invalid-frontmatter": "error",
52628
+ "skill/missing-frontmatter": "error",
52629
+ "skill/missing-name": "error",
52630
+ "skill/invalid-name-format": "error",
52631
+ "skill/name-mismatch": "error",
52632
+ "skill/missing-description": "error",
52633
+ "skill/invalid-description": "error",
52634
+ "skill/missing-allowed-tools": "warn",
52635
+ "skill/missing-argument-hint": "warn"
52636
+ }
52637
+ };
52638
+
52639
+ ;// CONCATENATED MODULE: ../core/src/lib/lint-config.ts
52640
+ /**
52641
+ * Lint configuration loader.
52642
+ * Loads lint rule severity overrides from c12 config and merges with defaults.
52643
+ */
52644
+
52645
+
52646
+ /** Zod schema for a rule config map: rule IDs validated with regex to prevent prototype pollution */ const RuleConfigMapSchema = schemas_record(schemas_string().regex(/^[a-z]+\/[a-z]+(?:-[a-z]+)*$/), schemas_enum([
52647
+ "error",
52648
+ "warn",
52649
+ "off"
52650
+ ]));
52651
+ /** Zod schema for the lint.rules section of the config */ const LintRulesConfigSchema = schemas_object({
52652
+ agents: RuleConfigMapSchema.optional(),
52653
+ hooks: RuleConfigMapSchema.optional(),
52654
+ instructions: RuleConfigMapSchema.optional(),
52655
+ skills: RuleConfigMapSchema.optional()
52656
+ });
52657
+ /** Zod schema for the lint section of the config */ const LintConfigSchema = schemas_object({
52658
+ lint: schemas_object({
52659
+ rules: LintRulesConfigSchema.optional()
52660
+ }).optional()
52661
+ });
52662
+ /**
52663
+ * Merges user overrides with defaults for a single target.
52664
+ * Only known rule IDs (present in defaults) are applied; unknown IDs are discarded.
52665
+ */ function mergeTargetRules(defaults, overrides) {
52666
+ if (!overrides) {
52667
+ return defaults;
52668
+ }
52669
+ const merged = {
52670
+ ...defaults
52671
+ };
52672
+ for (const [ruleId, severity] of Object.entries(overrides)){
52673
+ if (Object.hasOwn(defaults, ruleId)) {
52674
+ merged[ruleId] = severity;
52675
+ }
52676
+ }
52677
+ return merged;
52678
+ }
52679
+ /**
52680
+ * Loads lint configuration from the target directory using c12.
52681
+ * Merges user overrides with default rule severities.
52682
+ * Throws on config load failures (syntax errors, permission denied, validation errors).
52683
+ */ async function loadLintConfig(targetDir) {
52684
+ const { config } = await loadConfig({
52685
+ name: "lousy-agents",
52686
+ cwd: targetDir
52687
+ });
52688
+ if (!config) {
52689
+ return DEFAULT_LINT_RULES;
52690
+ }
52691
+ const parsed = LintConfigSchema.parse(config);
52692
+ const rules = parsed.lint?.rules;
52693
+ if (!rules) {
52694
+ return DEFAULT_LINT_RULES;
52695
+ }
52696
+ return {
52697
+ agents: mergeTargetRules(DEFAULT_LINT_RULES.agents, rules.agents),
52698
+ hooks: mergeTargetRules(DEFAULT_LINT_RULES.hooks, rules.hooks),
52699
+ instructions: mergeTargetRules(DEFAULT_LINT_RULES.instructions, rules.instructions),
52700
+ skills: mergeTargetRules(DEFAULT_LINT_RULES.skills, rules.skills)
52701
+ };
52702
+ }
52703
+
52557
52704
  ;// CONCATENATED MODULE: ../core/src/use-cases/apply-severity-filter.ts
52558
52705
  /**
52559
52706
  * Shared severity filtering for lint outputs.
@@ -53503,7 +53650,34 @@ function isTargetEnabled(key, targets) {
53503
53650
  /**
53504
53651
  * The `lint` command for validating agent skills, custom agents, and instruction files.
53505
53652
  * Also exposes `lint lessons` as a subcommand for lesson frontmatter validation.
53506
- */ function createLintCommand(lintLessonsCmd) {
53653
+ */ const lintArgs = {
53654
+ skills: {
53655
+ type: "boolean",
53656
+ description: "Lint skill frontmatter in .github/skills/ and .claude/skills/",
53657
+ default: false
53658
+ },
53659
+ agents: {
53660
+ type: "boolean",
53661
+ description: "Lint custom agent frontmatter in .github/agents/",
53662
+ default: false
53663
+ },
53664
+ hooks: {
53665
+ type: "boolean",
53666
+ description: "Lint pre-tool-use hook configurations in .github/hooks/agent-shell/hooks.json, .claude/settings.json, and .claude/settings.local.json",
53667
+ default: false
53668
+ },
53669
+ instructions: {
53670
+ type: "boolean",
53671
+ description: "Analyze instruction quality across all instruction file formats",
53672
+ default: false
53673
+ },
53674
+ format: {
53675
+ type: "string",
53676
+ description: "Output format: human (default), json, or rdjsonl",
53677
+ default: "human"
53678
+ }
53679
+ };
53680
+ function createLintCommand(lintLessonsCmd) {
53507
53681
  return defineCommand({
53508
53682
  meta: {
53509
53683
  name: "lint",
@@ -53512,33 +53686,7 @@ function isTargetEnabled(key, targets) {
53512
53686
  subCommands: {
53513
53687
  lessons: lintLessonsCmd
53514
53688
  },
53515
- args: {
53516
- skills: {
53517
- type: "boolean",
53518
- description: "Lint skill frontmatter in .github/skills/ and .claude/skills/",
53519
- default: false
53520
- },
53521
- agents: {
53522
- type: "boolean",
53523
- description: "Lint custom agent frontmatter in .github/agents/",
53524
- default: false
53525
- },
53526
- hooks: {
53527
- type: "boolean",
53528
- description: "Lint pre-tool-use hook configurations in .github/hooks/agent-shell/hooks.json, .claude/settings.json, and .claude/settings.local.json",
53529
- default: false
53530
- },
53531
- instructions: {
53532
- type: "boolean",
53533
- description: "Analyze instruction quality across all instruction file formats",
53534
- default: false
53535
- },
53536
- format: {
53537
- type: "string",
53538
- description: "Output format: human (default), json, or rdjsonl",
53539
- default: "human"
53540
- }
53541
- },
53689
+ args: lintArgs,
53542
53690
  run: async (context)=>{
53543
53691
  const rawTargetDir = typeof context.data?.targetDir === "string" ? context.data.targetDir : process.cwd();
53544
53692
  const rawArgs_ = context.rawArgs ?? [];
@@ -62888,4 +63036,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
62888
63036
  // module factories are used so entry inlining is disabled
62889
63037
  // startup
62890
63038
  // Load entry module and return exports
62891
- var __webpack_exports__ = __webpack_require__(6367);
63039
+ var __webpack_exports__ = __webpack_require__(7015);