@scalequality/cli 0.4.10 → 0.4.12

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "sdkVersion": "0.3.281",
3
- "sha256": "c24a69671fea8929ec550bbb8672d703980af3c6389cf97f878cc71083b4972c",
4
- "sourceCommit": "011350388f19c1ce50250458dd2810efa3113e84"
3
+ "sha256": "7b199d28673b767dce41406f737d3c8042ed0658ca1a233a7d8363b5b7b27dfd",
4
+ "sourceCommit": "0da92af88f77faa5f860d7f37480a65800017946"
5
5
  }
package/dist/connect.cjs CHANGED
@@ -1290,13 +1290,14 @@ async function decideToolUse(toolName, input, ctx) {
1290
1290
  }
1291
1291
 
1292
1292
  // src/application/services/workspaceSandbox/protections.ts
1293
- var NO_PROTECTIONS = Object.freeze({ secretFiles: false, protectedPush: false, deployApproval: false, destructiveCommands: false, unknownPackage: null });
1293
+ var NO_PROTECTIONS = Object.freeze({ secretFiles: false, protectedPush: false, deployApproval: false, destructiveCommands: false, unknownPackage: null, toolApproval: false });
1294
1294
  var KNOWN_RULES = {
1295
1295
  "secret-files": ["block"],
1296
1296
  "protected-push": ["require"],
1297
1297
  "deploy-approval": ["require"],
1298
1298
  "destructive-commands": ["require"],
1299
- "unknown-package": ["block", "warn"]
1299
+ "unknown-package": ["block", "warn"],
1300
+ "tool-approval": ["require"]
1300
1301
  };
1301
1302
  function protectionsWireOf(raw) {
1302
1303
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
@@ -1318,7 +1319,8 @@ function parseProtections(raw) {
1318
1319
  protectedPush: r["protected-push"] === "require",
1319
1320
  deployApproval: r["deploy-approval"] === "require",
1320
1321
  destructiveCommands: r["destructive-commands"] === "require",
1321
- unknownPackage: r["unknown-package"] === "block" || r["unknown-package"] === "warn" ? r["unknown-package"] : null
1322
+ unknownPackage: r["unknown-package"] === "block" || r["unknown-package"] === "warn" ? r["unknown-package"] : null,
1323
+ toolApproval: r["tool-approval"] === "require"
1322
1324
  };
1323
1325
  }
1324
1326
  var SECRET_FILE_MESSAGE = "Blocked by your organization's protection: secret files are never sent to the model. Do not try to read this file another way; ask the user for the specific value your task needs, or use the example file (such as .env.example).";
@@ -1857,6 +1859,7 @@ var RegistryChecker = class {
1857
1859
  }
1858
1860
  };
1859
1861
  function mayNeedProtection(command, p) {
1862
+ if (p.toolApproval) return true;
1860
1863
  if (p.destructiveCommands && isDestructiveCommand(command)) return true;
1861
1864
  if (p.deployApproval && isDeployCommand(command)) return true;
1862
1865
  if (p.protectedPush && gitPushes(command).length > 0) return true;
@@ -1889,6 +1892,7 @@ async function protectionVerdict(command, p, deps) {
1889
1892
  if (p.destructiveCommands && isDestructiveCommand(command)) reasons.push("PROTECTION_DESTRUCTIVE_COMMAND");
1890
1893
  if (p.deployApproval && isDeployCommand(command)) reasons.push("PROTECTION_DEPLOY");
1891
1894
  if (p.protectedPush && await isProtectedPush(command, deps.currentBranch)) reasons.push("PROTECTION_PROTECTED_PUSH");
1895
+ if (p.toolApproval && !reasons.length) reasons.push("PROTECTION_TOOL_APPROVAL");
1892
1896
  if (!reasons.length) return { kind: "allow" };
1893
1897
  return { kind: "ask", reasons, ...pkg ? { package: pkg.name, packageReason: pkg.reason } : {} };
1894
1898
  }
@@ -1896,7 +1900,12 @@ var PROTECTION_MESSAGES = {
1896
1900
  rejected: "Your organization's protection requires a person to approve this command, and the user denied it. It did not run. Do not run it again or another way unless the user asks.",
1897
1901
  timedOut: (minutes) => `Your organization's protection requires a person to approve this command, and nobody answered within ${minutes} minutes. It did not run. Ask the user before trying it again.`,
1898
1902
  stopped: "The request was stopped before the command ran.",
1899
- unavailable: "Your organization's protection requires a person to approve this command, and the approval could not be requested. It did not run. Tell the user; do not try to run it another way."
1903
+ unavailable: "Your organization's protection requires a person to approve this command, and the approval could not be requested. It did not run. Tell the user; do not try to run it another way.",
1904
+ // A file change under tool-approval.
1905
+ editRejected: (path) => `Your organization's protection requires a person to approve every change, and the user denied this change to ${path}. The file was not changed. Do not change it another way unless the user asks.`,
1906
+ editTimedOut: (path, minutes) => `Your organization's protection requires a person to approve every change, and nobody answered about ${path} within ${minutes} minutes. The file was not changed. Ask the user before trying again.`,
1907
+ editStopped: "The request was stopped before the file was changed.",
1908
+ editUnavailable: (path) => `Your organization's protection requires a person to approve every change, and the approval for ${path} could not be requested. The file was not changed. Tell the user; do not change it another way.`
1900
1909
  };
1901
1910
 
1902
1911
  // src/application/services/workspaceSandbox/SessionTransport.ts
@@ -3031,6 +3040,15 @@ var BrowserCommandGate = class {
3031
3040
  this.chain = next.catch(() => void 0);
3032
3041
  return next;
3033
3042
  }
3043
+ checkProtectedEdit(path, opts = {}) {
3044
+ const next = this.chain.then(() => this.ask(`edit ${path}`, {
3045
+ signal: opts.signal,
3046
+ edit: { path, ...opts.preview ? { preview: opts.preview } : {} },
3047
+ protection: { reasons: ["PROTECTION_TOOL_APPROVAL"] }
3048
+ }));
3049
+ this.chain = next.catch(() => void 0);
3050
+ return next;
3051
+ }
3034
3052
  checkEdit(path, opts = {}) {
3035
3053
  const next = this.chain.then(() => this.ask(`edit ${path}`, { signal: opts.signal, edit: { path, ...opts.preview ? { preview: opts.preview } : {} } }));
3036
3054
  this.chain = next.catch(() => void 0);
@@ -3047,7 +3065,7 @@ var BrowserCommandGate = class {
3047
3065
  command,
3048
3066
  cwd: this.o.root,
3049
3067
  ...opts.description ? { description: opts.description.slice(0, 1e3) } : {},
3050
- reasons: opts.edit ? [] : protection ? [...protection.reasons, ...approvalReasons(command)] : approvalReasons(command),
3068
+ reasons: protection ? [...protection.reasons, ...opts.edit ? [] : approvalReasons(command)] : opts.edit ? [] : approvalReasons(command),
3051
3069
  suggestedRule: opts.edit || protection ? null : suggestRule(command),
3052
3070
  ...opts.edit ? { edit: { path: opts.edit.path, ...opts.edit.preview ? { preview: opts.edit.preview.slice(0, 4e3) } : {} } } : {},
3053
3071
  ...protection?.package ? { package: protection.package, ...protection.packageReason ? { packageReason: protection.packageReason } : {} } : {}
@@ -3055,6 +3073,7 @@ var BrowserCommandGate = class {
3055
3073
  } catch (e) {
3056
3074
  this.o.log?.(`command approval could not be requested: ${e.message}`);
3057
3075
  const code = e instanceof CommandApprovalError ? e.code : null;
3076
+ if (protection && opts.edit) return { allow: false, message: PROTECTION_MESSAGES.editUnavailable(opts.edit.path) };
3058
3077
  if (protection && code !== "COMMAND_NOT_ALLOWED") return { allow: false, message: PROTECTION_MESSAGES.unavailable };
3059
3078
  return {
3060
3079
  allow: false,
@@ -3072,10 +3091,12 @@ var BrowserCommandGate = class {
3072
3091
  if (!decision) {
3073
3092
  const reason = opts.signal?.aborted ? "STOPPED" : "TIMED_OUT";
3074
3093
  void this.o.api.cancel(approvalId, reason).catch(() => void 0);
3094
+ if (protection && opts.edit) return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.editStopped : PROTECTION_MESSAGES.editTimedOut(opts.edit.path, Math.round(timeoutMs / 6e4)) };
3075
3095
  if (protection) return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.stopped : PROTECTION_MESSAGES.timedOut(Math.round(timeoutMs / 6e4)) };
3076
3096
  return reason === "STOPPED" ? { allow: false, message: STOPPED } : { allow: false, message: `The user did not answer the approval for this command within ${Math.round(timeoutMs / 6e4)} minutes, so it did not run. Ask the user before trying it again.` };
3077
3097
  }
3078
3098
  if (decision.decision === "REJECT") {
3099
+ if (protection && opts.edit) return { allow: false, message: PROTECTION_MESSAGES.editRejected(opts.edit.path) };
3079
3100
  if (protection) return { allow: false, message: PROTECTION_MESSAGES.rejected };
3080
3101
  return opts.edit ? { allow: false, message: `The user declined this change to ${opts.edit.path} in the browser. The file was not changed. Ask the user what to do differently.` } : { allow: false, message: "The user denied this command in the browser. It did not run. Do not run the same command again unless the user asks; continue another way or ask the user." };
3081
3102
  }
@@ -4821,6 +4842,8 @@ function sandboxTestEnv() {
4821
4842
  "BUNDLE_PATH",
4822
4843
  "PUB_CACHE",
4823
4844
  "FLUTTER_ROOT",
4845
+ "MIX_HOME",
4846
+ "HEX_HOME",
4824
4847
  "MISE_DATA_DIR",
4825
4848
  "MISE_CACHE_DIR",
4826
4849
  "ELIXIR_ERL_OPTIONS"
@@ -4943,6 +4966,8 @@ var ENGINE_MESSAGES = {
4943
4966
  // Pull requests.
4944
4967
  BASE_ADVANCED: "The pull request could not be opened: the base branch in the repository is not at the commit this change was made on.",
4945
4968
  PR_FAILED: "The pull request could not be opened.",
4969
+ PROVIDER_CONNECTION_REQUIRED: "The pull request could not be opened: the provider connection of {repoFullName} needs to be reconnected in ScaleQuality.",
4970
+ REPOSITORY_NOT_IN_PROJECT: "The pull request could not be opened: {repoFullName} is no longer a repository of this project.",
4946
4971
  // Attachments and mentions.
4947
4972
  ATTACHMENT_SKIPPED: "An attached image could not be read and was left out.",
4948
4973
  MENTION_SKIPPED: "{path} could not be attached ({reason}).",
@@ -5176,7 +5201,7 @@ function limitInEngineText(text3) {
5176
5201
  // src/application/services/workspaceSandbox/scalequalityTools.ts
5177
5202
  var import_crypto4 = require("crypto");
5178
5203
 
5179
- // node_modules/zod/v3/external.js
5204
+ // ../../scalequality-ai-governance-service/node_modules/zod/v3/external.js
5180
5205
  var external_exports = {};
5181
5206
  __export(external_exports, {
5182
5207
  BRAND: () => BRAND,
@@ -5288,7 +5313,7 @@ __export(external_exports, {
5288
5313
  void: () => voidType
5289
5314
  });
5290
5315
 
5291
- // node_modules/zod/v3/helpers/util.js
5316
+ // ../../scalequality-ai-governance-service/node_modules/zod/v3/helpers/util.js
5292
5317
  var util;
5293
5318
  (function(util2) {
5294
5319
  util2.assertEqual = (_) => {
@@ -5422,7 +5447,7 @@ var getParsedType = (data) => {
5422
5447
  }
5423
5448
  };
5424
5449
 
5425
- // node_modules/zod/v3/ZodError.js
5450
+ // ../../scalequality-ai-governance-service/node_modules/zod/v3/ZodError.js
5426
5451
  var ZodIssueCode = util.arrayToEnum([
5427
5452
  "invalid_type",
5428
5453
  "invalid_literal",
@@ -5540,7 +5565,7 @@ ZodError.create = (issues) => {
5540
5565
  return error;
5541
5566
  };
5542
5567
 
5543
- // node_modules/zod/v3/locales/en.js
5568
+ // ../../scalequality-ai-governance-service/node_modules/zod/v3/locales/en.js
5544
5569
  var errorMap = (issue, _ctx) => {
5545
5570
  let message;
5546
5571
  switch (issue.code) {
@@ -5643,7 +5668,7 @@ var errorMap = (issue, _ctx) => {
5643
5668
  };
5644
5669
  var en_default = errorMap;
5645
5670
 
5646
- // node_modules/zod/v3/errors.js
5671
+ // ../../scalequality-ai-governance-service/node_modules/zod/v3/errors.js
5647
5672
  var overrideErrorMap = en_default;
5648
5673
  function setErrorMap(map) {
5649
5674
  overrideErrorMap = map;
@@ -5652,7 +5677,7 @@ function getErrorMap() {
5652
5677
  return overrideErrorMap;
5653
5678
  }
5654
5679
 
5655
- // node_modules/zod/v3/helpers/parseUtil.js
5680
+ // ../../scalequality-ai-governance-service/node_modules/zod/v3/helpers/parseUtil.js
5656
5681
  var makeIssue = (params) => {
5657
5682
  const { data, path, errorMaps, issueData } = params;
5658
5683
  const fullPath = [...path, ...issueData.path || []];
@@ -5762,14 +5787,14 @@ var isDirty = (x) => x.status === "dirty";
5762
5787
  var isValid = (x) => x.status === "valid";
5763
5788
  var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
5764
5789
 
5765
- // node_modules/zod/v3/helpers/errorUtil.js
5790
+ // ../../scalequality-ai-governance-service/node_modules/zod/v3/helpers/errorUtil.js
5766
5791
  var errorUtil;
5767
5792
  (function(errorUtil2) {
5768
5793
  errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
5769
5794
  errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
5770
5795
  })(errorUtil || (errorUtil = {}));
5771
5796
 
5772
- // node_modules/zod/v3/types.js
5797
+ // ../../scalequality-ai-governance-service/node_modules/zod/v3/types.js
5773
5798
  var ParseInputLazyPath = class {
5774
5799
  constructor(parent, value, path, key) {
5775
5800
  this._cachedPath = [];
@@ -9615,7 +9640,8 @@ var SdkEventMapper = class {
9615
9640
  startedAt: iso(startedAt)
9616
9641
  } });
9617
9642
  const target = activityTarget(name, input);
9618
- this.cb.emit({ type: "activity", data: { tool: name, ...target ? { target } : {} } });
9643
+ const description = d.code === "STEP_COMMAND_DESCRIBED" && typeof d.params?.description === "string" ? d.params.description : void 0;
9644
+ this.cb.emit({ type: "activity", data: { tool: name, ...target ? { target } : {}, ...description ? { description } : {} } });
9619
9645
  }
9620
9646
  stepDone(id, t, status, endedAt = this.now()) {
9621
9647
  this.cb.emit({ type: "step", data: {
@@ -9826,7 +9852,8 @@ function activityTarget(name, input) {
9826
9852
  for (const part of s("command").split(/&&|\|\||[;|\n]/)) {
9827
9853
  for (const token of part.trim().split(/\s+/)) {
9828
9854
  if (!token || token.includes("=") || COMMAND_WRAPPERS.has(token)) continue;
9829
- if (COMMAND_SETUP.has(token)) break;
9855
+ if (SHELL_PREFIX.has(token)) continue;
9856
+ if (COMMAND_SETUP.has(token) || SHELL_HEADER.has(token)) break;
9830
9857
  if (!/^[A-Za-z0-9._/-]+$/.test(token)) return void 0;
9831
9858
  const word = token.split("/").filter(Boolean).pop() ?? "";
9832
9859
  return word && word.length <= 40 ? word : void 0;
@@ -9837,6 +9864,8 @@ function activityTarget(name, input) {
9837
9864
  }
9838
9865
  var COMMAND_WRAPPERS = /* @__PURE__ */ new Set(["sudo", "env", "time", "nice", "exec", "command", "nohup"]);
9839
9866
  var COMMAND_SETUP = /* @__PURE__ */ new Set(["cd", "pushd", "popd", "export", "source", ".", "set", "unset"]);
9867
+ var SHELL_HEADER = /* @__PURE__ */ new Set(["for", "case", "select", "[", "[[", "test", "true", "false", ":"]);
9868
+ var SHELL_PREFIX = /* @__PURE__ */ new Set(["do", "then", "else", "elif", "if", "while", "until", "done", "fi", "esac", "!", "{", "}", "(", ")"]);
9840
9869
  function blockText(content) {
9841
9870
  if (typeof content === "string") return content;
9842
9871
  if (Array.isArray(content)) {
@@ -10000,12 +10029,16 @@ var TurnTiming = class {
10000
10029
  // src/application/services/workspaceSandbox/protectionApproval.ts
10001
10030
  function gateProtectionApprover(gate) {
10002
10031
  if (!gate.checkProtected) return null;
10003
- return { ask: (command, check) => gate.checkProtected(command, check) };
10032
+ return {
10033
+ ask: (command, check) => gate.checkProtected(command, check),
10034
+ ...gate.checkProtectedEdit ? { askEdit: (path, check) => gate.checkProtectedEdit(path, check) } : {}
10035
+ };
10004
10036
  }
10005
10037
  function cloudProtectionApprover(o) {
10006
10038
  let chain = Promise.resolve();
10007
10039
  const ask = async (command, check) => {
10008
- if (check.signal?.aborted) return { allow: false, message: PROTECTION_MESSAGES.stopped };
10040
+ const edit = check.edit;
10041
+ if (check.signal?.aborted) return { allow: false, message: edit ? PROTECTION_MESSAGES.editStopped : PROTECTION_MESSAGES.stopped };
10009
10042
  let approvalId;
10010
10043
  try {
10011
10044
  ({ approvalId } = await o.calls.requestCommandApproval({
@@ -10014,11 +10047,12 @@ function cloudProtectionApprover(o) {
10014
10047
  ...check.description ? { description: check.description.slice(0, 1e3) } : {},
10015
10048
  reasons: [...check.reasons],
10016
10049
  suggestedRule: null,
10050
+ ...edit ? { edit: { path: edit.path, ...edit.preview ? { preview: edit.preview.slice(0, 4e3) } : {} } } : {},
10017
10051
  ...check.package ? { package: check.package, ...check.packageReason ? { packageReason: check.packageReason } : {} } : {}
10018
10052
  }));
10019
10053
  } catch (e) {
10020
10054
  o.log?.(`protection approval could not be requested: ${e.message}`);
10021
- return { allow: false, message: PROTECTION_MESSAGES.unavailable };
10055
+ return { allow: false, message: edit ? PROTECTION_MESSAGES.editUnavailable(edit.path) : PROTECTION_MESSAGES.unavailable };
10022
10056
  }
10023
10057
  const timeoutMs = o.timeoutMs ?? COMMAND_APPROVAL_TIMEOUT_MS;
10024
10058
  const timer = new AbortController();
@@ -10029,16 +10063,21 @@ function cloudProtectionApprover(o) {
10029
10063
  if (!decision) {
10030
10064
  const reason = check.signal?.aborted ? "STOPPED" : "TIMED_OUT";
10031
10065
  void o.calls.cancelCommandApproval(approvalId, reason).catch(() => void 0);
10032
- return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.stopped : PROTECTION_MESSAGES.timedOut(Math.round(timeoutMs / 6e4)) };
10066
+ const minutes = Math.round(timeoutMs / 6e4);
10067
+ if (edit) return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.editStopped : PROTECTION_MESSAGES.editTimedOut(edit.path, minutes) };
10068
+ return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.stopped : PROTECTION_MESSAGES.timedOut(minutes) };
10033
10069
  }
10034
- return decision.decision === "APPROVE" ? { allow: true } : { allow: false, message: PROTECTION_MESSAGES.rejected };
10070
+ if (decision.decision === "APPROVE") return { allow: true };
10071
+ return { allow: false, message: edit ? PROTECTION_MESSAGES.editRejected(edit.path) : PROTECTION_MESSAGES.rejected };
10072
+ };
10073
+ const queued = (run2) => {
10074
+ const next = chain.then(run2);
10075
+ chain = next.catch(() => void 0);
10076
+ return next;
10035
10077
  };
10036
10078
  return {
10037
- ask(command, check) {
10038
- const next = chain.then(() => ask(command, check));
10039
- chain = next.catch(() => void 0);
10040
- return next;
10041
- }
10079
+ ask: (command, check) => queued(() => ask(command, check)),
10080
+ askEdit: (path, check) => queued(() => ask(`edit ${path}`, { reasons: ["PROTECTION_TOOL_APPROVAL"], signal: check.signal, edit: { path, ...check.preview ? { preview: check.preview } : {} } }))
10042
10081
  };
10043
10082
  }
10044
10083
 
@@ -13150,16 +13189,33 @@ _Measured on an earlier version of this change._`);
13150
13189
  if (e instanceof TransportError && e.code === REPOSITORY_NOT_IN_SCOPE) {
13151
13190
  return text(`${REPOSITORY_NOT_IN_SCOPE}: ${target} left this session's scope before the pull request was opened. It was not opened.`, true);
13152
13191
  }
13153
- if (status === 409) {
13154
- const where = this.local ? " Update this folder to the latest commit of the base branch (git pull), then ask again." : "";
13155
- this.error("BASE_ADVANCED", { repoFullName: target, local: this.local });
13156
- return text(`The pull request could not be opened: the base branch in the repository is not at the commit this change was made on.${where} Tell the user; do not retry without being asked.`, true);
13157
- }
13158
- this.error("PR_FAILED", { repoFullName: target });
13159
- return text("The pull request could not be opened. Tell the user; do not retry without being asked.", true);
13192
+ const failure = pullRequestFailure(status, e instanceof TransportError ? e.code : void 0, target, this.local);
13193
+ this.error(failure.code, failure.params);
13194
+ return text(`${failure.say} Tell the user; do not retry without being asked.`, true);
13160
13195
  }
13161
13196
  }
13162
13197
  };
13198
+ function pullRequestFailure(status, code, repoFullName, local) {
13199
+ if (status === 409 && code === "PROVIDER_CONNECTION_REQUIRED") return {
13200
+ code: "PROVIDER_CONNECTION_REQUIRED",
13201
+ params: { repoFullName },
13202
+ say: `The pull request could not be opened: the provider connection of ${repoFullName} needs to be reconnected in ScaleQuality.`
13203
+ };
13204
+ if (status === 409 && code === "REPOSITORY_NOT_IN_PROJECT") return {
13205
+ code: "REPOSITORY_NOT_IN_PROJECT",
13206
+ params: { repoFullName },
13207
+ say: `The pull request could not be opened: ${repoFullName} is no longer a repository of this project.`
13208
+ };
13209
+ if (status === 409 && (!code || code === "BASE_ADVANCED")) {
13210
+ const where = local ? " Update this folder to the latest commit of the base branch (git pull), then ask again." : "";
13211
+ return {
13212
+ code: "BASE_ADVANCED",
13213
+ params: { repoFullName, local },
13214
+ say: `The pull request could not be opened: the base branch in the repository is not at the commit this change was made on.${where}`
13215
+ };
13216
+ }
13217
+ return { code: "PR_FAILED", params: { repoFullName, ...code ? { reason: code } : {} }, say: "The pull request could not be opened." };
13218
+ }
13163
13219
  var LOCAL_ENV_PASSTHROUGH = [
13164
13220
  "LOGNAME",
13165
13221
  "COLORTERM",
@@ -13312,11 +13368,11 @@ function buildQueryOptions(o) {
13312
13368
  }
13313
13369
  const protections = o.protections?.() ?? NO_PROTECTIONS;
13314
13370
  const d = await decideToolUse(name, input.tool_input ?? {}, { ...o.policy, protections });
13315
- const guarded = d.behavior === "allow" && name === "Bash" && mayNeedProtection(String(d.updatedInput.command ?? ""), protections);
13371
+ const guarded = d.behavior === "allow" && (name === "Bash" && mayNeedProtection(String(d.updatedInput.command ?? ""), protections) || protections.toolApproval && !o.plan && name in WRITE_TOOL_PATHS);
13316
13372
  const gated = guarded || d.behavior === "allow" && !!gate && (name === "Bash" || !!gate.checkEdit && !o.plan && name in WRITE_TOOL_PATHS);
13317
13373
  if (d.behavior === "allow" && !gated) await beforeWrite(name, d.updatedInput);
13318
13374
  if (gated) {
13319
- return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: guarded ? "This command follows the organization's protections." : "Commands and changes on this machine follow the person's permission mode." } };
13375
+ return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: guarded ? "This action follows the organization's protections." : "Commands and changes on this machine follow the person's permission mode." } };
13320
13376
  }
13321
13377
  return d.behavior === "allow" ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: d.updatedInput } } : { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: d.message } };
13322
13378
  };
@@ -13354,10 +13410,18 @@ function buildQueryOptions(o) {
13354
13410
  protectionApproved = true;
13355
13411
  }
13356
13412
  }
13357
- if (gate?.checkEdit && !o.plan && toolName in WRITE_TOOL_PATHS) {
13413
+ const changes = !o.plan && toolName in WRITE_TOOL_PATHS;
13414
+ const changePath = () => {
13358
13415
  const field = WRITE_TOOL_PATHS[toolName];
13359
- const path = typeof d.updatedInput[field] === "string" ? (0, import_path15.relative)(o.root, d.updatedInput[field]) || String(d.updatedInput[field]) : "";
13360
- const r = await gate.checkEdit(path, { preview: editPreview(toolName, d.updatedInput), signal: opts?.signal ?? o.abortController.signal });
13416
+ return typeof d.updatedInput[field] === "string" ? (0, import_path15.relative)(o.root, d.updatedInput[field]) || String(d.updatedInput[field]) : "";
13417
+ };
13418
+ if (changes && protections.toolApproval) {
13419
+ const path = changePath(), askEdit = o.protection?.approver?.askEdit;
13420
+ if (!askEdit) return { behavior: "deny", message: PROTECTION_MESSAGES.editUnavailable(path) };
13421
+ const r = await askEdit(path, { preview: editPreview(toolName, d.updatedInput), signal: opts?.signal ?? o.abortController.signal });
13422
+ if (!r.allow) return { behavior: "deny", message: r.message };
13423
+ } else if (gate?.checkEdit && changes) {
13424
+ const r = await gate.checkEdit(changePath(), { preview: editPreview(toolName, d.updatedInput), signal: opts?.signal ?? o.abortController.signal });
13361
13425
  if (!r.allow) return { behavior: "deny", message: r.message };
13362
13426
  }
13363
13427
  await beforeWrite(toolName, d.updatedInput);
@@ -13420,7 +13484,8 @@ function timedGate(gate, took) {
13420
13484
  return {
13421
13485
  check: (command, opts) => timed(() => gate.check(command, opts)),
13422
13486
  ...gate.checkEdit ? { checkEdit: (path, opts) => timed(() => gate.checkEdit(path, opts)) } : {},
13423
- ...gate.checkProtected ? { checkProtected: (command, opts) => timed(() => gate.checkProtected(command, opts)) } : {}
13487
+ ...gate.checkProtected ? { checkProtected: (command, opts) => timed(() => gate.checkProtected(command, opts)) } : {},
13488
+ ...gate.checkProtectedEdit ? { checkProtectedEdit: (path, opts) => timed(() => gate.checkProtectedEdit(path, opts)) } : {}
13424
13489
  };
13425
13490
  }
13426
13491
  async function hasLocalTranscript(configDir, sessionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalequality/cli",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "ScaleQuality CLI. Connect your computer to the ScaleQuality AI Workspace (`scalequality login`), run its coding engine in any folder of your computer, and import your Claude Code and Codex conversations.",
5
5
  "type": "module",
6
6
  "bin": {