@notiamo/runner 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +79 -6
  2. package/dist/cli.js +1098 -260
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1045,8 +1045,8 @@ var require_safePathNames = __commonJS({
1045
1045
  })();
1046
1046
  Object.defineProperty(exports, "__esModule", { value: true });
1047
1047
  exports.isSafePathName = isSafePathName;
1048
- var path11 = __importStar(__require("path"));
1049
- function isSafePathName(name, localPath = path11) {
1048
+ var path13 = __importStar(__require("path"));
1049
+ function isSafePathName(name, localPath = path13) {
1050
1050
  if (!name || name === "." || name === ".." || name.includes("/")) {
1051
1051
  return false;
1052
1052
  }
@@ -1704,8 +1704,8 @@ var require_Client = __commonJS({
1704
1704
  /**
1705
1705
  * Set the working directory.
1706
1706
  */
1707
- async cd(path11) {
1708
- const validPath = await this.protectWhitespace(path11);
1707
+ async cd(path13) {
1708
+ const validPath = await this.protectWhitespace(path13);
1709
1709
  return this.send("CWD " + validPath);
1710
1710
  }
1711
1711
  /**
@@ -1718,8 +1718,8 @@ var require_Client = __commonJS({
1718
1718
  * Get the last modified time of a file. This is not supported by every FTP server, in which case
1719
1719
  * calling this method will throw an exception.
1720
1720
  */
1721
- async lastMod(path11) {
1722
- const validPath = await this.protectWhitespace(path11);
1721
+ async lastMod(path13) {
1722
+ const validPath = await this.protectWhitespace(path13);
1723
1723
  const res = await this.send(`MDTM ${validPath}`);
1724
1724
  const date5 = res.message.slice(4);
1725
1725
  return (0, parseListMLSD_1.parseMLSxDate)(date5);
@@ -1727,8 +1727,8 @@ var require_Client = __commonJS({
1727
1727
  /**
1728
1728
  * Get the size of a file.
1729
1729
  */
1730
- async size(path11) {
1731
- const validPath = await this.protectWhitespace(path11);
1730
+ async size(path13) {
1731
+ const validPath = await this.protectWhitespace(path13);
1732
1732
  const command = `SIZE ${validPath}`;
1733
1733
  const res = await this.send(command);
1734
1734
  const size = parseInt(res.message.slice(4), 10);
@@ -1755,8 +1755,8 @@ var require_Client = __commonJS({
1755
1755
  * You can ignore FTP error return codes which won't throw an exception if e.g.
1756
1756
  * the file doesn't exist.
1757
1757
  */
1758
- async remove(path11, ignoreErrorCodes = false) {
1759
- const validPath = await this.protectWhitespace(path11);
1758
+ async remove(path13, ignoreErrorCodes = false) {
1759
+ const validPath = await this.protectWhitespace(path13);
1760
1760
  if (ignoreErrorCodes) {
1761
1761
  return this.sendIgnoringError(`DELE ${validPath}`);
1762
1762
  }
@@ -1915,8 +1915,8 @@ var require_Client = __commonJS({
1915
1915
  *
1916
1916
  * @param [path] Path to remote file or directory.
1917
1917
  */
1918
- async list(path11 = "") {
1919
- const validPath = await this.protectWhitespace(path11);
1918
+ async list(path13 = "") {
1919
+ const validPath = await this.protectWhitespace(path13);
1920
1920
  let lastError;
1921
1921
  for (const candidate of this.availableListCommands) {
1922
1922
  const command = validPath === "" ? candidate : `${candidate} ${validPath}`;
@@ -2085,21 +2085,21 @@ var require_Client = __commonJS({
2085
2085
  /**
2086
2086
  * Remove an empty directory, will fail if not empty.
2087
2087
  */
2088
- async removeEmptyDir(path11) {
2089
- const validPath = await this.protectWhitespace(path11);
2088
+ async removeEmptyDir(path13) {
2089
+ const validPath = await this.protectWhitespace(path13);
2090
2090
  return this.send(`RMD ${validPath}`);
2091
2091
  }
2092
2092
  /**
2093
2093
  * FTP servers can't handle filenames that have leading whitespace. This method transforms
2094
2094
  * a given path to fix that issue for most cases.
2095
2095
  */
2096
- async protectWhitespace(path11) {
2097
- if (!path11.startsWith(" ")) {
2098
- return path11;
2096
+ async protectWhitespace(path13) {
2097
+ if (!path13.startsWith(" ")) {
2098
+ return path13;
2099
2099
  }
2100
2100
  const pwd = await this.pwd();
2101
2101
  const absolutePathPrefix = pwd.endsWith("/") ? pwd : pwd + "/";
2102
- return absolutePathPrefix + path11;
2102
+ return absolutePathPrefix + path13;
2103
2103
  }
2104
2104
  async _exitAtCurrentDirectory(func) {
2105
2105
  const userDir = await this.pwd();
@@ -2176,11 +2176,11 @@ var require_Client = __commonJS({
2176
2176
  }
2177
2177
  };
2178
2178
  exports.Client = Client3;
2179
- async function ensureLocalDirectory(path11) {
2179
+ async function ensureLocalDirectory(path13) {
2180
2180
  try {
2181
- await fsStat(path11);
2181
+ await fsStat(path13);
2182
2182
  } catch (_a3) {
2183
- await fsMkDir(path11, { recursive: true });
2183
+ await fsMkDir(path13, { recursive: true });
2184
2184
  }
2185
2185
  }
2186
2186
  async function expectedBytesFromLocalFile(fd, options) {
@@ -5477,8 +5477,8 @@ var require_utils = __commonJS({
5477
5477
  }
5478
5478
  return ind;
5479
5479
  }
5480
- function removeDotSegments(path11) {
5481
- let input2 = path11;
5480
+ function removeDotSegments(path13) {
5481
+ let input2 = path13;
5482
5482
  const output2 = [];
5483
5483
  let nextSlash = -1;
5484
5484
  let len = 0;
@@ -5887,8 +5887,8 @@ var require_schemes = __commonJS({
5887
5887
  }
5888
5888
  if (wsComponent.resourceName) {
5889
5889
  const queryIndex = wsComponent.resourceName.indexOf("?");
5890
- const path11 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
5891
- wsComponent.path = path11 && path11 !== "/" ? path11 : void 0;
5890
+ const path13 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
5891
+ wsComponent.path = path13 && path13 !== "/" ? path13 : void 0;
5892
5892
  wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
5893
5893
  wsComponent.resourceName = void 0;
5894
5894
  }
@@ -16025,7 +16025,7 @@ var require_keyParser = __commonJS({
16025
16025
  var {
16026
16026
  createDecipheriv,
16027
16027
  createECDH,
16028
- createHash,
16028
+ createHash: createHash2,
16029
16029
  createHmac,
16030
16030
  createSign,
16031
16031
  createVerify,
@@ -16713,11 +16713,11 @@ ${formatted}-----END ${type} KEY-----`;
16713
16713
  );
16714
16714
  }
16715
16715
  const ivSlice = bufferSlice(cipherIV, 0, 8);
16716
- let cipherKey = createHash("md5").update(passphrase).update(ivSlice).digest();
16716
+ let cipherKey = createHash2("md5").update(passphrase).update(ivSlice).digest();
16717
16717
  while (cipherKey.length < encInfo.keyLen) {
16718
16718
  cipherKey = combineBuffers(
16719
16719
  cipherKey,
16720
- createHash("md5").update(cipherKey).update(passphrase).update(ivSlice).digest()
16720
+ createHash2("md5").update(cipherKey).update(passphrase).update(ivSlice).digest()
16721
16721
  );
16722
16722
  }
16723
16723
  if (cipherKey.length > encInfo.keyLen)
@@ -16882,8 +16882,8 @@ ${formatted}-----END ${type} KEY-----`;
16882
16882
  if (encrypted) {
16883
16883
  const encInfo = CIPHER_INFO[cipherName];
16884
16884
  let cipherKey = combineBuffers(
16885
- createHash("sha1").update(PPK_PP1).update(passphrase).digest(),
16886
- createHash("sha1").update(PPK_PP2).update(passphrase).digest()
16885
+ createHash2("sha1").update(PPK_PP1).update(passphrase).digest(),
16886
+ createHash2("sha1").update(PPK_PP2).update(passphrase).digest()
16887
16887
  );
16888
16888
  if (cipherKey.length > encInfo.keyLen)
16889
16889
  cipherKey = bufferSlice(cipherKey, 0, encInfo.keyLen);
@@ -16923,7 +16923,7 @@ ${formatted}-----END ${type} KEY-----`;
16923
16923
  passphrase = EMPTY_PASSPHRASE;
16924
16924
  const calcMAC = createHmac(
16925
16925
  "sha1",
16926
- createHash("sha1").update("putty-private-key-file-mac-key").update(passphrase).digest()
16926
+ createHash2("sha1").update("putty-private-key-file-mac-key").update(passphrase).digest()
16927
16927
  ).update(macData).digest("hex");
16928
16928
  if (calcMAC !== mac3) {
16929
16929
  if (encrypted) {
@@ -17567,11 +17567,11 @@ var require_agent = __commonJS({
17567
17567
  };
17568
17568
  })();
17569
17569
  var WINDOWS_PIPE_REGEX = /^[/\\][/\\]\.[/\\]pipe[/\\].+/;
17570
- function createAgent(path11) {
17571
- if (process.platform === "win32" && !WINDOWS_PIPE_REGEX.test(path11)) {
17572
- return path11 === "pageant" ? new PageantAgent() : new CygwinAgent(path11);
17570
+ function createAgent(path13) {
17571
+ if (process.platform === "win32" && !WINDOWS_PIPE_REGEX.test(path13)) {
17572
+ return path13 === "pageant" ? new PageantAgent() : new CygwinAgent(path13);
17573
17573
  }
17574
- return new OpenSSHAgent(path11);
17574
+ return new OpenSSHAgent(path13);
17575
17575
  }
17576
17576
  var AgentProtocol = (() => {
17577
17577
  const SSH_AGENTC_REQUEST_IDENTITIES = 11;
@@ -19245,7 +19245,7 @@ var require_kex = __commonJS({
19245
19245
  createDiffieHellman,
19246
19246
  createDiffieHellmanGroup,
19247
19247
  createECDH,
19248
- createHash,
19248
+ createHash: createHash2,
19249
19249
  createPublicKey,
19250
19250
  diffieHellman,
19251
19251
  generateKeyPairSync,
@@ -19671,7 +19671,7 @@ var require_kex = __commonJS({
19671
19671
  DISCONNECT_REASON.KEY_EXCHANGE_FAILED
19672
19672
  );
19673
19673
  }
19674
- const hash2 = createHash(this.hashName);
19674
+ const hash2 = createHash2(this.hashName);
19675
19675
  hashString(hash2, isServer ? this._remoteIdentRaw : this._identRaw);
19676
19676
  hashString(hash2, isServer ? this._identRaw : this._remoteIdentRaw);
19677
19677
  hashString(hash2, isServer ? this._remoteKexinit : this._kexinit);
@@ -20679,9 +20679,9 @@ var require_kex = __commonJS({
20679
20679
  function generateKEXVal(len, hashName, secret, exchangeHash, sessionID, char) {
20680
20680
  let ret;
20681
20681
  if (len) {
20682
- let digest = createHash(hashName).update(secret).update(exchangeHash).update(char).update(sessionID).digest();
20682
+ let digest = createHash2(hashName).update(secret).update(exchangeHash).update(char).update(sessionID).digest();
20683
20683
  while (digest.length < len) {
20684
- const chunk = createHash(hashName).update(secret).update(exchangeHash).update(digest).digest();
20684
+ const chunk = createHash2(hashName).update(secret).update(exchangeHash).update(digest).digest();
20685
20685
  const extended2 = Buffer.allocUnsafe(digest.length + chunk.length);
20686
20686
  extended2.set(digest, 0);
20687
20687
  extended2.set(chunk, digest.length);
@@ -22815,17 +22815,17 @@ var require_SFTP = __commonJS({
22815
22815
  // ===========================================================================
22816
22816
  // Client-specific ===========================================================
22817
22817
  // ===========================================================================
22818
- createReadStream(path11, options) {
22818
+ createReadStream(path13, options) {
22819
22819
  if (this.server)
22820
22820
  throw new Error("Client-only method called in server mode");
22821
- return new ReadStream(this, path11, options);
22821
+ return new ReadStream(this, path13, options);
22822
22822
  }
22823
- createWriteStream(path11, options) {
22823
+ createWriteStream(path13, options) {
22824
22824
  if (this.server)
22825
22825
  throw new Error("Client-only method called in server mode");
22826
- return new WriteStream(this, path11, options);
22826
+ return new WriteStream(this, path13, options);
22827
22827
  }
22828
- open(path11, flags_, attrs, cb) {
22828
+ open(path13, flags_, attrs, cb) {
22829
22829
  if (this.server)
22830
22830
  throw new Error("Client-only method called in server mode");
22831
22831
  if (typeof attrs === "function") {
@@ -22844,7 +22844,7 @@ var require_SFTP = __commonJS({
22844
22844
  attrsFlags = attrs.flags;
22845
22845
  attrsLen = attrs.nb;
22846
22846
  }
22847
- const pathLen = Buffer.byteLength(path11);
22847
+ const pathLen = Buffer.byteLength(path13);
22848
22848
  let p = 9;
22849
22849
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + 4 + attrsLen);
22850
22850
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -22852,7 +22852,7 @@ var require_SFTP = __commonJS({
22852
22852
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
22853
22853
  writeUInt32BE(buf, reqid, 5);
22854
22854
  writeUInt32BE(buf, pathLen, p);
22855
- buf.utf8Write(path11, p += 4, pathLen);
22855
+ buf.utf8Write(path13, p += 4, pathLen);
22856
22856
  writeUInt32BE(buf, flags, p += pathLen);
22857
22857
  writeUInt32BE(buf, attrsFlags, p += 4);
22858
22858
  if (attrsLen) {
@@ -22983,7 +22983,7 @@ var require_SFTP = __commonJS({
22983
22983
  throw new Error("Client-only method called in server mode");
22984
22984
  fastXfer(fs, this, localPath, remotePath, opts, cb);
22985
22985
  }
22986
- readFile(path11, options, callback_) {
22986
+ readFile(path13, options, callback_) {
22987
22987
  if (this.server)
22988
22988
  throw new Error("Client-only method called in server mode");
22989
22989
  let callback;
@@ -23056,13 +23056,13 @@ var require_SFTP = __commonJS({
23056
23056
  return callback && callback(er, buffer);
23057
23057
  });
23058
23058
  };
23059
- this.open(path11, flag, 438, (er, handle_) => {
23059
+ this.open(path13, flag, 438, (er, handle_) => {
23060
23060
  if (er)
23061
23061
  return callback && callback(er);
23062
23062
  handle = handle_;
23063
23063
  const tryStat = (er2, st) => {
23064
23064
  if (er2) {
23065
- this.stat(path11, (er_, st_) => {
23065
+ this.stat(path13, (er_, st_) => {
23066
23066
  if (er_) {
23067
23067
  return this.close(handle, () => {
23068
23068
  callback && callback(er2);
@@ -23083,7 +23083,7 @@ var require_SFTP = __commonJS({
23083
23083
  this.fstat(handle, tryStat);
23084
23084
  });
23085
23085
  }
23086
- writeFile(path11, data, options, callback_) {
23086
+ writeFile(path13, data, options, callback_) {
23087
23087
  if (this.server)
23088
23088
  throw new Error("Client-only method called in server mode");
23089
23089
  let callback;
@@ -23102,7 +23102,7 @@ var require_SFTP = __commonJS({
23102
23102
  if (options.encoding && !Buffer.isEncoding(options.encoding))
23103
23103
  throw new Error(`Unknown encoding: ${options.encoding}`);
23104
23104
  const flag = options.flag || "w";
23105
- this.open(path11, flag, options.mode, (openErr, handle) => {
23105
+ this.open(path13, flag, options.mode, (openErr, handle) => {
23106
23106
  if (openErr) {
23107
23107
  callback && callback(openErr);
23108
23108
  } else {
@@ -23111,7 +23111,7 @@ var require_SFTP = __commonJS({
23111
23111
  if (position === null) {
23112
23112
  const tryStat = (er, st) => {
23113
23113
  if (er) {
23114
- this.stat(path11, (er_, st_) => {
23114
+ this.stat(path13, (er_, st_) => {
23115
23115
  if (er_) {
23116
23116
  return this.close(handle, () => {
23117
23117
  callback && callback(er);
@@ -23130,7 +23130,7 @@ var require_SFTP = __commonJS({
23130
23130
  }
23131
23131
  });
23132
23132
  }
23133
- appendFile(path11, data, options, callback_) {
23133
+ appendFile(path13, data, options, callback_) {
23134
23134
  if (this.server)
23135
23135
  throw new Error("Client-only method called in server mode");
23136
23136
  let callback;
@@ -23148,12 +23148,12 @@ var require_SFTP = __commonJS({
23148
23148
  throw new TypeError("Bad arguments");
23149
23149
  if (!options.flag)
23150
23150
  options = Object.assign({ flag: "a" }, options);
23151
- this.writeFile(path11, data, options, callback);
23151
+ this.writeFile(path13, data, options, callback);
23152
23152
  }
23153
- exists(path11, cb) {
23153
+ exists(path13, cb) {
23154
23154
  if (this.server)
23155
23155
  throw new Error("Client-only method called in server mode");
23156
- this.stat(path11, (err) => {
23156
+ this.stat(path13, (err) => {
23157
23157
  cb && cb(err ? false : true);
23158
23158
  });
23159
23159
  }
@@ -23196,7 +23196,7 @@ var require_SFTP = __commonJS({
23196
23196
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} RENAME`
23197
23197
  );
23198
23198
  }
23199
- mkdir(path11, attrs, cb) {
23199
+ mkdir(path13, attrs, cb) {
23200
23200
  if (this.server)
23201
23201
  throw new Error("Client-only method called in server mode");
23202
23202
  let flags = 0;
@@ -23210,7 +23210,7 @@ var require_SFTP = __commonJS({
23210
23210
  flags = attrs.flags;
23211
23211
  attrsLen = attrs.nb;
23212
23212
  }
23213
- const pathLen = Buffer.byteLength(path11);
23213
+ const pathLen = Buffer.byteLength(path13);
23214
23214
  let p = 9;
23215
23215
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen);
23216
23216
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23218,7 +23218,7 @@ var require_SFTP = __commonJS({
23218
23218
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23219
23219
  writeUInt32BE(buf, reqid, 5);
23220
23220
  writeUInt32BE(buf, pathLen, p);
23221
- buf.utf8Write(path11, p += 4, pathLen);
23221
+ buf.utf8Write(path13, p += 4, pathLen);
23222
23222
  writeUInt32BE(buf, flags, p += pathLen);
23223
23223
  if (attrsLen) {
23224
23224
  p += 4;
@@ -23234,10 +23234,10 @@ var require_SFTP = __commonJS({
23234
23234
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} MKDIR`
23235
23235
  );
23236
23236
  }
23237
- rmdir(path11, cb) {
23237
+ rmdir(path13, cb) {
23238
23238
  if (this.server)
23239
23239
  throw new Error("Client-only method called in server mode");
23240
- const pathLen = Buffer.byteLength(path11);
23240
+ const pathLen = Buffer.byteLength(path13);
23241
23241
  let p = 9;
23242
23242
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
23243
23243
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23245,7 +23245,7 @@ var require_SFTP = __commonJS({
23245
23245
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23246
23246
  writeUInt32BE(buf, reqid, 5);
23247
23247
  writeUInt32BE(buf, pathLen, p);
23248
- buf.utf8Write(path11, p += 4, pathLen);
23248
+ buf.utf8Write(path13, p += 4, pathLen);
23249
23249
  this._requests[reqid] = { cb };
23250
23250
  const isBuffered = sendOrBuffer(this, buf);
23251
23251
  this._debug && this._debug(
@@ -23335,10 +23335,10 @@ var require_SFTP = __commonJS({
23335
23335
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} FSTAT`
23336
23336
  );
23337
23337
  }
23338
- stat(path11, cb) {
23338
+ stat(path13, cb) {
23339
23339
  if (this.server)
23340
23340
  throw new Error("Client-only method called in server mode");
23341
- const pathLen = Buffer.byteLength(path11);
23341
+ const pathLen = Buffer.byteLength(path13);
23342
23342
  let p = 9;
23343
23343
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
23344
23344
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23346,17 +23346,17 @@ var require_SFTP = __commonJS({
23346
23346
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23347
23347
  writeUInt32BE(buf, reqid, 5);
23348
23348
  writeUInt32BE(buf, pathLen, p);
23349
- buf.utf8Write(path11, p += 4, pathLen);
23349
+ buf.utf8Write(path13, p += 4, pathLen);
23350
23350
  this._requests[reqid] = { cb };
23351
23351
  const isBuffered = sendOrBuffer(this, buf);
23352
23352
  this._debug && this._debug(
23353
23353
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} STAT`
23354
23354
  );
23355
23355
  }
23356
- lstat(path11, cb) {
23356
+ lstat(path13, cb) {
23357
23357
  if (this.server)
23358
23358
  throw new Error("Client-only method called in server mode");
23359
- const pathLen = Buffer.byteLength(path11);
23359
+ const pathLen = Buffer.byteLength(path13);
23360
23360
  let p = 9;
23361
23361
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
23362
23362
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23364,17 +23364,17 @@ var require_SFTP = __commonJS({
23364
23364
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23365
23365
  writeUInt32BE(buf, reqid, 5);
23366
23366
  writeUInt32BE(buf, pathLen, p);
23367
- buf.utf8Write(path11, p += 4, pathLen);
23367
+ buf.utf8Write(path13, p += 4, pathLen);
23368
23368
  this._requests[reqid] = { cb };
23369
23369
  const isBuffered = sendOrBuffer(this, buf);
23370
23370
  this._debug && this._debug(
23371
23371
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} LSTAT`
23372
23372
  );
23373
23373
  }
23374
- opendir(path11, cb) {
23374
+ opendir(path13, cb) {
23375
23375
  if (this.server)
23376
23376
  throw new Error("Client-only method called in server mode");
23377
- const pathLen = Buffer.byteLength(path11);
23377
+ const pathLen = Buffer.byteLength(path13);
23378
23378
  let p = 9;
23379
23379
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
23380
23380
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23382,14 +23382,14 @@ var require_SFTP = __commonJS({
23382
23382
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23383
23383
  writeUInt32BE(buf, reqid, 5);
23384
23384
  writeUInt32BE(buf, pathLen, p);
23385
- buf.utf8Write(path11, p += 4, pathLen);
23385
+ buf.utf8Write(path13, p += 4, pathLen);
23386
23386
  this._requests[reqid] = { cb };
23387
23387
  const isBuffered = sendOrBuffer(this, buf);
23388
23388
  this._debug && this._debug(
23389
23389
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} OPENDIR`
23390
23390
  );
23391
23391
  }
23392
- setstat(path11, attrs, cb) {
23392
+ setstat(path13, attrs, cb) {
23393
23393
  if (this.server)
23394
23394
  throw new Error("Client-only method called in server mode");
23395
23395
  let flags = 0;
@@ -23401,7 +23401,7 @@ var require_SFTP = __commonJS({
23401
23401
  } else if (typeof attrs === "function") {
23402
23402
  cb = attrs;
23403
23403
  }
23404
- const pathLen = Buffer.byteLength(path11);
23404
+ const pathLen = Buffer.byteLength(path13);
23405
23405
  let p = 9;
23406
23406
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen);
23407
23407
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23409,7 +23409,7 @@ var require_SFTP = __commonJS({
23409
23409
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23410
23410
  writeUInt32BE(buf, reqid, 5);
23411
23411
  writeUInt32BE(buf, pathLen, p);
23412
- buf.utf8Write(path11, p += 4, pathLen);
23412
+ buf.utf8Write(path13, p += 4, pathLen);
23413
23413
  writeUInt32BE(buf, flags, p += pathLen);
23414
23414
  if (attrsLen) {
23415
23415
  p += 4;
@@ -23469,8 +23469,8 @@ var require_SFTP = __commonJS({
23469
23469
  mtime: toUnixTimestamp(mtime)
23470
23470
  }, cb);
23471
23471
  }
23472
- utimes(path11, atime, mtime, cb) {
23473
- return this.setstat(path11, {
23472
+ utimes(path13, atime, mtime, cb) {
23473
+ return this.setstat(path13, {
23474
23474
  atime: toUnixTimestamp(atime),
23475
23475
  mtime: toUnixTimestamp(mtime)
23476
23476
  }, cb);
@@ -23481,8 +23481,8 @@ var require_SFTP = __commonJS({
23481
23481
  gid
23482
23482
  }, cb);
23483
23483
  }
23484
- chown(path11, uid, gid, cb) {
23485
- return this.setstat(path11, {
23484
+ chown(path13, uid, gid, cb) {
23485
+ return this.setstat(path13, {
23486
23486
  uid,
23487
23487
  gid
23488
23488
  }, cb);
@@ -23492,15 +23492,15 @@ var require_SFTP = __commonJS({
23492
23492
  mode
23493
23493
  }, cb);
23494
23494
  }
23495
- chmod(path11, mode, cb) {
23496
- return this.setstat(path11, {
23495
+ chmod(path13, mode, cb) {
23496
+ return this.setstat(path13, {
23497
23497
  mode
23498
23498
  }, cb);
23499
23499
  }
23500
- readlink(path11, cb) {
23500
+ readlink(path13, cb) {
23501
23501
  if (this.server)
23502
23502
  throw new Error("Client-only method called in server mode");
23503
- const pathLen = Buffer.byteLength(path11);
23503
+ const pathLen = Buffer.byteLength(path13);
23504
23504
  let p = 9;
23505
23505
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
23506
23506
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23508,7 +23508,7 @@ var require_SFTP = __commonJS({
23508
23508
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23509
23509
  writeUInt32BE(buf, reqid, 5);
23510
23510
  writeUInt32BE(buf, pathLen, p);
23511
- buf.utf8Write(path11, p += 4, pathLen);
23511
+ buf.utf8Write(path13, p += 4, pathLen);
23512
23512
  this._requests[reqid] = {
23513
23513
  cb: (err, names) => {
23514
23514
  if (typeof cb !== "function")
@@ -23553,10 +23553,10 @@ var require_SFTP = __commonJS({
23553
23553
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} SYMLINK`
23554
23554
  );
23555
23555
  }
23556
- realpath(path11, cb) {
23556
+ realpath(path13, cb) {
23557
23557
  if (this.server)
23558
23558
  throw new Error("Client-only method called in server mode");
23559
- const pathLen = Buffer.byteLength(path11);
23559
+ const pathLen = Buffer.byteLength(path13);
23560
23560
  let p = 9;
23561
23561
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen);
23562
23562
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23564,7 +23564,7 @@ var require_SFTP = __commonJS({
23564
23564
  const reqid = this._writeReqid = this._writeReqid + 1 & MAX_REQID;
23565
23565
  writeUInt32BE(buf, reqid, 5);
23566
23566
  writeUInt32BE(buf, pathLen, p);
23567
- buf.utf8Write(path11, p += 4, pathLen);
23567
+ buf.utf8Write(path13, p += 4, pathLen);
23568
23568
  this._requests[reqid] = {
23569
23569
  cb: (err, names) => {
23570
23570
  if (typeof cb !== "function")
@@ -23609,13 +23609,13 @@ var require_SFTP = __commonJS({
23609
23609
  this._debug(`SFTP: Outbound: ${which} posix-rename@openssh.com`);
23610
23610
  }
23611
23611
  }
23612
- ext_openssh_statvfs(path11, cb) {
23612
+ ext_openssh_statvfs(path13, cb) {
23613
23613
  if (this.server)
23614
23614
  throw new Error("Client-only method called in server mode");
23615
23615
  const ext = this._extensions["statvfs@openssh.com"];
23616
23616
  if (!ext || ext !== "2")
23617
23617
  throw new Error("Server does not support this extended request");
23618
- const pathLen = Buffer.byteLength(path11);
23618
+ const pathLen = Buffer.byteLength(path13);
23619
23619
  let p = 9;
23620
23620
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 19 + 4 + pathLen);
23621
23621
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23625,7 +23625,7 @@ var require_SFTP = __commonJS({
23625
23625
  writeUInt32BE(buf, 19, p);
23626
23626
  buf.utf8Write("statvfs@openssh.com", p += 4, 19);
23627
23627
  writeUInt32BE(buf, pathLen, p += 19);
23628
- buf.utf8Write(path11, p += 4, pathLen);
23628
+ buf.utf8Write(path13, p += 4, pathLen);
23629
23629
  this._requests[reqid] = { extended: "statvfs@openssh.com", cb };
23630
23630
  const isBuffered = sendOrBuffer(this, buf);
23631
23631
  if (this._debug) {
@@ -23711,7 +23711,7 @@ var require_SFTP = __commonJS({
23711
23711
  `SFTP: Outbound: ${isBuffered ? "Buffered" : "Sending"} fsync@openssh.com`
23712
23712
  );
23713
23713
  }
23714
- ext_openssh_lsetstat(path11, attrs, cb) {
23714
+ ext_openssh_lsetstat(path13, attrs, cb) {
23715
23715
  if (this.server)
23716
23716
  throw new Error("Client-only method called in server mode");
23717
23717
  const ext = this._extensions["lsetstat@openssh.com"];
@@ -23726,7 +23726,7 @@ var require_SFTP = __commonJS({
23726
23726
  } else if (typeof attrs === "function") {
23727
23727
  cb = attrs;
23728
23728
  }
23729
- const pathLen = Buffer.byteLength(path11);
23729
+ const pathLen = Buffer.byteLength(path13);
23730
23730
  let p = 9;
23731
23731
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + pathLen + 4 + attrsLen);
23732
23732
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23736,7 +23736,7 @@ var require_SFTP = __commonJS({
23736
23736
  writeUInt32BE(buf, 20, p);
23737
23737
  buf.utf8Write("lsetstat@openssh.com", p += 4, 20);
23738
23738
  writeUInt32BE(buf, pathLen, p += 20);
23739
- buf.utf8Write(path11, p += 4, pathLen);
23739
+ buf.utf8Write(path13, p += 4, pathLen);
23740
23740
  writeUInt32BE(buf, flags, p += pathLen);
23741
23741
  if (attrsLen) {
23742
23742
  p += 4;
@@ -23753,13 +23753,13 @@ var require_SFTP = __commonJS({
23753
23753
  this._debug(`SFTP: Outbound: ${status5} lsetstat@openssh.com`);
23754
23754
  }
23755
23755
  }
23756
- ext_openssh_expandPath(path11, cb) {
23756
+ ext_openssh_expandPath(path13, cb) {
23757
23757
  if (this.server)
23758
23758
  throw new Error("Client-only method called in server mode");
23759
23759
  const ext = this._extensions["expand-path@openssh.com"];
23760
23760
  if (ext !== "1")
23761
23761
  throw new Error("Server does not support this extended request");
23762
- const pathLen = Buffer.byteLength(path11);
23762
+ const pathLen = Buffer.byteLength(path13);
23763
23763
  let p = 9;
23764
23764
  const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 23 + 4 + pathLen);
23765
23765
  writeUInt32BE(buf, buf.length - 4, 0);
@@ -23769,7 +23769,7 @@ var require_SFTP = __commonJS({
23769
23769
  writeUInt32BE(buf, 23, p);
23770
23770
  buf.utf8Write("expand-path@openssh.com", p += 4, 23);
23771
23771
  writeUInt32BE(buf, pathLen, p += 20);
23772
- buf.utf8Write(path11, p += 4, pathLen);
23772
+ buf.utf8Write(path13, p += 4, pathLen);
23773
23773
  this._requests[reqid] = {
23774
23774
  cb: (err, names) => {
23775
23775
  if (typeof cb !== "function")
@@ -25047,12 +25047,12 @@ var require_SFTP = __commonJS({
25047
25047
  [REQUEST.LSTAT]: (sftp, payload) => {
25048
25048
  bufferParser.init(payload, 1);
25049
25049
  const reqID = bufferParser.readUInt32BE();
25050
- const path11 = bufferParser.readString(true);
25050
+ const path13 = bufferParser.readString(true);
25051
25051
  bufferParser.clear();
25052
- if (path11 === void 0)
25052
+ if (path13 === void 0)
25053
25053
  return doFatalSFTPError(sftp, "Malformed LSTAT packet");
25054
25054
  sftp._debug && sftp._debug(`SFTP: Inbound: Received LSTAT (id:${reqID})`);
25055
- if (!sftp.emit("LSTAT", reqID, path11)) {
25055
+ if (!sftp.emit("LSTAT", reqID, path13)) {
25056
25056
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25057
25057
  }
25058
25058
  },
@@ -25071,13 +25071,13 @@ var require_SFTP = __commonJS({
25071
25071
  [REQUEST.SETSTAT]: (sftp, payload) => {
25072
25072
  bufferParser.init(payload, 1);
25073
25073
  const reqID = bufferParser.readUInt32BE();
25074
- const path11 = bufferParser.readString(true);
25074
+ const path13 = bufferParser.readString(true);
25075
25075
  const attrs = readAttrs(sftp._biOpt);
25076
25076
  bufferParser.clear();
25077
25077
  if (attrs === void 0)
25078
25078
  return doFatalSFTPError(sftp, "Malformed SETSTAT packet");
25079
25079
  sftp._debug && sftp._debug(`SFTP: Inbound: Received SETSTAT (id:${reqID})`);
25080
- if (!sftp.emit("SETSTAT", reqID, path11, attrs)) {
25080
+ if (!sftp.emit("SETSTAT", reqID, path13, attrs)) {
25081
25081
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25082
25082
  }
25083
25083
  },
@@ -25099,12 +25099,12 @@ var require_SFTP = __commonJS({
25099
25099
  [REQUEST.OPENDIR]: (sftp, payload) => {
25100
25100
  bufferParser.init(payload, 1);
25101
25101
  const reqID = bufferParser.readUInt32BE();
25102
- const path11 = bufferParser.readString(true);
25102
+ const path13 = bufferParser.readString(true);
25103
25103
  bufferParser.clear();
25104
- if (path11 === void 0)
25104
+ if (path13 === void 0)
25105
25105
  return doFatalSFTPError(sftp, "Malformed OPENDIR packet");
25106
25106
  sftp._debug && sftp._debug(`SFTP: Inbound: Received OPENDIR (id:${reqID})`);
25107
- if (!sftp.emit("OPENDIR", reqID, path11)) {
25107
+ if (!sftp.emit("OPENDIR", reqID, path13)) {
25108
25108
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25109
25109
  }
25110
25110
  },
@@ -25123,63 +25123,63 @@ var require_SFTP = __commonJS({
25123
25123
  [REQUEST.REMOVE]: (sftp, payload) => {
25124
25124
  bufferParser.init(payload, 1);
25125
25125
  const reqID = bufferParser.readUInt32BE();
25126
- const path11 = bufferParser.readString(true);
25126
+ const path13 = bufferParser.readString(true);
25127
25127
  bufferParser.clear();
25128
- if (path11 === void 0)
25128
+ if (path13 === void 0)
25129
25129
  return doFatalSFTPError(sftp, "Malformed REMOVE packet");
25130
25130
  sftp._debug && sftp._debug(`SFTP: Inbound: Received REMOVE (id:${reqID})`);
25131
- if (!sftp.emit("REMOVE", reqID, path11)) {
25131
+ if (!sftp.emit("REMOVE", reqID, path13)) {
25132
25132
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25133
25133
  }
25134
25134
  },
25135
25135
  [REQUEST.MKDIR]: (sftp, payload) => {
25136
25136
  bufferParser.init(payload, 1);
25137
25137
  const reqID = bufferParser.readUInt32BE();
25138
- const path11 = bufferParser.readString(true);
25138
+ const path13 = bufferParser.readString(true);
25139
25139
  const attrs = readAttrs(sftp._biOpt);
25140
25140
  bufferParser.clear();
25141
25141
  if (attrs === void 0)
25142
25142
  return doFatalSFTPError(sftp, "Malformed MKDIR packet");
25143
25143
  sftp._debug && sftp._debug(`SFTP: Inbound: Received MKDIR (id:${reqID})`);
25144
- if (!sftp.emit("MKDIR", reqID, path11, attrs)) {
25144
+ if (!sftp.emit("MKDIR", reqID, path13, attrs)) {
25145
25145
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25146
25146
  }
25147
25147
  },
25148
25148
  [REQUEST.RMDIR]: (sftp, payload) => {
25149
25149
  bufferParser.init(payload, 1);
25150
25150
  const reqID = bufferParser.readUInt32BE();
25151
- const path11 = bufferParser.readString(true);
25151
+ const path13 = bufferParser.readString(true);
25152
25152
  bufferParser.clear();
25153
- if (path11 === void 0)
25153
+ if (path13 === void 0)
25154
25154
  return doFatalSFTPError(sftp, "Malformed RMDIR packet");
25155
25155
  sftp._debug && sftp._debug(`SFTP: Inbound: Received RMDIR (id:${reqID})`);
25156
- if (!sftp.emit("RMDIR", reqID, path11)) {
25156
+ if (!sftp.emit("RMDIR", reqID, path13)) {
25157
25157
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25158
25158
  }
25159
25159
  },
25160
25160
  [REQUEST.REALPATH]: (sftp, payload) => {
25161
25161
  bufferParser.init(payload, 1);
25162
25162
  const reqID = bufferParser.readUInt32BE();
25163
- const path11 = bufferParser.readString(true);
25163
+ const path13 = bufferParser.readString(true);
25164
25164
  bufferParser.clear();
25165
- if (path11 === void 0)
25165
+ if (path13 === void 0)
25166
25166
  return doFatalSFTPError(sftp, "Malformed REALPATH packet");
25167
25167
  sftp._debug && sftp._debug(
25168
25168
  `SFTP: Inbound: Received REALPATH (id:${reqID})`
25169
25169
  );
25170
- if (!sftp.emit("REALPATH", reqID, path11)) {
25170
+ if (!sftp.emit("REALPATH", reqID, path13)) {
25171
25171
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25172
25172
  }
25173
25173
  },
25174
25174
  [REQUEST.STAT]: (sftp, payload) => {
25175
25175
  bufferParser.init(payload, 1);
25176
25176
  const reqID = bufferParser.readUInt32BE();
25177
- const path11 = bufferParser.readString(true);
25177
+ const path13 = bufferParser.readString(true);
25178
25178
  bufferParser.clear();
25179
- if (path11 === void 0)
25179
+ if (path13 === void 0)
25180
25180
  return doFatalSFTPError(sftp, "Malformed STAT packet");
25181
25181
  sftp._debug && sftp._debug(`SFTP: Inbound: Received STAT (id:${reqID})`);
25182
- if (!sftp.emit("STAT", reqID, path11)) {
25182
+ if (!sftp.emit("STAT", reqID, path13)) {
25183
25183
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25184
25184
  }
25185
25185
  },
@@ -25199,14 +25199,14 @@ var require_SFTP = __commonJS({
25199
25199
  [REQUEST.READLINK]: (sftp, payload) => {
25200
25200
  bufferParser.init(payload, 1);
25201
25201
  const reqID = bufferParser.readUInt32BE();
25202
- const path11 = bufferParser.readString(true);
25202
+ const path13 = bufferParser.readString(true);
25203
25203
  bufferParser.clear();
25204
- if (path11 === void 0)
25204
+ if (path13 === void 0)
25205
25205
  return doFatalSFTPError(sftp, "Malformed READLINK packet");
25206
25206
  sftp._debug && sftp._debug(
25207
25207
  `SFTP: Inbound: Received READLINK (id:${reqID})`
25208
25208
  );
25209
- if (!sftp.emit("READLINK", reqID, path11)) {
25209
+ if (!sftp.emit("READLINK", reqID, path13)) {
25210
25210
  sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED);
25211
25211
  }
25212
25212
  },
@@ -25277,7 +25277,7 @@ var require_SFTP = __commonJS({
25277
25277
  function roundUpToMultipleOf8(n) {
25278
25278
  return n + 7 & ~7;
25279
25279
  }
25280
- function ReadStream(sftp, path11, options) {
25280
+ function ReadStream(sftp, path13, options) {
25281
25281
  if (options === void 0)
25282
25282
  options = {};
25283
25283
  else if (typeof options === "string")
@@ -25291,7 +25291,7 @@ var require_SFTP = __commonJS({
25291
25291
  options.emitClose = false;
25292
25292
  options.autoDestroy = false;
25293
25293
  ReadableStream2.call(this, options);
25294
- this.path = path11;
25294
+ this.path = path13;
25295
25295
  this.flags = options.flags === void 0 ? "r" : options.flags;
25296
25296
  this.mode = options.mode === void 0 ? 438 : options.mode;
25297
25297
  this.start = options.start;
@@ -25422,7 +25422,7 @@ var require_SFTP = __commonJS({
25422
25422
  },
25423
25423
  configurable: true
25424
25424
  });
25425
- function WriteStream(sftp, path11, options) {
25425
+ function WriteStream(sftp, path13, options) {
25426
25426
  if (options === void 0)
25427
25427
  options = {};
25428
25428
  else if (typeof options === "string")
@@ -25434,7 +25434,7 @@ var require_SFTP = __commonJS({
25434
25434
  options.emitClose = false;
25435
25435
  options.autoDestroy = false;
25436
25436
  WritableStream.call(this, options);
25437
- this.path = path11;
25437
+ this.path = path13;
25438
25438
  this.flags = options.flags === void 0 ? "w" : options.flags;
25439
25439
  this.mode = options.mode === void 0 ? 438 : options.mode;
25440
25440
  this.start = options.start;
@@ -26116,7 +26116,7 @@ var require_client = __commonJS({
26116
26116
  "../../node_modules/.pnpm/ssh2@1.17.0/node_modules/ssh2/lib/client.js"(exports, module) {
26117
26117
  "use strict";
26118
26118
  var {
26119
- createHash,
26119
+ createHash: createHash2,
26120
26120
  getHashes,
26121
26121
  randomFillSync
26122
26122
  } = __require("crypto");
@@ -26338,7 +26338,7 @@ var require_client = __commonJS({
26338
26338
  }
26339
26339
  hostVerifier = (key, verify) => {
26340
26340
  if (hashAlgo)
26341
- key = createHash(hashAlgo).update(key).digest("hex");
26341
+ key = createHash2(hashAlgo).update(key).digest("hex");
26342
26342
  const ret = hashCb(key, verify);
26343
26343
  if (ret !== void 0)
26344
26344
  verify(ret);
@@ -29655,7 +29655,11 @@ function paths(dir) {
29655
29655
  log: path.join(dir, "runner.log"),
29656
29656
  claudeConfigDir: path.join(dir, "claude"),
29657
29657
  projectsDir: path.join(dir, "projects"),
29658
- stateFile: path.join(dir, "state.json")
29658
+ stateFile: path.join(dir, "state.json"),
29659
+ /** Each run's Claude Code settings file, `<runId>.json`, there only while the run lasts. */
29660
+ runsDir: path.join(dir, "runs"),
29661
+ /** The runner's bare mirrors of the projects' repositories: the only ones that talk to GitHub. */
29662
+ gitDir: path.join(dir, "git")
29659
29663
  };
29660
29664
  }
29661
29665
  function loadConfig(dir = defaultDir()) {
@@ -29693,8 +29697,8 @@ function parseArgs(argv) {
29693
29697
  }
29694
29698
 
29695
29699
  // src/commands.ts
29696
- import { readFileSync as readFileSync3 } from "node:fs";
29697
- import os3 from "node:os";
29700
+ import { readFileSync as readFileSync4 } from "node:fs";
29701
+ import os6 from "node:os";
29698
29702
  import { createInterface } from "node:readline/promises";
29699
29703
  import { Writable as Writable2 } from "node:stream";
29700
29704
 
@@ -29736,6 +29740,10 @@ var NotiamoApi = class {
29736
29740
  done(runId, body) {
29737
29741
  return this.json("POST", `/api/runner/runs/${encodeURIComponent(runId)}/done`, body);
29738
29742
  }
29743
+ /** A fresh GitHub token for the run's repository, when the spec's one is close to its end. */
29744
+ gitToken(runId) {
29745
+ return this.json("POST", `/api/runner/runs/${encodeURIComponent(runId)}/git-token`, {});
29746
+ }
29739
29747
  /** Uploads a gzip backup of what the run changed. */
29740
29748
  async backup(runId, gzip) {
29741
29749
  await this.call("POST", `/api/runner/runs/${encodeURIComponent(runId)}/backup`, gzip, "application/gzip");
@@ -29758,10 +29766,91 @@ var NotiamoApi = class {
29758
29766
 
29759
29767
  // src/engine/claude.ts
29760
29768
  import * as child_process from "node:child_process";
29769
+ import os2 from "node:os";
29761
29770
  var MAX_TEXT = 2e3;
29762
29771
  var KILL_GRACE_MS = 1e4;
29763
29772
  var CLOSE_GRACE_MS = 2e3;
29764
29773
  var cut = (s) => s.length > MAX_TEXT ? s.slice(0, MAX_TEXT) : s;
29774
+ var ENV_ALLOW = [
29775
+ "PATH",
29776
+ "HOME",
29777
+ "USER",
29778
+ "LOGNAME",
29779
+ "SHELL",
29780
+ "LANG",
29781
+ "LC_ALL",
29782
+ "LC_CTYPE",
29783
+ "TERM",
29784
+ "TMPDIR",
29785
+ "TZ",
29786
+ // A machine behind a proxy or with its own CA needs these to reach anything.
29787
+ "HTTP_PROXY",
29788
+ "HTTPS_PROXY",
29789
+ "NO_PROXY",
29790
+ "http_proxy",
29791
+ "https_proxy",
29792
+ "no_proxy",
29793
+ "NODE_EXTRA_CA_CERTS",
29794
+ "SSL_CERT_FILE",
29795
+ "SSL_CERT_DIR"
29796
+ ];
29797
+ var ENV_ALLOW_PREFIX = ["XDG_"];
29798
+ var ENV_ALLOW_WINDOWS = [
29799
+ "PATHEXT",
29800
+ "SYSTEMROOT",
29801
+ "SYSTEMDRIVE",
29802
+ "WINDIR",
29803
+ "COMSPEC",
29804
+ "USERPROFILE",
29805
+ "USERNAME",
29806
+ "HOMEDRIVE",
29807
+ "HOMEPATH",
29808
+ "APPDATA",
29809
+ "LOCALAPPDATA",
29810
+ "PROGRAMDATA",
29811
+ "PROGRAMFILES",
29812
+ "PROGRAMFILES(X86)",
29813
+ "TEMP",
29814
+ "TMP"
29815
+ ];
29816
+ function baseEnv(env, windows) {
29817
+ const out = {};
29818
+ for (const [k, v] of Object.entries(env)) {
29819
+ if (v === void 0) continue;
29820
+ const key = windows ? k.toUpperCase() : k;
29821
+ if (!(ENV_ALLOW.includes(key) || ENV_ALLOW_PREFIX.some((p) => key.startsWith(p)) || windows && ENV_ALLOW_WINDOWS.includes(key))) continue;
29822
+ if (PROXY_URLS.includes(key.toUpperCase())) {
29823
+ const url2 = withoutUserinfo(v);
29824
+ if (url2 !== null) out[k] = url2;
29825
+ continue;
29826
+ }
29827
+ out[k] = v;
29828
+ }
29829
+ return out;
29830
+ }
29831
+ var PROXY_URLS = ["HTTP_PROXY", "HTTPS_PROXY"];
29832
+ function withoutUserinfo(value) {
29833
+ try {
29834
+ const u = new URL(value);
29835
+ u.username = "";
29836
+ u.password = "";
29837
+ return u.toString();
29838
+ } catch {
29839
+ return null;
29840
+ }
29841
+ }
29842
+ var AGENT_GIT_ENV = {
29843
+ GIT_CONFIG_GLOBAL: os2.devNull,
29844
+ GIT_CONFIG_NOSYSTEM: "1",
29845
+ GIT_TERMINAL_PROMPT: "0",
29846
+ GIT_AUTHOR_NAME: "Notiamo agent",
29847
+ GIT_AUTHOR_EMAIL: "agent@notiamo.com",
29848
+ GIT_COMMITTER_NAME: "Notiamo agent",
29849
+ GIT_COMMITTER_EMAIL: "agent@notiamo.com"
29850
+ };
29851
+ function isSandboxUnavailable(line) {
29852
+ return /sandbox/i.test(line) && /(unavailable|not available|stopped being available|failed to (start|initiali[sz]e)|requires wsl2|bubblewrap|bwrap|socat)/i.test(line);
29853
+ }
29765
29854
  function claudeEngine(deps = {}) {
29766
29855
  const spawn4 = deps.spawn ?? child_process.spawn;
29767
29856
  const bin = deps.claudeBin ?? "claude";
@@ -29782,13 +29871,22 @@ function claudeEngine(deps = {}) {
29782
29871
  // tokens never show on the command line (`ps`). --mcp-config is variadic: the next argument must be a --flag.
29783
29872
  "--mcp-config",
29784
29873
  JSON.stringify(mcpConfig(o)),
29785
- // Only Notiamo's servers: permissions are skipped, so no .mcp.json of the cloned repo or of the config dir may load.
29874
+ // Only Notiamo's servers: no .mcp.json of the cloned repo or of the config dir may load.
29786
29875
  "--strict-mcp-config",
29787
- "--dangerously-skip-permissions"
29876
+ // Edits are accepted only inside the working folder; the settings file allows every (sandboxed) Bash command and
29877
+ // Notiamo's tools. Anything else would prompt, and in -p mode a prompt is a refusal.
29878
+ "--permission-mode",
29879
+ "acceptEdits",
29880
+ // The run's settings file (allow and deny rules, read and write block, shell sandbox): only paths and domains, never a secret.
29881
+ ...o.settingsFile ? ["--settings", o.settingsFile] : [],
29882
+ // Only the runner's own user settings (in CLAUDE_CONFIG_DIR): the clone's .claude/settings*.json never load.
29883
+ "--setting-sources",
29884
+ "user"
29788
29885
  ];
29789
29886
  const engineEnv = (o) => ({
29790
- ...process.env,
29887
+ ...baseEnv(deps.env ?? process.env, windows),
29791
29888
  ...o.env,
29889
+ ...AGENT_GIT_ENV,
29792
29890
  CLAUDE_CONFIG_DIR: o.claudeConfigDir,
29793
29891
  NOTIAMO_RUN_TOKEN: o.mcp.token,
29794
29892
  ...o.helper ? { NOTIAMO_WP_TOKEN: o.helper.token } : {}
@@ -29799,6 +29897,7 @@ function claudeEngine(deps = {}) {
29799
29897
  reported: false,
29800
29898
  result: false,
29801
29899
  resultError: void 0,
29900
+ sandboxError: false,
29802
29901
  limitAt: void 0,
29803
29902
  timedOut: false,
29804
29903
  spawnError: void 0,
@@ -29881,7 +29980,9 @@ function claudeEngine(deps = {}) {
29881
29980
  }
29882
29981
  lines(child.stdout, onLine);
29883
29982
  lines(child.stderr, (l) => {
29884
- if (l.trim()) onText("error", l, true);
29983
+ if (!l.trim()) return;
29984
+ if (isSandboxUnavailable(l)) st.sandboxError = true;
29985
+ onText("error", l, true);
29885
29986
  });
29886
29987
  for (const s of [child.stdout, child.stderr]) s?.on("error", (e) => emit({ kind: "error", text: cut(`stream error: ${e.message}`) }));
29887
29988
  const kill = (sig) => {
@@ -29908,6 +30009,12 @@ function claudeEngine(deps = {}) {
29908
30009
  clearTimeout(timer);
29909
30010
  clearTimeout(killTimer);
29910
30011
  clearTimeout(closeTimer);
30012
+ if (!windows && child.pid) {
30013
+ try {
30014
+ processKill(-child.pid, "SIGKILL");
30015
+ } catch {
30016
+ }
30017
+ }
29911
30018
  const usage = { costUsd: st.costUsd, inputTokens: st.inputTokens, outputTokens: st.outputTokens, sessionId: st.sessionId };
29912
30019
  const succeeded = st.result && !st.resultError;
29913
30020
  if (st.timedOut) return resolve({ outcome: "failed", error: "timeout", ...usage });
@@ -29916,6 +30023,7 @@ function claudeEngine(deps = {}) {
29916
30023
  if (st.resultError) return resolve({ outcome: "failed", error: st.resultError, ...usage });
29917
30024
  if (st.reported) return resolve({ outcome: "reported", ...usage });
29918
30025
  if (st.result) return resolve({ outcome: "ended", ...usage });
30026
+ if (code !== 0 && st.sandboxError) return resolve({ outcome: "failed", error: "sandbox_unavailable", ...usage });
29919
30027
  if (code !== 0) return resolve({ outcome: "failed", error: `claude exited with code ${code}`, ...usage });
29920
30028
  resolve({ outcome: "ended", ...usage });
29921
30029
  };
@@ -30044,15 +30152,15 @@ async function reportRun(mcp, args, fetchImpl) {
30044
30152
  // src/exec.ts
30045
30153
  import { spawn as spawn2 } from "node:child_process";
30046
30154
  var realExec = async (file2, args, opts) => {
30047
- const r = await run(file2, args, opts?.env, false);
30048
- if (r.code === 127 && process.platform === "win32") return run(file2, args.map(quoteForCmd), opts?.env, true);
30155
+ const r = await run(file2, args, opts?.env, false, opts?.timeout);
30156
+ if (r.code === 127 && process.platform === "win32") return run(file2, args.map(quoteForCmd), opts?.env, true, opts?.timeout);
30049
30157
  return r;
30050
30158
  };
30051
- function run(file2, args, env, shell) {
30159
+ function run(file2, args, env, shell, timeout) {
30052
30160
  return new Promise((resolve) => {
30053
30161
  let stdout = "";
30054
30162
  let stderr = "";
30055
- const child = spawn2(file2, args, { env: env ?? process.env, shell, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
30163
+ const child = spawn2(file2, args, { env: env ?? process.env, shell, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...timeout ? { timeout, killSignal: "SIGKILL" } : {} });
30056
30164
  child.stdout.on("data", (d) => {
30057
30165
  stdout += d.toString();
30058
30166
  });
@@ -30067,7 +30175,259 @@ var quoteForCmd = (a) => /^[\w.:/\\=-]+$/.test(a) ? a : `"${a.replace(/"/g, '""'
30067
30175
 
30068
30176
  // src/init.ts
30069
30177
  import { spawn as spawn3 } from "node:child_process";
30070
- import os2 from "node:os";
30178
+ import os4 from "node:os";
30179
+
30180
+ // src/sandbox.ts
30181
+ import { accessSync, constants as fsConstants, existsSync, readdirSync, realpathSync } from "node:fs";
30182
+ import os3 from "node:os";
30183
+ import path2 from "node:path";
30184
+ var ENGINE_MIN_VERSION = "2.1.314";
30185
+ var DEFAULT_DOMAINS = ["github.com", "api.github.com", "codeload.github.com", "objects.githubusercontent.com", "registry.npmjs.org"];
30186
+ function versionAtLeast(version2, min) {
30187
+ const parse3 = (v) => /(\d+)\.(\d+)\.(\d+)/.exec(v)?.slice(1).map(Number);
30188
+ const a = version2 ? parse3(version2) : void 0;
30189
+ const b = parse3(min);
30190
+ if (!a || !b) return false;
30191
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
30192
+ return true;
30193
+ }
30194
+ function engineTooOld(version2) {
30195
+ return !versionAtLeast(version2, ENGINE_MIN_VERSION);
30196
+ }
30197
+ var CREDENTIALS = [
30198
+ ".ssh",
30199
+ ".aws",
30200
+ ".gnupg",
30201
+ ".netrc",
30202
+ ".docker",
30203
+ path2.join(".config", "gh"),
30204
+ ".git-credentials",
30205
+ path2.join(".config", "git"),
30206
+ ".npmrc",
30207
+ ".pypirc",
30208
+ path2.join(".config", "gcloud"),
30209
+ path2.join(".config", "hub")
30210
+ ];
30211
+ var BASH_DENY = [
30212
+ "Bash(security *)",
30213
+ "Bash(git credential*)",
30214
+ "Bash(git-credential*)",
30215
+ "Bash(gh auth*)",
30216
+ "Bash(ssh-add *)",
30217
+ // A `*` may stand anywhere in a Bash rule: these also catch `git -c x=y credential fill`, a helper by its full path, `sh -c '…'`.
30218
+ "Bash(*credential*)",
30219
+ "Bash(*osxkeychain*)",
30220
+ "Bash(*libsecret*)",
30221
+ "Bash(*security find-*)",
30222
+ "Bash(*secret-tool*)",
30223
+ "Bash(*gh auth*)"
30224
+ ];
30225
+ var GIT_HELPERS = [
30226
+ "git-credential-osxkeychain",
30227
+ "git-credential-libsecret",
30228
+ "git-credential-store",
30229
+ "git-credential-cache",
30230
+ "git-credential-manager",
30231
+ "git-credential-manager-core"
30232
+ ];
30233
+ var HELPER_DIRS = [
30234
+ "/Library/Developer/CommandLineTools/usr/libexec/git-core",
30235
+ "/Applications/Xcode.app/Contents/Developer/usr/libexec/git-core",
30236
+ "/usr/libexec/git-core",
30237
+ "/usr/lib/git-core",
30238
+ "/usr/local/share/gcm-core",
30239
+ "/opt/homebrew/share/gcm-core",
30240
+ "/usr/local/bin"
30241
+ ];
30242
+ var KEYCHAIN_TOOLS = {
30243
+ darwin: ["/usr/bin/security", "/usr/bin/osascript"],
30244
+ linux: ["/usr/bin/secret-tool", "/usr/bin/gnome-keyring"]
30245
+ };
30246
+ async function credentialTools(exec, platform = process.platform) {
30247
+ const first = (out) => out.split(/\r?\n/).map((l) => l.trim()).find(Boolean) ?? null;
30248
+ const git = await exec("git", ["--exec-path"]).catch(() => null);
30249
+ const gh = await exec(platform === "win32" ? "where" : "which", ["gh"]).catch(() => null);
30250
+ const ghFound = gh?.code === 0 ? first(gh.stdout) : null;
30251
+ return {
30252
+ gitExecPath: git?.code === 0 ? first(git.stdout) : null,
30253
+ ghPath: ghFound && path2.isAbsolute(ghFound) ? realPath(ghFound) : null
30254
+ };
30255
+ }
30256
+ function findOnPath(name, dirs, platform) {
30257
+ const names = platform === "win32" ? [`${name}.exe`, `${name}.cmd`, name] : [name];
30258
+ for (const d of dirs) {
30259
+ for (const n of names) {
30260
+ const file2 = path2.join(d, n);
30261
+ try {
30262
+ accessSync(file2, fsConstants.X_OK);
30263
+ return realpathSync(file2);
30264
+ } catch {
30265
+ }
30266
+ }
30267
+ }
30268
+ return null;
30269
+ }
30270
+ function machineToolDirs(env = process.env, argv1 = process.argv[1], execPath = process.execPath, platform = process.platform) {
30271
+ const pathDirs = (env.PATH ?? env.Path ?? "").split(path2.delimiter).filter((d) => d && path2.isAbsolute(d));
30272
+ const dirs = pathDirs.map(realPath);
30273
+ for (const bin of ["git", "claude"]) {
30274
+ const found = findOnPath(bin, pathDirs, platform);
30275
+ if (found) dirs.push(path2.dirname(found));
30276
+ }
30277
+ dirs.push(path2.dirname(realPath(execPath)));
30278
+ if (argv1) {
30279
+ const install4 = path2.dirname(realPath(argv1));
30280
+ dirs.push(install4);
30281
+ if (path2.basename(install4) === "dist") dirs.push(path2.dirname(install4));
30282
+ }
30283
+ return [...new Set(dirs)];
30284
+ }
30285
+ function realListEntries(dir) {
30286
+ try {
30287
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() || e.isFile() || e.isSymbolicLink()).map((e) => ({ name: e.name, dir: e.isDirectory() }));
30288
+ } catch {
30289
+ return [];
30290
+ }
30291
+ }
30292
+ function realPath(p) {
30293
+ const abs = path2.resolve(p);
30294
+ const rest = [];
30295
+ for (let cur = abs; ; ) {
30296
+ try {
30297
+ return path2.join(realpathSync(cur), ...rest);
30298
+ } catch {
30299
+ const parent = path2.dirname(cur);
30300
+ if (parent === cur) return abs;
30301
+ rest.unshift(path2.basename(cur));
30302
+ cur = parent;
30303
+ }
30304
+ }
30305
+ }
30306
+ function rulePath(p) {
30307
+ const posix = p.replace(/\\/g, "/").replace(/^([A-Za-z]):/, (_m, d) => `/${d.toLowerCase()}`);
30308
+ return `/${posix}`;
30309
+ }
30310
+ function credentialBinaries(o, toolDirs, listEntries, exists2) {
30311
+ const out = [];
30312
+ const gitReal = /* @__PURE__ */ new Set();
30313
+ const scan = [];
30314
+ const execDir = o.gitExecPath && path2.isAbsolute(o.gitExecPath) ? realPath(o.gitExecPath) : null;
30315
+ for (const d of /* @__PURE__ */ new Set([...execDir ? [execDir] : [], ...toolDirs, ...HELPER_DIRS.map(realPath)])) {
30316
+ const names = listEntries(d).map((e) => e.name);
30317
+ if (names.includes("git")) gitReal.add(realPath(path2.join(d, "git")));
30318
+ const helpers = names.filter((n) => n.startsWith("git-credential-"));
30319
+ scan.push({ dir: d, names: d === execDir ? [.../* @__PURE__ */ new Set([...helpers, ...GIT_HELPERS])] : helpers });
30320
+ }
30321
+ for (const { dir, names } of scan) {
30322
+ for (const n of names) {
30323
+ const own2 = path2.join(dir, n);
30324
+ const real = realPath(own2);
30325
+ out.push(gitReal.has(real) ? own2 : real);
30326
+ }
30327
+ }
30328
+ for (const t of KEYCHAIN_TOOLS[o.platform] ?? []) if (exists2(t)) out.push(realPath(t));
30329
+ if (o.ghPath && path2.isAbsolute(o.ghPath) && !gitReal.has(realPath(o.ghPath))) out.push(realPath(o.ghPath));
30330
+ return out;
30331
+ }
30332
+ function sandboxSettings(o) {
30333
+ const root = realPath(o.runnerRoot);
30334
+ const claudeDir = realPath(o.claudeConfigDir);
30335
+ const runDir = realPath(o.runDir);
30336
+ const home = realPath(o.home ?? os3.homedir());
30337
+ const at = (...parts) => path2.join(...parts);
30338
+ const edit = (p) => `Edit(${rulePath(p)})`;
30339
+ const gitHooks = at(runDir, ".git", "hooks");
30340
+ const gitConfig = at(runDir, ".git", "config");
30341
+ const listEntries = o.listEntries ?? realListEntries;
30342
+ const exists2 = o.exists ?? existsSync;
30343
+ const walked = [];
30344
+ const visited = /* @__PURE__ */ new Set();
30345
+ const holds = (dir, p) => p === dir || p.startsWith(dir.endsWith(path2.sep) ? dir : dir + path2.sep);
30346
+ const denyAllBut = (dir) => {
30347
+ if (visited.has(dir)) return;
30348
+ visited.add(dir);
30349
+ for (const e of listEntries(dir)) {
30350
+ const child = realPath(at(dir, e.name));
30351
+ if (child === runDir) continue;
30352
+ if (holds(child, runDir)) {
30353
+ if (holds(root, child) && child !== root) denyAllBut(child);
30354
+ } else if (child !== path2.parse(child).root) walked.push({ name: child, dir: e.dir });
30355
+ }
30356
+ };
30357
+ denyAllBut(root);
30358
+ const profiles = listEntries(at(root, "profiles")).filter((e) => e.dir).map((e) => at(root, "profiles", e.name));
30359
+ const known = [root, ...profiles].flatMap((d) => [realPath(at(d, "config.json")), realPath(at(d, "state.json"))]);
30360
+ const toolDirs = (o.toolDirs ?? machineToolDirs()).map(realPath);
30361
+ const denyRead = [...walked.map((e) => e.name), ...known];
30362
+ if (exists2(claudeDir)) denyRead.push(claudeDir);
30363
+ for (const c of CREDENTIALS) if (exists2(at(home, c))) denyRead.push(realPath(at(home, c)));
30364
+ denyRead.push(...credentialBinaries(o, toolDirs, listEntries, exists2));
30365
+ const holdsRun = (dir) => holds(dir, runDir);
30366
+ const editTree = (dir) => holdsRun(dir) ? [] : [edit(`${dir}/**`)];
30367
+ const deny = [
30368
+ edit(at(root, "config.json")),
30369
+ edit(at(root, "state.json")),
30370
+ edit(at(root, "runner.log")),
30371
+ ...editTree(at(root, "runs")),
30372
+ edit(at(root, "profiles", "*", "config.json")),
30373
+ edit(at(root, "profiles", "*", "state.json")),
30374
+ edit(`${at(root, "profiles", "*", "runs")}/**`),
30375
+ ...editTree(claudeDir),
30376
+ edit(`${at(root, "profiles", "*", "claude")}/**`),
30377
+ ...[".ssh", ".aws", ".gnupg", ".docker", ".config", ".npm", ".local", ".claude", ...o.platform === "darwin" ? ["Library"] : []].flatMap((d) => editTree(at(home, d))),
30378
+ ...[
30379
+ ".netrc",
30380
+ ".git-credentials",
30381
+ ".npmrc",
30382
+ ".pypirc",
30383
+ ".zshrc",
30384
+ ".zshenv",
30385
+ ".zprofile",
30386
+ ".bashrc",
30387
+ ".bash_profile",
30388
+ ".bash_login",
30389
+ ".profile",
30390
+ ".gitconfig"
30391
+ ].map((f) => edit(at(home, f))),
30392
+ edit(`${gitHooks}/**`),
30393
+ edit(gitConfig),
30394
+ edit(`${at(runDir, ".claude")}/**`),
30395
+ ...walked.flatMap((e) => e.dir ? editTree(e.name) : [edit(e.name)]),
30396
+ ...toolDirs.filter((d) => d !== path2.parse(d).root).flatMap(editTree),
30397
+ ...BASH_DENY
30398
+ ];
30399
+ const allow = [
30400
+ "Bash",
30401
+ "mcp__notiamo__*",
30402
+ "mcp__notiamo-wp__*",
30403
+ `Read(${rulePath(runDir)}/**)`,
30404
+ "Glob",
30405
+ "Grep",
30406
+ `Edit(${rulePath(runDir)}/**)`,
30407
+ "WebFetch(domain:github.com)"
30408
+ ];
30409
+ const permissions = { blockReadsOutsideWorkingDirectories: true, blockWritesOutsideWorkingDirectories: true, allow, deny: [...new Set(deny)] };
30410
+ if (o.platform === "win32") return { permissions };
30411
+ const domains = [...DEFAULT_DOMAINS, o.notiamoHost, ...o.wordpressHost ? [o.wordpressHost] : [], ...o.extraDomains].map((d) => d.trim().toLowerCase()).filter((d) => d.length > 0);
30412
+ return {
30413
+ permissions,
30414
+ sandbox: {
30415
+ enabled: true,
30416
+ failIfUnavailable: true,
30417
+ allowUnsandboxedCommands: false,
30418
+ filesystem: {
30419
+ denyRead: [...new Set(denyRead)],
30420
+ allowWrite: [runDir],
30421
+ denyWrite: [gitHooks, gitConfig]
30422
+ },
30423
+ network: {
30424
+ allowedDomains: [...new Set(domains)],
30425
+ // macOS: lets the engine's shell commands reach the run's local helper on 127.0.0.1 (no effect on Linux).
30426
+ ...o.wordpressHost ? { allowLocalBinding: true } : {}
30427
+ }
30428
+ }
30429
+ };
30430
+ }
30071
30431
 
30072
30432
  // src/service/darwin.ts
30073
30433
  var darwin_exports = {};
@@ -30079,8 +30439,8 @@ __export(darwin_exports, {
30079
30439
  uninstall: () => uninstall
30080
30440
  });
30081
30441
  import { setTimeout as delay } from "node:timers/promises";
30082
- import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2, existsSync } from "node:fs";
30083
- import path2 from "node:path";
30442
+ import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "node:fs";
30443
+ import path3 from "node:path";
30084
30444
 
30085
30445
  // src/service/common.ts
30086
30446
  var startArgs = (profile) => profile ? ["start", "--profile", profile] : ["start"];
@@ -30095,7 +30455,7 @@ function must(what, r) {
30095
30455
 
30096
30456
  // src/service/darwin.ts
30097
30457
  var label = (profile) => profile ? `com.notiamo.runner.${profile}` : "com.notiamo.runner";
30098
- var plistPath = (home, profile) => path2.join(home, "Library", "LaunchAgents", `${label(profile)}.plist`);
30458
+ var plistPath = (home, profile) => path3.join(home, "Library", "LaunchAgents", `${label(profile)}.plist`);
30099
30459
  var domain = (uid) => `gui/${uid}`;
30100
30460
  var BOOTSTRAP_TRIES = 5;
30101
30461
  var BOOTSTRAP_RETRY_MS = 500;
@@ -30135,7 +30495,7 @@ ${env}
30135
30495
  }
30136
30496
  async function install(o) {
30137
30497
  const file2 = plistPath(o.home, o.profile);
30138
- mkdirSync2(path2.dirname(file2), { recursive: true });
30498
+ mkdirSync2(path3.dirname(file2), { recursive: true });
30139
30499
  writeFileSync2(file2, renderPlist({ nodePath: o.nodePath, cliPath: o.cliPath, log: o.paths.log, env: o.env, profile: o.profile }));
30140
30500
  await o.exec("launchctl", ["bootout", domain(o.uid), file2]);
30141
30501
  const sleep = o.sleep ?? ((ms) => delay(ms));
@@ -30156,7 +30516,7 @@ async function uninstall(o) {
30156
30516
  rmSync(plistPath(o.home, o.profile), { force: true });
30157
30517
  }
30158
30518
  async function status(o) {
30159
- if (!existsSync(plistPath(o.home, o.profile))) return "not_installed";
30519
+ if (!existsSync2(plistPath(o.home, o.profile))) return "not_installed";
30160
30520
  const r = await o.exec("launchctl", ["print", `${domain(o.uid)}/${label(o.profile)}`]);
30161
30521
  return r.code === 0 ? "running" : "stopped";
30162
30522
  }
@@ -30170,10 +30530,10 @@ __export(linux_exports, {
30170
30530
  stop: () => stop2,
30171
30531
  uninstall: () => uninstall2
30172
30532
  });
30173
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
30174
- import path3 from "node:path";
30533
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
30534
+ import path4 from "node:path";
30175
30535
  var unit = (profile) => profile ? `notiamo-runner-${profile}` : "notiamo-runner";
30176
- var unitPath = (home, profile) => path3.join(home, ".config", "systemd", "user", `${unit(profile)}.service`);
30536
+ var unitPath = (home, profile) => path4.join(home, ".config", "systemd", "user", `${unit(profile)}.service`);
30177
30537
  var q = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
30178
30538
  function renderUnit(o) {
30179
30539
  const env = Object.entries(serviceEnv(o.env)).map(([k, v]) => `Environment=${q(`${k}=${v}`)}`).join("\n");
@@ -30195,7 +30555,7 @@ WantedBy=default.target
30195
30555
  }
30196
30556
  async function install2(o) {
30197
30557
  const file2 = unitPath(o.home, o.profile);
30198
- mkdirSync3(path3.dirname(file2), { recursive: true });
30558
+ mkdirSync3(path4.dirname(file2), { recursive: true });
30199
30559
  writeFileSync3(file2, renderUnit({ nodePath: o.nodePath, cliPath: o.cliPath, log: o.paths.log, env: o.env, profile: o.profile }));
30200
30560
  must("systemctl --user daemon-reload", await o.exec("systemctl", ["--user", "daemon-reload"]));
30201
30561
  must("systemctl --user enable", await o.exec("systemctl", ["--user", "enable", unit(o.profile)]));
@@ -30211,7 +30571,7 @@ async function uninstall2(o) {
30211
30571
  await o.exec("systemctl", ["--user", "daemon-reload"]);
30212
30572
  }
30213
30573
  async function status2(o) {
30214
- if (!existsSync2(unitPath(o.home, o.profile))) return "not_installed";
30574
+ if (!existsSync3(unitPath(o.home, o.profile))) return "not_installed";
30215
30575
  const r = await o.exec("systemctl", ["--user", "is-active", unit(o.profile)]);
30216
30576
  return r.stdout.trim() === "active" ? "running" : "stopped";
30217
30577
  }
@@ -30262,6 +30622,7 @@ var serviceStatus = async (o) => impl(o).status(withUid(o));
30262
30622
 
30263
30623
  // src/init.ts
30264
30624
  var DEFAULT_HOST = "https://my.notiamo.com";
30625
+ var KEYCHAIN_WARNING = "Warning: this user's keychain holds a GitHub login. A run on this machine could reach it. Remove it (Keychain Access, search github.com) or install the runner under a separate macOS user. The runner itself does not need it.";
30265
30626
  var runInteractive = (file2, args, env) => new Promise((resolve, reject) => {
30266
30627
  const child = spawn3(file2, args, { stdio: "inherit", env, shell: process.platform === "win32" });
30267
30628
  child.on("error", reject);
@@ -30281,11 +30642,27 @@ async function runInit(deps) {
30281
30642
  const token = (await deps.prompt("Runner token (made in Notiamo under Settings \u203A Agent runs \u203A Runners; nothing shows while you paste it): ", { hidden: true })).trim();
30282
30643
  if (!token.startsWith("ntr_")) throw new Error("That is not a runner token: it starts with ntr_. Make one in Notiamo: Settings \u203A Agent runs \u203A Runners.");
30283
30644
  const host = normalizeHost(await ask("Notiamo address: ", DEFAULT_HOST));
30284
- const hostname3 = deps.hostname ?? os2.hostname();
30645
+ const hostname3 = deps.hostname ?? os4.hostname();
30285
30646
  const name = await ask("Name of this runner: ", hostname3);
30286
30647
  const version2 = await deps.exec("claude", ["--version"], { env });
30287
30648
  if (version2.code !== 0) throw new Error("Claude Code is not installed. Install it first: https://claude.com/claude-code");
30288
30649
  const engineVersion = version2.stdout.trim().slice(0, 100) || "unknown";
30650
+ if (engineTooOld(engineVersion)) throw new Error(`Claude Code 2.1.314 or newer is needed (yours: ${engineVersion}). Update it: claude update`);
30651
+ const git = await deps.exec("git", ["--version"], { env });
30652
+ const gitVersion = git.code === 0 ? /git version (\d+)\.(\d+)/.exec(git.stdout) ?? null : null;
30653
+ if (!gitVersion || Number(gitVersion[1]) < 2 || Number(gitVersion[1]) === 2 && Number(gitVersion[2]) < 31) {
30654
+ const yours = git.code === 0 ? git.stdout.trim().replace(/^git version /, "").slice(0, 60) || "unknown" : "not installed";
30655
+ throw new Error(`git 2.31 or newer is needed (yours: ${yours}). Update git.`);
30656
+ }
30657
+ if (platform === "linux") {
30658
+ const bwrap = await deps.exec("which", ["bwrap"], { env });
30659
+ const socat = await deps.exec("which", ["socat"], { env });
30660
+ if (bwrap.code !== 0 || socat.code !== 0) {
30661
+ throw new Error("The sandbox needs bubblewrap and socat. Install them (Debian/Ubuntu: sudo apt install bubblewrap socat), then run init again.");
30662
+ }
30663
+ } else if (platform === "win32") {
30664
+ print("Note: on Windows without WSL2, shell commands of a run are not sandboxed; Claude's own file tools are still limited to the run folder.");
30665
+ }
30289
30666
  try {
30290
30667
  await new NotiamoApi({ host, token }, deps.fetch).hello({ version: VERSION, os: platform, engine: "claude", engineVersion });
30291
30668
  } catch (e) {
@@ -30299,13 +30676,18 @@ async function runInit(deps) {
30299
30676
  }
30300
30677
  const check2 = await deps.exec("claude", ["-p", "say ok"], { env: claudeEnv });
30301
30678
  if (check2.code !== 0) throw new Error("Claude Code is not signed in for the runner.");
30679
+ if (platform === "darwin") {
30680
+ const git2 = await deps.exec("security", ["find-internet-password", "-s", "github.com"], { env, timeout: 1e4 });
30681
+ const gh = await deps.exec("security", ["find-generic-password", "-s", "gh:github.com"], { env, timeout: 1e4 });
30682
+ if (git2.code === 0 || gh.code === 0) print(KEYCHAIN_WARNING);
30683
+ }
30302
30684
  saveConfig({ host, token, name, dataDir: dir, engine: "claude" }, dir);
30303
30685
  await installService({
30304
30686
  platform,
30305
30687
  profile: deps.profile,
30306
30688
  paths: p,
30307
30689
  exec: deps.exec,
30308
- home: deps.home ?? os2.homedir(),
30690
+ home: deps.home ?? os4.homedir(),
30309
30691
  env,
30310
30692
  nodePath: deps.nodePath ?? process.execPath,
30311
30693
  cliPath: deps.cliPath ?? process.argv[1]
@@ -30315,10 +30697,10 @@ async function runInit(deps) {
30315
30697
 
30316
30698
  // src/log.ts
30317
30699
  import { appendFileSync, mkdirSync as mkdirSync4, renameSync, statSync } from "node:fs";
30318
- import path4 from "node:path";
30700
+ import path5 from "node:path";
30319
30701
  var MAX_BYTES = 10 * 1024 * 1024;
30320
30702
  function createLogger(file2) {
30321
- mkdirSync4(path4.dirname(file2), { recursive: true, mode: 448 });
30703
+ mkdirSync4(path5.dirname(file2), { recursive: true, mode: 448 });
30322
30704
  const write = (level, msg) => {
30323
30705
  try {
30324
30706
  if (statSync(file2).size > MAX_BYTES) renameSync(file2, file2 + ".1");
@@ -30331,18 +30713,110 @@ function createLogger(file2) {
30331
30713
  }
30332
30714
 
30333
30715
  // src/loop.ts
30334
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync2, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
30335
- import path10 from "node:path";
30716
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
30717
+ import path12 from "node:path";
30336
30718
  import { setTimeout as delay2 } from "node:timers/promises";
30337
30719
 
30720
+ // src/clones.ts
30721
+ import { lstatSync, mkdirSync as mkdirSync5, readdirSync as readdirSync2, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
30722
+ import path6 from "node:path";
30723
+ var lastRunPath = (gitDir, projectId) => path6.join(gitDir, `${projectId}.last-run`);
30724
+ function touchLastRun(gitDir, projectId, now) {
30725
+ mkdirSync5(gitDir, { recursive: true, mode: 448 });
30726
+ const file2 = lastRunPath(gitDir, projectId);
30727
+ rmSync3(file2, { force: true });
30728
+ writeFileSync4(file2, now.toISOString() + "\n", { flag: "wx", mode: 384 });
30729
+ }
30730
+ var DAY_MS = 864e5;
30731
+ var SLOT_RE = /^\d+$/;
30732
+ var ID_RE = /^[\w-]+$/;
30733
+ function removeStaleClones(projectsDir, gitDir, keepDays, busyProjectIds, now, log) {
30734
+ const removed = [];
30735
+ let names;
30736
+ try {
30737
+ names = readdirSync2(projectsDir);
30738
+ } catch (e) {
30739
+ if (e.code !== "ENOENT") log.warn(`project folders not listed: ${errorText(e)}`);
30740
+ return removed;
30741
+ }
30742
+ const cutoff = now.getTime() - keepDays * DAY_MS;
30743
+ const isDir = (p) => {
30744
+ try {
30745
+ return lstatSync(p).isDirectory();
30746
+ } catch {
30747
+ return false;
30748
+ }
30749
+ };
30750
+ const gone = /* @__PURE__ */ new Set();
30751
+ const remove = (folder, label2, id, extra, why) => {
30752
+ try {
30753
+ rmSync3(folder, { recursive: true, force: true });
30754
+ for (const f of extra) rmSync3(f, { force: true });
30755
+ removed.push(label2);
30756
+ gone.add(id);
30757
+ log.info(`removed project folder ${label2}: ${why}`);
30758
+ } catch (e) {
30759
+ log.warn(`project folder ${label2} not removed: ${errorText(e)}`);
30760
+ }
30761
+ };
30762
+ const slots = names.filter((n) => SLOT_RE.test(n) && isDir(path6.join(projectsDir, n)));
30763
+ for (const name of names) {
30764
+ if (SLOT_RE.test(name) || !ID_RE.test(name) || busyProjectIds.has(name)) continue;
30765
+ const folder = path6.join(projectsDir, name);
30766
+ if (!isDir(folder)) continue;
30767
+ remove(folder, name, name, [path6.join(gitDir, `${name}.worktree.json`)], "left by an older runner");
30768
+ }
30769
+ for (const slot of slots) {
30770
+ let ids;
30771
+ try {
30772
+ ids = readdirSync2(path6.join(projectsDir, slot));
30773
+ } catch (e) {
30774
+ log.warn(`slot ${slot} not listed: ${errorText(e)}`);
30775
+ continue;
30776
+ }
30777
+ for (const id of ids) {
30778
+ if (!ID_RE.test(id) || busyProjectIds.has(id)) continue;
30779
+ const folder = path6.join(projectsDir, slot, id);
30780
+ try {
30781
+ const st = lstatSync(folder);
30782
+ if (!st.isDirectory()) continue;
30783
+ let lastUse = st.mtimeMs;
30784
+ try {
30785
+ const marker = lstatSync(lastRunPath(gitDir, id));
30786
+ if (marker.isFile()) lastUse = marker.mtimeMs;
30787
+ } catch {
30788
+ }
30789
+ if (lastUse >= cutoff) continue;
30790
+ } catch (e) {
30791
+ log.warn(`project folder ${slot}/${id} not checked: ${errorText(e)}`);
30792
+ continue;
30793
+ }
30794
+ remove(folder, `${slot}/${id}`, id, [path6.join(gitDir, `${id}.${slot}.worktree.json`)], `unused for more than ${keepDays} days`);
30795
+ }
30796
+ }
30797
+ for (const id of gone) {
30798
+ if (slots.some((s) => isDir(path6.join(projectsDir, s, id)))) continue;
30799
+ try {
30800
+ rmSync3(path6.join(gitDir, `${id}.git`), { recursive: true, force: true });
30801
+ rmSync3(lastRunPath(gitDir, id), { force: true });
30802
+ } catch (e) {
30803
+ log.warn(`mirror of ${id} not removed: ${errorText(e)}`);
30804
+ }
30805
+ }
30806
+ return removed;
30807
+ }
30808
+ var errorText = (e) => e instanceof Error ? e.message : String(e);
30809
+
30338
30810
  // src/run.ts
30339
- import { mkdirSync as mkdirSync6, mkdtempSync, rmSync as rmSync3 } from "node:fs";
30340
- import path9 from "node:path";
30811
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, mkdtempSync, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
30812
+ import path11 from "node:path";
30341
30813
 
30342
30814
  // src/project-folder.ts
30343
30815
  import { execFile } from "node:child_process";
30344
- import { existsSync as existsSync3, mkdirSync as mkdirSync5 } from "node:fs";
30345
- import path5 from "node:path";
30816
+ import { createHash } from "node:crypto";
30817
+ import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync2, realpathSync as realpathSync2, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
30818
+ import os5 from "node:os";
30819
+ import path7 from "node:path";
30346
30820
  import { promisify } from "node:util";
30347
30821
  var execFileAsync = async (file2, args, opts) => {
30348
30822
  const { stdout, stderr } = await promisify(execFile)(file2, args, {
@@ -30354,25 +30828,182 @@ var execFileAsync = async (file2, args, opts) => {
30354
30828
  });
30355
30829
  return { stdout: String(stdout), stderr: String(stderr) };
30356
30830
  };
30831
+ var SAFE_GIT = ["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false"];
30832
+ function gitEnv() {
30833
+ const env = { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: os5.devNull };
30834
+ for (const k of Object.keys(env)) if (/^GIT_CONFIG_(COUNT|KEY_\d+|VALUE_\d+|PARAMETERS)$/.test(k) || k === "GIT_ASKPASS") delete env[k];
30835
+ return env;
30836
+ }
30837
+ function githubAuthEnv(token) {
30838
+ return {
30839
+ ...gitEnv(),
30840
+ GIT_CONFIG_COUNT: "1",
30841
+ GIT_CONFIG_KEY_0: `http.${GITHUB_URL}.extraheader`,
30842
+ GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`
30843
+ };
30844
+ }
30357
30845
  var CLONE_TIMEOUT_MS = 10 * 6e4;
30358
30846
  var GIT_TIMEOUT_MS = 2 * 6e4;
30359
- async function prepareGithubFolder(projectsDir, projectId, g, exec = execFileAsync) {
30360
- const folder = path5.join(projectsDir, projectId);
30361
- const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
30362
- const git = (args, timeout = GIT_TIMEOUT_MS) => exec("git", ["-C", folder, ...args], { env, timeout });
30363
- if (existsSync3(path5.join(folder, ".git"))) {
30364
- await git(["fetch", "origin", "--prune"]);
30847
+ var barePath = (gitDir, projectId) => path7.join(gitDir, `${projectId}.git`);
30848
+ var projectFolder = (projectsDir, slot, projectId) => path7.join(projectsDir, String(slot), projectId);
30849
+ var GITHUB_URL = "https://github.com/";
30850
+ async function prepareGithubFolder(dirs, projectId, g, token, exec = execFileAsync) {
30851
+ if (!/^[\w-]+$/.test(projectId)) throw new Error("runner_error");
30852
+ if (typeof g.cloneUrl !== "string" || !g.cloneUrl.startsWith(GITHUB_URL) || /\s/.test(g.cloneUrl)) throw new Error("runner_error");
30853
+ const slot = dirs.slot ?? 0;
30854
+ const slotDir = path7.join(dirs.projectsDir, String(slot));
30855
+ const folder = projectFolder(dirs.projectsDir, slot, projectId);
30856
+ const bare = barePath(dirs.gitDir, projectId);
30857
+ const saved = savedSnapshotPath(dirs.gitDir, projectId, slot);
30858
+ const env = gitEnv();
30859
+ const gitIn = (dir, args, timeout = GIT_TIMEOUT_MS) => exec("git", [...SAFE_GIT, "-C", dir, ...args], { env, timeout });
30860
+ if (existsSync4(path7.join(bare, "HEAD"))) {
30861
+ await gitIn(bare, ["remote", "set-url", "--", "origin", g.cloneUrl]);
30862
+ await exec("git", [...SAFE_GIT, "-C", bare, "fetch", "origin", "--prune", "+refs/heads/*:refs/heads/*"], { env: githubAuthEnv(await token()), timeout: CLONE_TIMEOUT_MS });
30863
+ } else {
30864
+ mkdirSync6(dirs.gitDir, { recursive: true, mode: 448 });
30865
+ await exec("git", [...SAFE_GIT, "clone", "--bare", "--", g.cloneUrl, bare], { cwd: dirs.gitDir, env: githubAuthEnv(await token()), timeout: CLONE_TIMEOUT_MS });
30866
+ }
30867
+ if (exists(folder)) {
30868
+ const snap2 = readSavedSnapshot(saved);
30869
+ let ok = snap2 !== null;
30870
+ if (snap2) {
30871
+ try {
30872
+ verifyGit(folder, snap2);
30873
+ } catch {
30874
+ ok = false;
30875
+ }
30876
+ }
30877
+ if (!ok) rmSync4(folder, { recursive: true, force: true });
30878
+ }
30879
+ if (existsSync4(path7.join(folder, ".git"))) {
30880
+ await gitIn(folder, ["fetch", "origin", "--prune"]);
30365
30881
  } else {
30366
- mkdirSync5(projectsDir, { recursive: true, mode: 448 });
30367
- await exec("git", ["clone", g.cloneUrl, folder], { cwd: projectsDir, env, timeout: CLONE_TIMEOUT_MS });
30882
+ mkdirSync6(slotDir, { recursive: true, mode: 448 });
30883
+ await exec("git", [...SAFE_GIT, "clone", "--no-hardlinks", "--", bare, folder], { cwd: slotDir, env, timeout: CLONE_TIMEOUT_MS });
30368
30884
  }
30369
- const { stdout } = await git(["rev-parse", "--abbrev-ref", "HEAD"]);
30885
+ const { stdout } = await gitIn(folder, ["rev-parse", "--abbrev-ref", "HEAD"]);
30370
30886
  if (stdout.trim() !== g.branch) {
30371
- await git(["reset", "--hard"]);
30372
- await git(["clean", "-fd"]);
30373
- await git(["checkout", "-B", g.branch, `origin/${g.fromBranch ?? g.defaultBranch}`]);
30887
+ await gitIn(folder, ["reset", "--hard"]);
30888
+ await gitIn(folder, ["clean", "-fd"]);
30889
+ await gitIn(folder, ["checkout", "-B", g.branch, `origin/${g.fromBranch ?? g.defaultBranch}`]);
30890
+ }
30891
+ const snap = snapshotGit(folder);
30892
+ saveSnapshot(dirs.gitDir, projectId, slot, snap);
30893
+ return { folder, snap };
30894
+ }
30895
+ var savedSnapshotPath = (gitDir, projectId, slot) => path7.join(gitDir, `${projectId}.${slot}.worktree.json`);
30896
+ function saveSnapshot(gitDir, projectId, slot, snap) {
30897
+ if (snap.missing) return;
30898
+ mkdirSync6(gitDir, { recursive: true, mode: 448 });
30899
+ const body = { gitRealPath: snap.real, dev: snap.dev, ino: snap.ino, configHash: snap.configHash };
30900
+ writeFileSync5(savedSnapshotPath(gitDir, projectId, slot), JSON.stringify(body) + "\n", { mode: 384 });
30901
+ }
30902
+ function forgetSnapshot(gitDir, projectId, slot) {
30903
+ rmSync4(savedSnapshotPath(gitDir, projectId, slot), { force: true });
30904
+ }
30905
+ function readSavedSnapshot(file2) {
30906
+ try {
30907
+ const j = JSON.parse(readFileSync2(file2, "utf8"));
30908
+ if (typeof j.gitRealPath !== "string" || typeof j.dev !== "number" || typeof j.ino !== "number" || typeof j.configHash !== "string") return null;
30909
+ return { missing: false, real: j.gitRealPath, dev: j.dev, ino: j.ino, configHash: j.configHash };
30910
+ } catch {
30911
+ return null;
30912
+ }
30913
+ }
30914
+ async function pushBranch(folder, bare, branch, token, exec = execFileAsync) {
30915
+ const env = gitEnv();
30916
+ await exec("git", [...SAFE_GIT, "-C", bare, "fetch", folder, `+refs/heads/${branch}:refs/heads/${branch}`], { env, timeout: GIT_TIMEOUT_MS }).catch(gitError);
30917
+ const auth = githubAuthEnv(await token());
30918
+ await exec("git", [...SAFE_GIT, "-C", bare, "push", "--no-verify", "origin", branch], { env: auth, timeout: GIT_TIMEOUT_MS }).catch(gitError);
30919
+ }
30920
+ function gitError(e) {
30921
+ const stderr = typeof e.stderr === "string" ? e.stderr : "";
30922
+ const line = stderr.split("\n").map((l) => l.trim()).find(Boolean);
30923
+ throw new Error(line ?? "git push failed");
30924
+ }
30925
+ var RepoTamperedError = class extends Error {
30926
+ constructor(detail) {
30927
+ super("repo_tampered");
30928
+ this.detail = detail;
30929
+ }
30930
+ detail;
30931
+ };
30932
+ function snapshotGit(folder) {
30933
+ const dir = path7.join(folder, ".git");
30934
+ let st;
30935
+ try {
30936
+ st = lstatSync2(dir);
30937
+ } catch {
30938
+ return { missing: true };
30939
+ }
30940
+ if (!st.isDirectory()) throw new RepoTamperedError(".git is not a folder");
30941
+ return { missing: false, ino: st.ino, dev: st.dev, real: realpathSync2(dir), configHash: configHash(dir) };
30942
+ }
30943
+ function verifyGit(folder, snap) {
30944
+ const dir = path7.join(folder, ".git");
30945
+ let st;
30946
+ try {
30947
+ st = lstatSync2(dir);
30948
+ } catch {
30949
+ if (snap.missing) return;
30950
+ throw new RepoTamperedError(".git is gone");
30951
+ }
30952
+ if (snap.missing) throw new RepoTamperedError(".git appeared");
30953
+ if (st.isSymbolicLink() || !st.isDirectory()) throw new RepoTamperedError(".git is not a folder");
30954
+ if (st.ino !== snap.ino || st.dev !== snap.dev || realpathSync2(dir) !== snap.real) throw new RepoTamperedError(".git was replaced");
30955
+ if (existsSync4(path7.join(dir, "commondir"))) throw new RepoTamperedError(".git has a commondir file");
30956
+ for (const f of [path7.join("objects", "info", "alternates"), path7.join("objects", "info", "http-alternates"), "worktrees"]) {
30957
+ if (exists(path7.join(dir, f))) throw new RepoTamperedError(`.git/${f} exists`);
30958
+ }
30959
+ const link = findSymlink(dir);
30960
+ if (link) throw new RepoTamperedError(`.git/${link} is a symlink`);
30961
+ if (configHash(dir) !== snap.configHash) throw new RepoTamperedError(".git/config changed");
30962
+ }
30963
+ var WALK_MAX_DEPTH = 12;
30964
+ var WALK_MAX_ENTRIES = 2e5;
30965
+ function findSymlink(dir) {
30966
+ let count = 0;
30967
+ const walk = (rel, depth) => {
30968
+ if (depth > WALK_MAX_DEPTH) throw new RepoTamperedError(`.git is nested too deep: ${rel}`);
30969
+ let entries;
30970
+ try {
30971
+ entries = readdirSync3(path7.join(dir, rel), { withFileTypes: true });
30972
+ } catch {
30973
+ throw new RepoTamperedError(`unreadable entry in .git: ${rel || "."}`);
30974
+ }
30975
+ count += entries.length;
30976
+ if (count > WALK_MAX_ENTRIES) throw new RepoTamperedError(".git has too many entries");
30977
+ for (const e of entries) {
30978
+ const child = rel ? `${rel}/${e.name}` : e.name;
30979
+ if (e.isSymbolicLink()) return child;
30980
+ if (e.isDirectory()) {
30981
+ const found = walk(child, depth + 1);
30982
+ if (found) return found;
30983
+ }
30984
+ }
30985
+ return null;
30986
+ };
30987
+ return walk("", 0);
30988
+ }
30989
+ function configHash(gitDir) {
30990
+ const file2 = path7.join(gitDir, "config");
30991
+ let st;
30992
+ try {
30993
+ st = lstatSync2(file2);
30994
+ } catch {
30995
+ return "none";
30996
+ }
30997
+ if (!st.isFile()) throw new RepoTamperedError(".git/config is not a file");
30998
+ return createHash("sha256").update(readFileSync2(file2)).digest("hex");
30999
+ }
31000
+ function exists(p) {
31001
+ try {
31002
+ lstatSync2(p);
31003
+ return true;
31004
+ } catch {
31005
+ return false;
30374
31006
  }
30375
- return folder;
30376
31007
  }
30377
31008
 
30378
31009
  // src/scrub.ts
@@ -30444,12 +31075,12 @@ function resolveRoute(siteUrl, route) {
30444
31075
  return { error: "route is not a valid URL path" };
30445
31076
  }
30446
31077
  if (url2.origin !== site.origin || !url2.pathname.startsWith(prefix + "/wp/v2/")) return { error: "route must stay under /wp/v2/" };
30447
- const path11 = url2.pathname.slice(prefix.length);
30448
- return { path: path11, route: path11 + url2.search, query: url2.searchParams };
31078
+ const path13 = url2.pathname.slice(prefix.length);
31079
+ return { path: path13, route: path13 + url2.search, query: url2.searchParams };
30449
31080
  }
30450
31081
 
30451
31082
  // src/wp/backup.ts
30452
- import path6 from "node:path";
31083
+ import path8 from "node:path";
30453
31084
 
30454
31085
  // src/wp/transport.ts
30455
31086
  var shellQuote = (a) => /^[A-Za-z0-9_\-.,/:=@%+]+$/.test(a) ? a : `'${a.replace(/'/g, `'\\''`)}'`;
@@ -30468,7 +31099,7 @@ async function backupBefore(t, root) {
30468
31099
  const wp = await dump(`{ wp db export - --path=${shellQuote(root)} || echo ${FAILED} >&2; } | gzip`);
30469
31100
  if (wp === "overflow") throw new Error("backup_failed: the database dump is over 200 MB");
30470
31101
  if (wp) return { dump: wp, how: "wp-cli" };
30471
- const config2 = await t.getFile(path6.posix.join(root, "wp-config.php")).catch(() => null);
31102
+ const config2 = await t.getFile(path8.posix.join(root, "wp-config.php")).catch(() => null);
30472
31103
  const db = config2 ? dbCredentials(config2) : null;
30473
31104
  if (!db) throw new Error("backup_failed: no wp-cli export and no database credentials in wp-config.php");
30474
31105
  const [host, port] = splitHost(db.host);
@@ -30600,7 +31231,7 @@ async function ftpTransport(conn) {
30600
31231
  // src/wp/helper.ts
30601
31232
  import { randomBytes, timingSafeEqual } from "node:crypto";
30602
31233
  import http from "node:http";
30603
- import path7 from "node:path";
31234
+ import path9 from "node:path";
30604
31235
 
30605
31236
  // ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v3/helpers/util.js
30606
31237
  var util;
@@ -30976,8 +31607,8 @@ function getErrorMap() {
30976
31607
 
30977
31608
  // ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v3/helpers/parseUtil.js
30978
31609
  var makeIssue = (params) => {
30979
- const { data, path: path11, errorMaps, issueData } = params;
30980
- const fullPath = [...path11, ...issueData.path || []];
31610
+ const { data, path: path13, errorMaps, issueData } = params;
31611
+ const fullPath = [...path13, ...issueData.path || []];
30981
31612
  const fullIssue = {
30982
31613
  ...issueData,
30983
31614
  path: fullPath
@@ -31092,11 +31723,11 @@ var errorUtil;
31092
31723
 
31093
31724
  // ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v3/types.js
31094
31725
  var ParseInputLazyPath = class {
31095
- constructor(parent, value, path11, key) {
31726
+ constructor(parent, value, path13, key) {
31096
31727
  this._cachedPath = [];
31097
31728
  this.parent = parent;
31098
31729
  this.data = value;
31099
- this._path = path11;
31730
+ this._path = path13;
31100
31731
  this._key = key;
31101
31732
  }
31102
31733
  get path() {
@@ -35051,10 +35682,10 @@ function mergeDefs(...defs) {
35051
35682
  function cloneDef(schema) {
35052
35683
  return mergeDefs(schema._zod.def);
35053
35684
  }
35054
- function getElementAtPath(obj, path11) {
35055
- if (!path11)
35685
+ function getElementAtPath(obj, path13) {
35686
+ if (!path13)
35056
35687
  return obj;
35057
- return path11.reduce((acc, key) => acc?.[key], obj);
35688
+ return path13.reduce((acc, key) => acc?.[key], obj);
35058
35689
  }
35059
35690
  function promiseAllObject(promisesObj) {
35060
35691
  const keys = Object.keys(promisesObj);
@@ -35394,11 +36025,11 @@ function explicitlyAborted(x, startIndex = 0) {
35394
36025
  }
35395
36026
  return false;
35396
36027
  }
35397
- function prefixIssues(path11, issues) {
36028
+ function prefixIssues(path13, issues) {
35398
36029
  return issues.map((iss) => {
35399
36030
  var _a3;
35400
36031
  (_a3 = iss).path ?? (_a3.path = []);
35401
- iss.path.unshift(path11);
36032
+ iss.path.unshift(path13);
35402
36033
  return iss;
35403
36034
  });
35404
36035
  }
@@ -35860,16 +36491,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
35860
36491
  }
35861
36492
  function formatError(error62, mapper = (issue2) => issue2.message) {
35862
36493
  const fieldErrors = { _errors: [] };
35863
- const processError = (error63, path11 = []) => {
36494
+ const processError = (error63, path13 = []) => {
35864
36495
  for (const issue2 of error63.issues) {
35865
36496
  if (issue2.code === "invalid_union" && issue2.errors.length) {
35866
- issue2.errors.map((issues) => processError({ issues }, [...path11, ...issue2.path]));
36497
+ issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
35867
36498
  } else if (issue2.code === "invalid_key") {
35868
- processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
36499
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
35869
36500
  } else if (issue2.code === "invalid_element") {
35870
- processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
36501
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
35871
36502
  } else {
35872
- const fullpath = [...path11, ...issue2.path];
36503
+ const fullpath = [...path13, ...issue2.path];
35873
36504
  if (fullpath.length === 0) {
35874
36505
  fieldErrors._errors.push(mapper(issue2));
35875
36506
  } else {
@@ -35908,17 +36539,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
35908
36539
  }
35909
36540
  function treeifyError(error62, mapper = (issue2) => issue2.message) {
35910
36541
  const result = { errors: [] };
35911
- const processError = (error63, path11 = []) => {
36542
+ const processError = (error63, path13 = []) => {
35912
36543
  var _a3;
35913
36544
  for (const issue2 of error63.issues) {
35914
36545
  if (issue2.code === "invalid_union" && issue2.errors.length) {
35915
- issue2.errors.map((issues) => processError({ issues }, [...path11, ...issue2.path]));
36546
+ issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
35916
36547
  } else if (issue2.code === "invalid_key") {
35917
- processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
36548
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
35918
36549
  } else if (issue2.code === "invalid_element") {
35919
- processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
36550
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
35920
36551
  } else {
35921
- const fullpath = [...path11, ...issue2.path];
36552
+ const fullpath = [...path13, ...issue2.path];
35922
36553
  if (fullpath.length === 0) {
35923
36554
  result.errors.push(mapper(issue2));
35924
36555
  continue;
@@ -35957,8 +36588,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
35957
36588
  }
35958
36589
  function toDotPath(_path) {
35959
36590
  const segs = [];
35960
- const path11 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
35961
- for (const seg of path11) {
36591
+ const path13 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
36592
+ for (const seg of path13) {
35962
36593
  if (typeof seg === "number")
35963
36594
  segs.push(`[${seg}]`);
35964
36595
  else if (typeof seg === "symbol")
@@ -51509,11 +52140,11 @@ function normalizeObjectSchema(schema) {
51509
52140
  }
51510
52141
  return void 0;
51511
52142
  }
51512
- function getDotPath(path11) {
51513
- if (path11.length === 0) {
52143
+ function getDotPath(path13) {
52144
+ if (path13.length === 0) {
51514
52145
  return "object root";
51515
52146
  }
51516
- return path11.reduce((acc, seg, index) => {
52147
+ return path13.reduce((acc, seg, index) => {
51517
52148
  if (index === 0) {
51518
52149
  return String(seg);
51519
52150
  }
@@ -53740,13 +54371,13 @@ function resolveRef(ref, ctx) {
53740
54371
  if (!ref.startsWith("#")) {
53741
54372
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
53742
54373
  }
53743
- const path11 = ref.slice(1).split("/").filter(Boolean);
53744
- if (path11.length === 0) {
54374
+ const path13 = ref.slice(1).split("/").filter(Boolean);
54375
+ if (path13.length === 0) {
53745
54376
  return ctx.rootSchema;
53746
54377
  }
53747
54378
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
53748
- if (path11[0] === defsKey) {
53749
- const key = path11[1] === void 0 ? void 0 : decodeJSONPointerSegment(path11[1]);
54379
+ if (path13[0] === defsKey) {
54380
+ const key = path13[1] === void 0 ? void 0 : decodeJSONPointerSegment(path13[1]);
53750
54381
  if (!key || !ctx.defs[key]) {
53751
54382
  throw new Error(`Reference not found: ${ref}`);
53752
54383
  }
@@ -62194,7 +62825,7 @@ function buildServer(t) {
62194
62825
  if (!t.allow.files && !t.allow.all) return fail("the files group is off for this project");
62195
62826
  if (t.root === null) return fail("wordpress_root_not_found: the WordPress folder was not found on the server");
62196
62827
  const bad = pathRefusal(p, t.allow.all);
62197
- return bad ? fail(bad) : path7.posix.join(t.root, p);
62828
+ return bad ? fail(bad) : path9.posix.join(t.root, p);
62198
62829
  };
62199
62830
  server.registerTool("wp_get_file", {
62200
62831
  description: "Reads a file of the WordPress site, by its path relative to the WordPress folder (e.g. wp-content/themes/x/style.css).",
@@ -62266,9 +62897,9 @@ function apiRefusal(route, query, method, body, allow) {
62266
62897
  }
62267
62898
  function pathRefusal(p, all) {
62268
62899
  if (p.startsWith("/") || p.includes("\\") || p.includes("\0")) return "path must be relative to the WordPress folder";
62269
- const norm = path7.posix.normalize(p);
62900
+ const norm = path9.posix.normalize(p);
62270
62901
  if (norm === ".." || norm.startsWith("../") || p.split("/").includes("..")) return "path must stay inside the WordPress folder";
62271
- if (!all && PROTECTED_FILES.has(path7.posix.basename(norm))) return `refused: ${path7.posix.basename(norm)} is never changed unless "Allow everything" is on`;
62902
+ if (!all && PROTECTED_FILES.has(path9.posix.basename(norm))) return `refused: ${path9.posix.basename(norm)} is never changed unless "Allow everything" is on`;
62272
62903
  return null;
62273
62904
  }
62274
62905
  function publishedLink(json3) {
@@ -62280,7 +62911,7 @@ var cut2 = (s) => s.length > MAX_OUTPUT ? `${s.slice(0, MAX_OUTPUT)}
62280
62911
  [cut at ${MAX_OUTPUT} characters]` : s;
62281
62912
 
62282
62913
  // src/wp/root.ts
62283
- import path8 from "node:path";
62914
+ import path10 from "node:path";
62284
62915
  var FTP_CANDIDATES = [".", "public_html", "www", "htdocs", "httpdocs", "html", "web"];
62285
62916
  async function findRoot(t) {
62286
62917
  if (t.run) {
@@ -62288,10 +62919,10 @@ async function findRoot(t) {
62288
62919
  if (home?.code === 0) return ".";
62289
62920
  const found = await t.run(["find", ".", "-maxdepth", "3", "-name", "wp-config.php"]).catch(() => null);
62290
62921
  const first = found?.stdout.toString("utf8").split("\n").map((l) => l.trim()).filter(Boolean).sort((a, b) => a.split("/").length - b.split("/").length)[0];
62291
- return first ? path8.posix.normalize(path8.posix.dirname(first)) : null;
62922
+ return first ? path10.posix.normalize(path10.posix.dirname(first)) : null;
62292
62923
  }
62293
62924
  for (const dir of FTP_CANDIDATES) {
62294
- if (await t.getFile(path8.posix.join(dir, "wp-config.php")).catch(() => null) !== null) return dir;
62925
+ if (await t.getFile(path10.posix.join(dir, "wp-config.php")).catch(() => null) !== null) return dir;
62295
62926
  }
62296
62927
  return null;
62297
62928
  }
@@ -62390,29 +63021,45 @@ async function runOne(spec, deps) {
62390
63021
  return ms <= 0 ? 0 : Math.max(1, Math.ceil(ms / 6e4));
62391
63022
  };
62392
63023
  const sendDone = (input2) => doneWithRetry(spec.runId, input2, api, log, sleep);
63024
+ if (engineTooOld(deps.sandbox.engineVersion)) {
63025
+ log.error(`run ${spec.runId}: Claude Code ${deps.sandbox.engineVersion ?? "(unknown version)"} is too old; update it (claude update)`);
63026
+ await sendDone({ outcome: "failed", error: "engine_too_old", costUsd: 0, inputTokens: 0, outputTokens: 0 }).catch((e) => log.warn(`run ${spec.runId}: done failed: ${errorText2(e)}`));
63027
+ return;
63028
+ }
62393
63029
  const secrets = [
62394
63030
  spec.mcp.accessToken,
62395
63031
  spec.mcp.refreshToken,
62396
63032
  spec.wordpress?.apiPassword,
62397
63033
  spec.wordpress?.ssh?.privateKey,
62398
63034
  spec.wordpress?.ssh?.password,
62399
- spec.wordpress?.ftp?.password
63035
+ spec.wordpress?.ftp?.password,
63036
+ ...tokenForms(spec.github?.token)
62400
63037
  ].filter((s) => Boolean(s));
63038
+ const gitToken = spec.github ? githubToken(spec.runId, spec.github, api, secrets, now) : void 0;
62401
63039
  const totals = { costUsd: 0, inputTokens: 0, outputTokens: 0 };
62402
63040
  const events = transcript(spec.runId, api, log, secrets, now, totals);
62403
63041
  let helper;
62404
63042
  let tempDir;
63043
+ let settingsFile;
63044
+ let clone2;
62405
63045
  let pendingDone = true;
63046
+ const slot = deps.slot ?? 0;
63047
+ let started = false;
62406
63048
  try {
63049
+ if (!/^[\w-]+$/.test(spec.projectId)) throw new Error("runner_error");
62407
63050
  let cwd;
62408
63051
  if (spec.kind === "github" && spec.github) {
62409
- cwd = await prepareGithubFolder(deps.paths.projectsDir, spec.projectId, spec.github, deps.exec);
63052
+ started = true;
63053
+ clone2 = await prepareGithubFolder({ ...deps.paths, slot }, spec.projectId, spec.github, gitToken, deps.exec);
63054
+ cwd = clone2.folder;
62410
63055
  } else {
62411
- const parent = path9.join(deps.paths.projectsDir, spec.projectId);
62412
- mkdirSync6(parent, { recursive: true, mode: 448 });
62413
- cwd = tempDir = mkdtempSync(path9.join(parent, "run-"));
63056
+ started = true;
63057
+ const parent = projectFolder(deps.paths.projectsDir, slot, spec.projectId);
63058
+ mkdirSync7(parent, { recursive: true, mode: 448 });
63059
+ cwd = tempDir = mkdtempSync(path11.join(parent, "run-"));
62414
63060
  if (deps.helperFactory) helper = await deps.helperFactory(spec);
62415
63061
  }
63062
+ settingsFile = writeSettings(spec, deps, cwd);
62416
63063
  const base = {
62417
63064
  cwd,
62418
63065
  systemPrompt: spec.systemPrompt,
@@ -62420,7 +63067,8 @@ async function runOne(spec, deps) {
62420
63067
  helper: helper ? { url: helper.url, token: helper.token } : void 0,
62421
63068
  env: {},
62422
63069
  maxMinutes: spec.maxRunMinutes,
62423
- claudeConfigDir: deps.paths.claudeConfigDir
63070
+ claudeConfigDir: deps.paths.claudeConfigDir,
63071
+ settingsFile
62424
63072
  };
62425
63073
  log.info(`run ${spec.runId}: start`);
62426
63074
  const timedOut = (sessionId) => ({ outcome: "failed", error: "timeout", costUsd: 0, inputTokens: 0, outputTokens: 0, sessionId });
@@ -62433,10 +63081,26 @@ async function runOne(spec, deps) {
62433
63081
  if (result.sessionId) totals.sessionId = result.sessionId;
62434
63082
  const site = skipped ? void 0 : await helper?.checkSite?.();
62435
63083
  await events.flush(totals);
62436
- const input2 = site && site !== "ok" ? { outcome: "failed", error: site, ...totals } : doneInput(result, totals, secrets);
63084
+ let input2 = site && site !== "ok" ? { outcome: "failed", error: site, ...totals } : doneInput(result, totals, secrets);
63085
+ let pushFailed = false;
63086
+ if (input2.outcome === "reported" && spec.kind === "github" && spec.github) {
63087
+ try {
63088
+ if (clone2) verifyGit(clone2.folder, clone2.snap);
63089
+ await pushBranch(cwd, barePath(deps.paths.gitDir, spec.projectId), spec.github.branch, gitToken, deps.exec);
63090
+ } catch (e) {
63091
+ pushFailed = true;
63092
+ if (e instanceof RepoTamperedError) {
63093
+ log.error(`run ${spec.runId}: not pushed: the run changed the repository's own settings (${e.detail})`);
63094
+ input2 = { outcome: "failed", error: "repo_tampered", ...totals };
63095
+ } else {
63096
+ log.error(`run ${spec.runId}: push failed: ${scrub(errorText2(e), secrets).slice(0, 200)}`);
63097
+ input2 = { outcome: "failed", error: "push_failed", ...totals };
63098
+ }
63099
+ }
63100
+ }
62437
63101
  const answer = await sendDone(input2);
62438
63102
  log.info(`run ${spec.runId}: done ${input2.outcome}${input2.error ? ` ${input2.error}` : ""}`);
62439
- if (!answer.followUp || skipped) {
63103
+ if (!answer.followUp || skipped || pushFailed) {
62440
63104
  pendingDone = false;
62441
63105
  break;
62442
63106
  }
@@ -62456,7 +63120,7 @@ async function runOne(spec, deps) {
62456
63120
  log.error(`run ${spec.runId}: ${message}`);
62457
63121
  if (pendingDone && helper?.checkSite) {
62458
63122
  const site = await helper.checkSite().catch((e) => {
62459
- log.warn(`run ${spec.runId}: site check failed: ${errorText(e)}`);
63123
+ log.warn(`run ${spec.runId}: site check failed: ${errorText2(e)}`);
62460
63124
  return "ok";
62461
63125
  });
62462
63126
  if (site !== "ok") message = site;
@@ -62464,20 +63128,98 @@ async function runOne(spec, deps) {
62464
63128
  if (pendingDone) {
62465
63129
  const usage = events.usage();
62466
63130
  await events.flush();
62467
- await sendDone({ outcome: "failed", error: message, ...usage }).catch((e) => log.warn(`run ${spec.runId}: done failed: ${errorText(e)}`));
63131
+ await sendDone({ outcome: "failed", error: message, ...usage }).catch((e) => log.warn(`run ${spec.runId}: done failed: ${errorText2(e)}`));
62468
63132
  }
62469
63133
  } finally {
62470
63134
  events.stop();
62471
- if (helper) await helper.stop().catch((e) => log.warn(`run ${spec.runId}: helper stop failed: ${errorText(e)}`));
63135
+ if (helper) await helper.stop().catch((e) => log.warn(`run ${spec.runId}: helper stop failed: ${errorText2(e)}`));
63136
+ if (clone2) {
63137
+ try {
63138
+ verifyGit(clone2.folder, clone2.snap);
63139
+ saveSnapshot(deps.paths.gitDir, spec.projectId, slot, clone2.snap);
63140
+ } catch (e) {
63141
+ log.warn(`run ${spec.runId}: removing the project folder: ${e instanceof RepoTamperedError ? e.detail : errorText2(e)}`);
63142
+ try {
63143
+ forgetSnapshot(deps.paths.gitDir, spec.projectId, slot);
63144
+ } catch {
63145
+ }
63146
+ try {
63147
+ rmSync5(clone2.folder, { recursive: true, force: true });
63148
+ } catch (err) {
63149
+ log.warn(`run ${spec.runId}: could not remove ${clone2.folder}: ${errorText2(err)}`);
63150
+ }
63151
+ }
63152
+ }
63153
+ if (started) {
63154
+ try {
63155
+ touchLastRun(deps.paths.gitDir, spec.projectId, now());
63156
+ } catch (e) {
63157
+ log.warn(`run ${spec.runId}: could not write the last-run marker: ${errorText2(e)}`);
63158
+ }
63159
+ }
63160
+ if (settingsFile) {
63161
+ try {
63162
+ rmSync5(settingsFile, { force: true });
63163
+ } catch (e) {
63164
+ log.warn(`run ${spec.runId}: could not remove ${settingsFile}: ${errorText2(e)}`);
63165
+ }
63166
+ }
62472
63167
  if (tempDir) {
62473
63168
  try {
62474
- rmSync3(tempDir, { recursive: true, force: true });
63169
+ rmSync5(tempDir, { recursive: true, force: true });
62475
63170
  } catch (e) {
62476
- log.warn(`run ${spec.runId}: could not remove ${tempDir}: ${errorText(e)}`);
63171
+ log.warn(`run ${spec.runId}: could not remove ${tempDir}: ${errorText2(e)}`);
62477
63172
  }
62478
63173
  }
62479
63174
  }
62480
63175
  }
63176
+ var tokenForms = (token) => token ? [token, Buffer.from(`x-access-token:${token}`).toString("base64")] : [];
63177
+ var TOKEN_MARGIN_MS = 5 * 6e4;
63178
+ function githubToken(runId, g, api, secrets, now) {
63179
+ let token = g.token;
63180
+ let expiresAt = Date.parse(g.tokenExpiresAt);
63181
+ return async () => {
63182
+ if (!token || !Number.isFinite(expiresAt) || expiresAt - TOKEN_MARGIN_MS <= now().getTime()) {
63183
+ const fresh = await api.gitToken(runId);
63184
+ if (typeof fresh?.token !== "string" || !fresh.token) throw new Error("no GitHub token from Notiamo");
63185
+ token = fresh.token;
63186
+ expiresAt = Date.parse(fresh.expiresAt);
63187
+ secrets.push(...tokenForms(token));
63188
+ }
63189
+ return token;
63190
+ };
63191
+ }
63192
+ function writeSettings(spec, deps, cwd) {
63193
+ const { sandbox } = deps;
63194
+ let wordpressHost;
63195
+ if (spec.wordpress) {
63196
+ try {
63197
+ wordpressHost = new URL(spec.wordpress.siteUrl).hostname;
63198
+ } catch {
63199
+ }
63200
+ }
63201
+ if (!/^[\w-]+$/.test(spec.runId)) throw new Error("bad run id");
63202
+ const dir = path11.resolve(deps.paths.runsDir);
63203
+ mkdirSync7(dir, { recursive: true, mode: 448 });
63204
+ const settings = sandboxSettings({
63205
+ platform: sandbox.platform,
63206
+ runnerRoot: sandbox.runnerRoot,
63207
+ claudeConfigDir: deps.paths.claudeConfigDir,
63208
+ runDir: cwd,
63209
+ notiamoHost: sandbox.notiamoHost,
63210
+ extraDomains: sandbox.allowedDomains(),
63211
+ wordpressHost,
63212
+ gitExecPath: sandbox.gitExecPath,
63213
+ ghPath: sandbox.ghPath
63214
+ });
63215
+ const file2 = path11.join(dir, `${spec.runId}.json`);
63216
+ writeFileSync6(file2, JSON.stringify(settings, null, 2) + "\n", { mode: 384 });
63217
+ try {
63218
+ chmodSync2(file2, 384);
63219
+ } catch {
63220
+ }
63221
+ return file2;
63222
+ }
62481
63223
  function wordpressHelperFactory(deps) {
62482
63224
  const { log } = deps;
62483
63225
  return async (spec) => {
@@ -62490,7 +63232,7 @@ function wordpressHelperFactory(deps) {
62490
63232
  const backup = await backupBefore(transport, root);
62491
63233
  if (backup.dump) {
62492
63234
  log.info(`run ${spec.runId}: database backed up with ${backup.how} (${backup.dump.byteLength} bytes gzip)`);
62493
- await uploadDump(deps.api, spec.runId, backup.dump).catch((e) => log.warn(`run ${spec.runId}: backup upload failed: ${errorText(e)}`));
63235
+ await uploadDump(deps.api, spec.runId, backup.dump).catch((e) => log.warn(`run ${spec.runId}: backup upload failed: ${errorText2(e)}`));
62494
63236
  } else {
62495
63237
  log.warn(`run ${spec.runId}: no database backup (${transport ? "WordPress folder not found" : "no SSH"})`);
62496
63238
  }
@@ -62548,12 +63290,12 @@ async function doneWithRetry(runId, input2, api, log, sleep) {
62548
63290
  const retryable = !(e instanceof ApiError) || e.status >= 500;
62549
63291
  const wait = DONE_BACKOFF_MS[attempt];
62550
63292
  if (!retryable || wait === void 0) throw e;
62551
- log.warn(`run ${runId}: done failed (${errorText(e)}); retrying in ${wait / 1e3} s`);
63293
+ log.warn(`run ${runId}: done failed (${errorText2(e)}); retrying in ${wait / 1e3} s`);
62552
63294
  await sleep(wait);
62553
63295
  }
62554
63296
  }
62555
63297
  }
62556
- var errorText = (e) => e instanceof Error ? e.message : String(e);
63298
+ var errorText2 = (e) => e instanceof Error ? e.message : String(e);
62557
63299
  function doneInput(r, totals, secrets) {
62558
63300
  const usage = { ...totals };
62559
63301
  if (r.outcome === "ended") return { outcome: "failed", error: "no_report", ...usage };
@@ -62579,7 +63321,7 @@ function transcript(runId, api, log, secrets, now, totals) {
62579
63321
  try {
62580
63322
  await api.events(runId, body);
62581
63323
  } catch (err) {
62582
- log.warn(`run ${runId}: dropped ${body.lines.length} event lines: ${errorText(err)}`);
63324
+ log.warn(`run ${runId}: dropped ${body.lines.length} event lines: ${errorText2(err)}`);
62583
63325
  }
62584
63326
  }
62585
63327
  };
@@ -62612,72 +63354,144 @@ function transcript(runId, api, log, secrets, now, totals) {
62612
63354
  }
62613
63355
 
62614
63356
  // src/loop.ts
63357
+ var defaultSettings = (notiamoHost) => ({ runsAtOnce: 1, keepClonesDays: 7, allowedDomains: [], notiamoHost });
63358
+ function applyHello(settings, answer, defaultHost) {
63359
+ const d = defaultSettings(defaultHost);
63360
+ settings.runsAtOnce = Number.isInteger(answer.runsAtOnce) && answer.runsAtOnce >= 1 ? answer.runsAtOnce : d.runsAtOnce;
63361
+ settings.keepClonesDays = typeof answer.keepClonesDays === "number" && Number.isFinite(answer.keepClonesDays) && answer.keepClonesDays > 0 ? answer.keepClonesDays : d.keepClonesDays;
63362
+ settings.allowedDomains = Array.isArray(answer.allowedDomains) ? answer.allowedDomains.filter((x) => typeof x === "string" && x.length > 0) : d.allowedDomains;
63363
+ settings.notiamoHost = typeof answer.notiamoHost === "string" && answer.notiamoHost ? answer.notiamoHost : d.notiamoHost;
63364
+ }
62615
63365
  var HELLO_MS = 6e4;
63366
+ var SLOT_FOLDERS = 3;
62616
63367
  var NETWORK_BACKOFF_MS = 5e3;
62617
63368
  var NEXT_WAIT_SECONDS = 25;
62618
63369
  async function startLoop(deps) {
62619
63370
  const { api, log, signal } = deps;
62620
63371
  const sleep = deps.sleep ?? ((ms) => delay2(ms, void 0, { signal }).catch(() => {
62621
63372
  }));
63373
+ const defaultHost = deps.sandbox.notiamoHost;
63374
+ const settings = deps.settings ?? defaultSettings(defaultHost);
63375
+ const clones = deps.clones ?? removeStaleClones;
63376
+ const running = /* @__PURE__ */ new Map();
63377
+ for (let i = 0; i < SLOT_FOLDERS; i++) {
63378
+ try {
63379
+ mkdirSync8(path12.join(deps.paths.projectsDir, String(i)), { recursive: true, mode: 448 });
63380
+ } catch (e) {
63381
+ log.warn(`slot folder ${i} not made: ${errorText3(e)}`);
63382
+ }
63383
+ }
63384
+ const freeSlot = () => {
63385
+ const used = new Set([...running.values()].map((r) => r.slot));
63386
+ let i = 0;
63387
+ while (used.has(i)) i++;
63388
+ return i;
63389
+ };
63390
+ const saveState = () => {
63391
+ try {
63392
+ writeState(deps.paths.stateFile, [...running.keys()]);
63393
+ } catch (e) {
63394
+ log.warn(`state file not written: ${errorText3(e)}`);
63395
+ }
63396
+ };
62622
63397
  const leftover = readState(deps.paths.stateFile);
62623
- if (leftover) {
62624
- log.warn(`run ${leftover}: the runner restarted during it`);
62625
- await api.done(leftover, { outcome: "failed", error: "runner_restarted", costUsd: 0, inputTokens: 0, outputTokens: 0 }).catch(() => {
63398
+ for (const runId of leftover) {
63399
+ log.warn(`run ${runId}: the runner restarted during it`);
63400
+ await api.done(runId, { outcome: "failed", error: "runner_restarted", costUsd: 0, inputTokens: 0, outputTokens: 0 }).catch(() => {
62626
63401
  });
62627
- clearState(deps.paths.stateFile);
62628
63402
  }
62629
- const hello = () => api.hello({ version: deps.version, os: deps.os, engine: deps.engineName ?? "claude", engineVersion: deps.engineVersion }).then(() => {
62630
- }, (e) => log.warn(`hello failed: ${errorText2(e)}`));
63403
+ if (leftover.length) {
63404
+ try {
63405
+ writeState(deps.paths.stateFile, []);
63406
+ } catch (e) {
63407
+ log.warn(`state file not cleared: ${errorText3(e)}`);
63408
+ }
63409
+ }
63410
+ let helloInFlight = false;
63411
+ const hello = async () => {
63412
+ if (helloInFlight) return;
63413
+ helloInFlight = true;
63414
+ try {
63415
+ await helloOnce();
63416
+ } finally {
63417
+ helloInFlight = false;
63418
+ }
63419
+ };
63420
+ const helloOnce = async () => {
63421
+ try {
63422
+ applyHello(settings, await api.hello({ version: deps.version, os: deps.os, engine: deps.engineName ?? "claude", engineVersion: deps.engineVersion }), defaultHost);
63423
+ } catch (e) {
63424
+ log.warn(`hello failed: ${errorText3(e)}`);
63425
+ }
63426
+ try {
63427
+ clones(deps.paths.projectsDir, deps.paths.gitDir, settings.keepClonesDays, new Set([...running.values()].map((r) => r.projectId)), /* @__PURE__ */ new Date(), log);
63428
+ } catch (e) {
63429
+ log.warn(`project folder clean-up failed: ${errorText3(e)}`);
63430
+ }
63431
+ };
62631
63432
  await hello();
62632
- const timer = setInterval(() => void hello(), HELLO_MS);
63433
+ const timer = setInterval(() => void hello(), deps.helloEveryMs ?? HELLO_MS);
62633
63434
  timer.unref();
62634
63435
  try {
62635
63436
  while (!signal?.aborted) {
63437
+ if (running.size >= settings.runsAtOnce) {
63438
+ await Promise.race([...running.values()].map((r) => r.ended));
63439
+ continue;
63440
+ }
62636
63441
  let spec;
62637
63442
  try {
62638
63443
  spec = await api.next(NEXT_WAIT_SECONDS, signal);
62639
63444
  } catch (e) {
62640
63445
  if (signal?.aborted) break;
62641
- log.warn(`next failed: ${errorText2(e)}`);
63446
+ log.warn(`next failed: ${errorText3(e)}`);
62642
63447
  await sleep(NETWORK_BACKOFF_MS);
62643
63448
  continue;
62644
63449
  }
62645
63450
  if (!spec) continue;
62646
- try {
62647
- writeState(deps.paths.stateFile, spec.runId);
62648
- } catch (e) {
62649
- log.warn(`state file not written: ${errorText2(e)}`);
62650
- }
62651
- try {
62652
- await runOne(spec, { engine: deps.engine, api, paths: deps.paths, log, helperFactory: deps.helperFactory });
62653
- } catch (e) {
62654
- log.error(`run ${spec.runId}: ${errorText2(e)}`);
63451
+ const s = spec;
63452
+ if (running.has(s.runId)) {
63453
+ log.warn(`run ${s.runId}: handed out again while it runs here; ignored`);
63454
+ continue;
62655
63455
  }
62656
- try {
62657
- clearState(deps.paths.stateFile);
62658
- } catch (e) {
62659
- log.warn(`state file not cleared: ${errorText2(e)}`);
63456
+ if ([...running.values()].some((r) => r.projectId === s.projectId)) {
63457
+ log.warn(`run ${s.runId}: refused: its project is already running here`);
63458
+ await api.done(s.runId, { outcome: "failed", error: "runner_busy", costUsd: 0, inputTokens: 0, outputTokens: 0 }).catch((e) => log.warn(`run ${s.runId}: done failed: ${errorText3(e)}`));
63459
+ continue;
62660
63460
  }
63461
+ const slot = freeSlot();
63462
+ const entry = { projectId: s.projectId, slot, ended: Promise.resolve() };
63463
+ running.set(s.runId, entry);
63464
+ saveState();
63465
+ const sandbox = { ...deps.sandbox, notiamoHost: settings.notiamoHost, allowedDomains: () => settings.allowedDomains };
63466
+ entry.ended = runOne(s, { engine: deps.engine, api, paths: deps.paths, slot, sandbox, log, helperFactory: deps.helperFactory }).catch((e) => log.error(`run ${s.runId}: ${errorText3(e)}`)).finally(() => {
63467
+ running.delete(s.runId);
63468
+ saveState();
63469
+ });
62661
63470
  }
63471
+ await Promise.all([...running.values()].map((r) => r.ended));
62662
63472
  } finally {
62663
63473
  clearInterval(timer);
62664
63474
  }
62665
63475
  }
62666
- var errorText2 = (e) => e instanceof Error ? e.message : String(e);
63476
+ var errorText3 = (e) => e instanceof Error ? e.message : String(e);
62667
63477
  function readState(file2) {
62668
63478
  try {
62669
- const s = JSON.parse(readFileSync2(file2, "utf8"));
62670
- return typeof s.busyRunId === "string" ? s.busyRunId : null;
63479
+ const s = JSON.parse(readFileSync3(file2, "utf8"));
63480
+ if (Array.isArray(s.busyRunIds)) return s.busyRunIds.filter((x) => typeof x === "string");
63481
+ return typeof s.busyRunId === "string" ? [s.busyRunId] : [];
62671
63482
  } catch {
62672
- return null;
63483
+ return [];
62673
63484
  }
62674
63485
  }
62675
- function writeState(file2, busyRunId) {
62676
- mkdirSync7(path10.dirname(file2), { recursive: true, mode: 448 });
62677
- writeFileSync4(file2, JSON.stringify({ busyRunId }) + "\n", { mode: 384 });
62678
- }
62679
- function clearState(file2) {
62680
- rmSync4(file2, { force: true });
63486
+ function writeState(file2, busyRunIds) {
63487
+ if (!busyRunIds.length) {
63488
+ rmSync6(file2, { force: true });
63489
+ return;
63490
+ }
63491
+ mkdirSync8(path12.dirname(file2), { recursive: true, mode: 448 });
63492
+ const tmp = `${file2}.tmp`;
63493
+ writeFileSync7(tmp, JSON.stringify({ busyRunIds }) + "\n", { mode: 384 });
63494
+ renameSync2(tmp, file2);
62681
63495
  }
62682
63496
 
62683
63497
  // src/commands.ts
@@ -62687,7 +63501,7 @@ var serviceOptions = (profile) => ({
62687
63501
  profile,
62688
63502
  paths: paths(profileDir(profile)),
62689
63503
  exec: realExec,
62690
- home: os3.homedir(),
63504
+ home: os6.homedir(),
62691
63505
  env: process.env,
62692
63506
  nodePath: process.execPath,
62693
63507
  cliPath: process.argv[1]
@@ -62724,6 +63538,20 @@ async function start({ profile } = {}) {
62724
63538
  const engine = fake ? fakeEngine([{ kind: "message", text: "Fake engine run." }, { kind: "result", costUsd: 0, inputTokens: 0, outputTokens: 0, sessionId: "fake-session" }], { report: true }) : claudeEngine({});
62725
63539
  const v = fake ? null : await realExec("claude", ["--version"], { env: { ...process.env, CLAUDE_CONFIG_DIR: p.claudeConfigDir } });
62726
63540
  const engineVersion = fake ? "fake" : v?.code === 0 && v.stdout.trim().slice(0, 100) || "unknown";
63541
+ const settings = defaultSettings(hostOf(cfg.host));
63542
+ const tools = await credentialTools(realExec);
63543
+ const sandbox = {
63544
+ platform: process.platform,
63545
+ runnerRoot: defaultDir(),
63546
+ get notiamoHost() {
63547
+ return settings.notiamoHost;
63548
+ },
63549
+ allowedDomains: () => settings.allowedDomains,
63550
+ // The fake engine ignores the settings file: it passes the check.
63551
+ engineVersion: fake ? ENGINE_MIN_VERSION : v?.code === 0 ? engineVersion : null,
63552
+ gitExecPath: tools.gitExecPath,
63553
+ ghPath: tools.ghPath
63554
+ };
62727
63555
  const ctrl = new AbortController();
62728
63556
  const stop5 = (signal) => {
62729
63557
  if (ctrl.signal.aborted) return;
@@ -62734,12 +63562,15 @@ async function start({ profile } = {}) {
62734
63562
  process.on("SIGTERM", () => stop5("SIGTERM"));
62735
63563
  process.on("SIGINT", () => stop5("SIGINT"));
62736
63564
  log.info(`runner ${cfg.name} ${VERSION} started (${engineVersion}), host ${cfg.host}`);
63565
+ if (engineTooOld(sandbox.engineVersion)) log.warn(`Claude Code ${engineVersion} is older than ${ENGINE_MIN_VERSION}: runs fail until it is updated (claude update)`);
62737
63566
  if (process.stdout.isTTY) console.log(`Runner ${cfg.name} is waiting for runs from ${cfg.host}. Logs: ${p.log}`);
62738
63567
  const api = new NotiamoApi(cfg);
62739
63568
  await startLoop({
62740
63569
  api,
62741
63570
  engine,
62742
63571
  paths: p,
63572
+ sandbox,
63573
+ settings,
62743
63574
  log,
62744
63575
  version: VERSION,
62745
63576
  os: process.platform,
@@ -62780,10 +63611,17 @@ function logs({ profile } = {}) {
62780
63611
  const lines2 = tail(paths(profileDir(profile)).log, 50);
62781
63612
  console.log(lines2.length ? lines2.join("\n") : "No log yet.");
62782
63613
  }
63614
+ function hostOf(url2) {
63615
+ try {
63616
+ return new URL(url2).hostname;
63617
+ } catch {
63618
+ return url2;
63619
+ }
63620
+ }
62783
63621
  function tail(file2, n) {
62784
63622
  let text;
62785
63623
  try {
62786
- text = readFileSync3(file2, "utf8");
63624
+ text = readFileSync4(file2, "utf8");
62787
63625
  } catch {
62788
63626
  return [];
62789
63627
  }