@mars-sea/dsh-commandcode-provider 0.11.12 → 0.11.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -738,6 +738,16 @@ declare class CommandCodeAccountPool {
738
738
  private pick;
739
739
  }
740
740
  //#endregion
741
+ //#region src/command-guard.d.ts
742
+ /**
743
+ * The auto-approve strictness a user picks (`Config.commandGuardLevel`). Each
744
+ * level is the probability every verdict must reach before the guard grants
745
+ * `allowed-once`; a higher level approves less, on stronger evidence. Three
746
+ * presets replace a free-form probability because the useful range is narrow
747
+ * and a hand-typed 0.6 silently turns the guard into a rubber stamp.
748
+ */
749
+ type CommandGuardLevel = 'high' | 'medium' | 'low';
750
+ //#endregion
741
751
  //#region src/capabilities.d.ts
742
752
  /**
743
753
  * Static capability snapshot for the Command Code provider: model →
@@ -1915,23 +1925,18 @@ interface Config {
1915
1925
  * opt-in, and the command text (plus the agent's own description of it) is
1916
1926
  * sent to Command Code to judge. Only commands a policy already wanted a
1917
1927
  * human to look at are ever judged, and only a confident "safe" verdict
1918
- * (`commandGuardThreshold`) skips the prompt — every other outcome, including
1928
+ * (`commandGuardLevel`) skips the prompt — every other outcome, including
1919
1929
  * any failure of the decision call itself, delegates to the normal approval
1920
1930
  * flow. See `./command-guard.ts`.
1921
1931
  */
1922
1932
  commandGuard?: boolean;
1923
1933
  /**
1924
- * Minimum probability of "safe" that lets the guard skip the approval prompt;
1925
- * defaults to 0.9. Lowering it approves more, on less evidence.
1926
- */
1927
- commandGuardThreshold?: number;
1928
- /**
1929
- * Milliseconds the guard waits for a decision before falling back to the
1930
- * human prompt; defaults to 1500. This is a latency budget for an interactive
1931
- * approval, not a request timeout — a decision that arrives late is useless,
1932
- * because the user is staring at a prompt.
1934
+ * How confident every verdict must be before the guard skips the approval
1935
+ * prompt: `high` (0.95), `medium` (0.9, the default) or `low` (0.8). A lower
1936
+ * level approves more, on less evidence. The decision budget is fixed
1937
+ * (`COMMAND_GUARD_TIMEOUT_MS`), so this is the guard's only tuning knob.
1933
1938
  */
1934
- commandGuardTimeoutMs?: number;
1939
+ commandGuardLevel?: CommandGuardLevel;
1935
1940
  /**
1936
1941
  * Whether requests enforce zero data retention: the provider then routes
1937
1942
  * them only through upstreams that keep no prompts/completions and never
package/lib/index.js CHANGED
@@ -6950,8 +6950,8 @@ function buildCommandCodeTuiSection(deps) {
6950
6950
  kind: "boolean",
6951
6951
  label: "AI command guard",
6952
6952
  descriptions: { zh: "AI 命令安全预判" },
6953
- hint: "typesafe/jev judges whether a shell command is safe and auto-approves it; everything else asks as usual. Off by default.",
6954
- hintDescriptions: { zh: "由 typesafe/jev 判断 shell 命令是否安全,安全则自动放行,其余照常弹窗。默认关闭。" },
6953
+ hint: "typesafe/jev judges whether a shell command is safe; sandbox escalations also need separate scope and necessity verdicts. All must be confident enough; everything else asks as usual. Off by default.",
6954
+ hintDescriptions: { zh: "由 typesafe/jev 判断 shell 命令是否安全;沙箱提权还要分别判断范围与必要性,三项都足够确定才自动放行,其余照常弹窗。默认关闭。" },
6955
6955
  format: (value) => value === true ? "true" : "false",
6956
6956
  parse: (text) => ({
6957
6957
  kind: "set",
@@ -6959,38 +6959,36 @@ function buildCommandCodeTuiSection(deps) {
6959
6959
  })
6960
6960
  },
6961
6961
  {
6962
- path: ["commandGuardThreshold"],
6962
+ path: ["commandGuardLevel"],
6963
6963
  group: "advanced",
6964
- kind: "number",
6965
- label: "Auto-approve threshold (0.5-1)",
6966
- descriptions: { zh: "自动放行阈值(0.5-1)" },
6967
- hint: "Approve when typesafe/jev rates the command \"safe\" at least this likely; default 0.9.",
6968
- hintDescriptions: { zh: "typesafe/jev 判定\"安全\"的概率达到该值即放行,默认 0.9。" },
6969
- parse: (text) => {
6970
- const trimmed = text.trim();
6971
- if (trimmed === "") return { kind: "clear" };
6972
- const parsed = Number(trimmed);
6973
- return Number.isFinite(parsed) && parsed >= .5 && parsed <= 1 ? {
6974
- kind: "set",
6975
- value: parsed
6976
- } : void 0;
6977
- }
6978
- },
6979
- {
6980
- path: ["commandGuardTimeoutMs"],
6981
- group: "advanced",
6982
- kind: "number",
6983
- label: "Judgement timeout (ms, 200-10000)",
6984
- descriptions: { zh: "判定超时(毫秒,200-10000)" },
6985
- hint: "If typesafe/jev does not answer in time, the normal prompt appears; default 1500.",
6986
- hintDescriptions: { zh: "typesafe/jev 超过该时间未返回就照常弹窗,默认 1500。" },
6964
+ kind: "select",
6965
+ label: "Auto-approve threshold",
6966
+ descriptions: { zh: "自动放行阈值" },
6967
+ hint: "How confident typesafe/jev must be before a command runs without asking. Default: medium.",
6968
+ hintDescriptions: { zh: "typesafe/jev 需要多确定才免弹窗放行;默认「中」。" },
6969
+ options: [
6970
+ {
6971
+ value: "high",
6972
+ label: "High (0.95, approves least)",
6973
+ descriptions: { zh: "高(0.95,放行最少)" }
6974
+ },
6975
+ {
6976
+ value: "medium",
6977
+ label: "Medium (0.9)",
6978
+ descriptions: { zh: "中(0.9)" }
6979
+ },
6980
+ {
6981
+ value: "low",
6982
+ label: "Low (0.8, approves most)",
6983
+ descriptions: { zh: "低(0.8,放行最多)" }
6984
+ }
6985
+ ],
6986
+ format: (value) => value === "high" || value === "low" ? value : "medium",
6987
6987
  parse: (text) => {
6988
6988
  const trimmed = text.trim();
6989
- if (trimmed === "") return { kind: "clear" };
6990
- const parsed = Number(trimmed);
6991
- return Number.isFinite(parsed) && parsed >= 200 && parsed <= 1e4 ? {
6989
+ return trimmed === "high" || trimmed === "medium" || trimmed === "low" ? {
6992
6990
  kind: "set",
6993
- value: parsed
6991
+ value: trimmed
6994
6992
  } : void 0;
6995
6993
  }
6996
6994
  },
@@ -7311,8 +7309,6 @@ function transportBudgetMessage(failureMessage, maxRetries) {
7311
7309
  */
7312
7310
  /** Path of the decision endpoint, relative to `apiBase`. */
7313
7311
  const SYSTEMONE_ROUTE = "/provider/v1/systemone";
7314
- /** Milliseconds a decision may take before the caller gives up on it. */
7315
- const SYSTEMONE_DEFAULT_TIMEOUT_MS = 1500;
7316
7312
  /** The one error type this client throws. Callers fail closed on it. */
7317
7313
  var SystemOneError = class extends Error {
7318
7314
  kind;
@@ -7570,13 +7566,40 @@ async function runSystemOne(deps, request, options = {}) {
7570
7566
  //#region src/command-guard.ts
7571
7567
  /** Tool names whose `arguments.command` this guard judges. */
7572
7568
  const COMMAND_GUARD_SHELL_TOOLS = ["bash", "pwsh"];
7573
- /** Question id the safety verdict is keyed by. */
7569
+ /** Question id the primary safety verdict is keyed by. */
7574
7570
  const COMMAND_GUARD_QUESTION_ID = "safe";
7575
- /** Decision budget; the approval prompt must not wait longer than this. */
7576
- const COMMAND_GUARD_DEFAULT_TIMEOUT_MS = SYSTEMONE_DEFAULT_TIMEOUT_MS;
7577
- /** Bounds of the configurable probability, mirrored by the settings schemas. */
7578
- const COMMAND_GUARD_MIN_THRESHOLD = .5;
7579
- const COMMAND_GUARD_MAX_TIMEOUT_MS = 1e4;
7571
+ /** Question id for the narrow-purpose part of a sandbox escalation. */
7572
+ const COMMAND_GUARD_ESCALATION_SCOPE_QUESTION_ID = "escalation_scope";
7573
+ /** Question id for the necessity/proportionality part of a sandbox escalation. */
7574
+ const COMMAND_GUARD_ESCALATION_NECESSITY_QUESTION_ID = "escalation_necessity";
7575
+ /** The selectable levels, strictest first (the order the settings surfaces list them in). */
7576
+ const COMMAND_GUARD_LEVELS = [
7577
+ "high",
7578
+ "medium",
7579
+ "low"
7580
+ ];
7581
+ /** Each level's probability threshold. */
7582
+ const COMMAND_GUARD_LEVEL_THRESHOLDS = {
7583
+ high: .95,
7584
+ medium: .9,
7585
+ low: .8
7586
+ };
7587
+ /** The level an unset config reads as. */
7588
+ const COMMAND_GUARD_DEFAULT_LEVEL = "medium";
7589
+ /** Probability of "safe" at or above which the guard grants `allowed-once` by default. */
7590
+ const COMMAND_GUARD_DEFAULT_THRESHOLD = COMMAND_GUARD_LEVEL_THRESHOLDS[COMMAND_GUARD_DEFAULT_LEVEL];
7591
+ /** The threshold for a stored level; anything unrecognized reads as the default level. */
7592
+ function commandGuardThreshold(level) {
7593
+ return typeof level === "string" && Object.hasOwn(COMMAND_GUARD_LEVEL_THRESHOLDS, level) ? COMMAND_GUARD_LEVEL_THRESHOLDS[level] : COMMAND_GUARD_DEFAULT_THRESHOLD;
7594
+ }
7595
+ /**
7596
+ * Decision budget; the approval prompt must not wait longer than this. Fixed
7597
+ * rather than configurable: it only bounds how long a late verdict can delay
7598
+ * the prompt, and every failure (a timeout included) already falls back to
7599
+ * asking the user, so there is nothing for a user to tune. 3 s leaves room for
7600
+ * credential resolution plus one decision round trip on a slow link.
7601
+ */
7602
+ const COMMAND_GUARD_TIMEOUT_MS = 3e3;
7580
7603
  /**
7581
7604
  * Longest command the guard will send for judgement. A longer one is delegated
7582
7605
  * instead of truncated: a verdict on the first N characters of a command is a
@@ -7755,17 +7778,21 @@ function shellCommandOf(name, args) {
7755
7778
  /**
7756
7779
  * Build the decision request for one command.
7757
7780
  *
7758
- * The question is deliberately one `noul`: a single probability is a decision
7759
- * rule that can be explained, thresholded and logged in one line, and the
7760
- * criteria carry the whole distinction between "reads, or writes reversibly
7761
- * inside the project" and "everything else". The agent's own `description` is
7762
- * included as context, but the criteria say the command text itself decides — a
7781
+ * A normal approval asks one question. A call that explicitly requests a wider
7782
+ * sandbox gets the same safety question plus two veto questions. This keeps the
7783
+ * common case cheap while making the broader permission change an explicit
7784
+ * decision: JEV must agree that the extra access has a narrow purpose and is
7785
+ * necessary, not merely that the command text looks familiar.
7786
+ *
7787
+ * The agent's own `description` and the harness's `reason` are included as
7788
+ * context, but the criteria say the command text itself decides — a
7763
7789
  * model-written description is a claim, not evidence.
7764
7790
  *
7765
7791
  * @param fact - the command and its context.
7766
7792
  * @returns the request to send.
7767
7793
  */
7768
7794
  function buildSafetyRequest(fact) {
7795
+ const escalated = fact.sandboxPermissions !== void 0;
7769
7796
  const lines = [
7770
7797
  `Tool: ${fact.tool}`,
7771
7798
  "Command:",
@@ -7777,24 +7804,43 @@ function buildSafetyRequest(fact) {
7777
7804
  ];
7778
7805
  const question = {
7779
7806
  type: "noul",
7780
- instructions: "Is it safe to run this command right now WITHOUT asking the user first? Judge the command text itself; a description is only a claim about it.",
7807
+ instructions: escalated ? "Is it safe to run this command right now WITHOUT asking the user first, including the requested sandbox escalation? Judge the command text itself; a description or stated reason is only a claim about it." : "Is it safe to run this command right now WITHOUT asking the user first? Judge the command text itself; a description is only a claim about it.",
7781
7808
  criteria: {
7782
- true: "Read-only, or its effects stay inside the current project and are trivially reversible: no data leaves the machine, nothing outside the project is modified, and no credentials, git history or remote state are touched.",
7783
- false: "Anything else. Deleting or overwriting files outside the project, rewriting or publishing git history, uploading data anywhere, installing system or global packages, changing permissions, ownership or credentials, killing processes, running as another user or as root, piping a download into a shell, or a command obfuscated enough that its effect cannot be read."
7809
+ true: escalated ? "A normal, non-obfuscated read, build, or test operation whose outside-workspace effects are limited to conventional build/dependency caches or temporary files and are easily reversible: no unrelated user or system data is accessed, no data leaves the machine, and no credentials, git history or remote state are touched." : "Read-only, or its effects stay inside the current project and are trivially reversible: no data leaves the machine, nothing outside the project is modified, and no credentials, git history or remote state are touched.",
7810
+ false: "Anything else. Broad or unrelated outside-workspace access, deleting or overwriting files outside the project, rewriting or publishing git history, uploading data anywhere, installing system or global packages, changing permissions, ownership or credentials, killing processes, running as another user or as root, piping a download into a shell, or a command obfuscated enough that its effect cannot be read."
7784
7811
  }
7785
7812
  };
7813
+ const questions = { [COMMAND_GUARD_QUESTION_ID]: question };
7814
+ if (escalated) {
7815
+ questions[COMMAND_GUARD_ESCALATION_SCOPE_QUESTION_ID] = {
7816
+ type: "noul",
7817
+ instructions: "Is the requested wider sandbox access limited to a narrow, conventional purpose supported by this command, such as a build cache, dependency cache, or temporary directory, rather than unrelated user files, credentials, other projects, or the network?",
7818
+ criteria: {
7819
+ true: "The wider access is plausibly limited to conventional local build/dependency caches or temporary files required by the command, with no unrelated host data or remote transfer in scope.",
7820
+ false: "The command needs broad or unrelated host access, cannot establish a narrow purpose, or would use the wider permission to reach credentials, other projects, user files, or the network."
7821
+ }
7822
+ };
7823
+ questions[COMMAND_GUARD_ESCALATION_NECESSITY_QUESTION_ID] = {
7824
+ type: "noul",
7825
+ instructions: "Is the requested sandbox escalation necessary and proportionate for this one command, rather than broader than the command needs? Treat the stated reason as a claim to check against the command, not as proof.",
7826
+ criteria: {
7827
+ true: "The command and its stated purpose support a one-shot escalation that is needed for the task and is no broader than the requested effect requires.",
7828
+ false: "The wider mode is unnecessary, broader than needed, unsupported by the command, or the command is too ambiguous to justify the extra authority."
7829
+ }
7830
+ };
7831
+ }
7786
7832
  return {
7787
7833
  state: lines.join("\n"),
7788
- questions: { [COMMAND_GUARD_QUESTION_ID]: question }
7834
+ questions
7789
7835
  };
7790
7836
  }
7791
7837
  /**
7792
- * Read the safety probability out of a decision response.
7838
+ * Read a probability out of a decision response.
7793
7839
  *
7794
7840
  * @param response - the parsed response.
7795
7841
  * @param questionId - the question id to read (defaults to the guard's own).
7796
- * @returns the probability of "safe", or undefined when the answer is missing
7797
- * or is not a `noul` verdict.
7842
+ * @returns the probability, or undefined when the answer is missing or is not
7843
+ * a `noul` verdict.
7798
7844
  */
7799
7845
  function safeProbabilityOf(response, questionId = COMMAND_GUARD_QUESTION_ID) {
7800
7846
  const answer = response.answers[questionId];
@@ -7901,7 +7947,7 @@ var CommandGuard = class {
7901
7947
  const agentDecisions = request.agent === void 0 ? void 0 : this.decisions.get(request.agent);
7902
7948
  const memo = agentDecisions?.get(memoKey);
7903
7949
  if (memo !== void 0) {
7904
- if (now - memo.at < 6e5) return this.verdict(memo.probability, settings.threshold, "memo", excerpt);
7950
+ if (now - memo.at < 6e5) return this.verdict(memo, settings.threshold, "memo", excerpt, full.sandboxPermissions !== void 0);
7905
7951
  agentDecisions?.delete(memoKey);
7906
7952
  }
7907
7953
  let response;
@@ -7923,12 +7969,28 @@ var CommandGuard = class {
7923
7969
  reason: "the decision carried no safe verdict",
7924
7970
  command: excerpt
7925
7971
  };
7972
+ let escalation;
7973
+ if (full.sandboxPermissions !== void 0) {
7974
+ const scope = safeProbabilityOf(response, COMMAND_GUARD_ESCALATION_SCOPE_QUESTION_ID);
7975
+ const necessity = safeProbabilityOf(response, COMMAND_GUARD_ESCALATION_NECESSITY_QUESTION_ID);
7976
+ if (scope === void 0 || necessity === void 0) return {
7977
+ kind: "delegate",
7978
+ reason: "the decision carried no complete sandbox-escalation verdict",
7979
+ command: excerpt
7980
+ };
7981
+ escalation = {
7982
+ scope,
7983
+ necessity
7984
+ };
7985
+ }
7986
+ const memoEntry = {
7987
+ probability,
7988
+ ...escalation === void 0 ? {} : { escalation },
7989
+ at: now
7990
+ };
7926
7991
  if (request.agent !== void 0) {
7927
7992
  const decisions = agentDecisions ?? /* @__PURE__ */ new Map();
7928
- decisions.set(memoKey, {
7929
- probability,
7930
- at: now
7931
- });
7993
+ decisions.set(memoKey, memoEntry);
7932
7994
  while (decisions.size > 64) {
7933
7995
  const oldest = decisions.keys().next();
7934
7996
  if (oldest.done === true) break;
@@ -7936,19 +7998,44 @@ var CommandGuard = class {
7936
7998
  }
7937
7999
  this.decisions.set(request.agent, decisions);
7938
8000
  }
7939
- return this.verdict(probability, settings.threshold, "model", excerpt);
8001
+ return this.verdict(memoEntry, settings.threshold, "model", excerpt, full.sandboxPermissions !== void 0);
7940
8002
  }
7941
- /** Apply the threshold to one probability. */
7942
- verdict(probability, threshold, source, command) {
7943
- return probability >= threshold ? {
8003
+ /** Apply the threshold to the primary verdict and any escalation vetoes. */
8004
+ verdict(entry, threshold, source, command, escalated) {
8005
+ if (entry.probability < threshold) return {
8006
+ kind: "delegate",
8007
+ reason: `safe probability ${entry.probability.toFixed(3)} below ${threshold}`,
8008
+ probability: entry.probability,
8009
+ command
8010
+ };
8011
+ if (!escalated) return {
7944
8012
  kind: "approve",
7945
- probability,
8013
+ probability: entry.probability,
7946
8014
  source,
7947
8015
  command
7948
- } : {
8016
+ };
8017
+ if (entry.escalation === void 0) return {
7949
8018
  kind: "delegate",
7950
- reason: `safe probability ${probability.toFixed(3)} below ${threshold}`,
7951
- probability,
8019
+ reason: "the cached sandbox-escalation verdict was incomplete",
8020
+ command
8021
+ };
8022
+ if (entry.escalation.scope < threshold) return {
8023
+ kind: "delegate",
8024
+ reason: `sandbox scope probability ${entry.escalation.scope.toFixed(3)} below ${threshold}`,
8025
+ probability: entry.probability,
8026
+ command
8027
+ };
8028
+ if (entry.escalation.necessity < threshold) return {
8029
+ kind: "delegate",
8030
+ reason: `sandbox necessity probability ${entry.escalation.necessity.toFixed(3)} below ${threshold}`,
8031
+ probability: entry.probability,
8032
+ command
8033
+ };
8034
+ return {
8035
+ kind: "approve",
8036
+ probability: entry.probability,
8037
+ source,
8038
+ ...entry.escalation === void 0 ? {} : { escalation: entry.escalation },
7952
8039
  command
7953
8040
  };
7954
8041
  }
@@ -7986,7 +8073,7 @@ function applyCommandGuard(ctx, deps) {
7986
8073
  return next();
7987
8074
  }
7988
8075
  if (verdict.kind === "approve") {
7989
- deps.log?.(`command guard auto-approved a ${request.toolName} call (safe probability ${verdict.probability.toFixed(3)}, ${verdict.source}): ${verdict.command ?? "(command unknown)"}`);
8076
+ deps.log?.(`command guard auto-approved a ${request.toolName} call (safe probability ${verdict.probability.toFixed(3)}${verdict.escalation === void 0 ? "" : `, sandbox scope ${verdict.escalation.scope.toFixed(3)}, necessity ${verdict.escalation.necessity.toFixed(3)}`}, ${verdict.source}): ${verdict.command ?? "(command unknown)"}`);
7990
8077
  return "allowed-once";
7991
8078
  }
7992
8079
  deps.log?.(`command guard delegated a ${request.toolName} call: ${verdict.reason}${verdict.command === void 0 ? "" : ` — ${verdict.command}`}`);
@@ -8170,8 +8257,7 @@ const Config = z.object(markVolatileFields({
8170
8257
  account: z.string()
8171
8258
  })),
8172
8259
  commandGuard: z.boolean().default(false),
8173
- commandGuardThreshold: z.number().min(COMMAND_GUARD_MIN_THRESHOLD).max(1),
8174
- commandGuardTimeoutMs: z.number().min(200).max(COMMAND_GUARD_MAX_TIMEOUT_MS),
8260
+ commandGuardLevel: z.union([...COMMAND_GUARD_LEVELS]).default(COMMAND_GUARD_DEFAULT_LEVEL),
8175
8261
  zdr: z.boolean().default(false),
8176
8262
  lang: z.string().pattern(/^(zh|en)$/).default("zh")
8177
8263
  }, ["apiKey"]));
@@ -8402,8 +8488,8 @@ function apply(ctx, config) {
8402
8488
  const raw = current();
8403
8489
  return {
8404
8490
  enabled: raw.commandGuard === true,
8405
- threshold: raw.commandGuardThreshold ?? .9,
8406
- timeoutMs: raw.commandGuardTimeoutMs ?? COMMAND_GUARD_DEFAULT_TIMEOUT_MS
8491
+ threshold: commandGuardThreshold(raw.commandGuardLevel),
8492
+ timeoutMs: COMMAND_GUARD_TIMEOUT_MS
8407
8493
  };
8408
8494
  };
8409
8495
  ctx.inject(["approval"], (approvalCtx) => {