@mars-sea/dsh-commandcode-provider 0.11.9 → 0.11.11

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.js CHANGED
@@ -284,6 +284,27 @@ const KNOWN_EFFORTS = {
284
284
  "xhigh",
285
285
  "max"
286
286
  ],
287
+ "claude-opus-5-5": [
288
+ "low",
289
+ "medium",
290
+ "high",
291
+ "xhigh",
292
+ "max"
293
+ ],
294
+ "gpt-6-luna": [
295
+ "low",
296
+ "medium",
297
+ "high",
298
+ "xhigh",
299
+ "max"
300
+ ],
301
+ "gpt-6-sol": [
302
+ "low",
303
+ "medium",
304
+ "high",
305
+ "xhigh",
306
+ "max"
307
+ ],
287
308
  "MiniMaxAI/MiniMax-M3": [
288
309
  "low",
289
310
  "medium",
@@ -320,6 +341,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
320
341
  "claude-haiku-4-5-20251001",
321
342
  "claude-opus-4-7",
322
343
  "claude-opus-4-8",
344
+ "claude-opus-5-5",
323
345
  "claude-opus-5",
324
346
  "claude-sonnet-4-6",
325
347
  "claude-sonnet-5",
@@ -339,6 +361,8 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
339
361
  "gpt-5.6-sol",
340
362
  "gpt-5.6-terra",
341
363
  "gpt-6-astra",
364
+ "gpt-6-luna",
365
+ "gpt-6-sol",
342
366
  "meta/muse-spark-1.1",
343
367
  "meta/muse-spark-1.2",
344
368
  "meta/muse-spark-1.2-contributor",
@@ -365,6 +389,69 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
365
389
  "z-ai/glm-5.3-flashx"
366
390
  ]);
367
391
  /**
392
+ * Models WITHOUT a zero-data-retention upstream, per the official CLI's own
393
+ * registry (`command-code@1.64.0` `dist/cli.mjs`: `modelSupportsZdr(id)` is
394
+ * exactly `!nonZdrSet.has(canonicalize(id))`, and `knownModelSupportsZdr`
395
+ * carries the same membership in the sibling route table — the union is this
396
+ * set). The official docs (commandcode.ai/docs/resources/zdr) put it in prose
397
+ * — "99% of our models have ZDR-capable upstreams … only a small handful of
398
+ * models are affected" — so the CLI's exclusion list is the only per-model
399
+ * evidence there is; a ZDR request naming one of these fails with HTTP 422
400
+ * `cmd_zdr_no_providers` instead of routing through a provider that retains.
401
+ *
402
+ * Why a NEGATIVE set, and why "not listed" answers TRUE: 99% of the catalog is
403
+ * covered, so the maintained difference is the exception list. This helper is
404
+ * informational; the adapter sends the ZDR header for EVERY request when the
405
+ * switch is on. The provider remains the routing authority and refuses an
406
+ * unsupported model rather than silently dropping the privacy guarantee.
407
+ *
408
+ * `minimax/minimax-m3-free` is the one entry the public catalog
409
+ * (`/provider/v1/models`) does not serve (it is CLI/pricing-visible and hidden
410
+ * from the picker); it stays listed because the CLI carries it and a Go-plan
411
+ * request can still name it.
412
+ *
413
+ * Keep in sync via the dsh-commandcode-upstream skill: the CLI's registry data
414
+ * (its `zdr:{only:[…]}` provider routes and the per-provider `zdr`/`noTraining`
415
+ * flags) is upstream-internal routing, not a per-model contract, so this table
416
+ * is the snapshot of the exclusion set and nothing more. The 1.62.0 → 1.64.0
417
+ * diff of that union is EMPTY: both anchors were extracted from both bundles
418
+ * during the 2026-09-23 check and each carries the same 20 members — which is
419
+ * how rare a change here is expected to be. `meituan/LongCat-2.0` is the one
420
+ * member the two anchors disagree about (it is in `modelSupportsZdr`'s set in
421
+ * both releases and in neither `knownModelSupportsZdr` set), so the union is
422
+ * what this table follows; reading only the sibling route table would drop it.
423
+ */
424
+ const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
425
+ "MiniMaxAI/MiniMax-M3",
426
+ "Qwen/Qwen3.8-Max-0902",
427
+ "meituan/LongCat-2.0",
428
+ "meta/muse-spark-1.1",
429
+ "meta/muse-spark-1.2",
430
+ "meta/muse-spark-1.2-contributor",
431
+ "meta/muse-spark-1.3",
432
+ "meta/muse-spark-1.3-contributor",
433
+ "minimax/minimax-m3-free",
434
+ "poolside/laguna-s-2.1-free",
435
+ "sakana/fugu-ultra",
436
+ "stepfun/Step-3.7-Flash",
437
+ "stepfun/Step-5-Preview",
438
+ "xai/grok-4.5",
439
+ "xai/grok-4.6",
440
+ "xiaomi/mimo-v2.6-flash",
441
+ "xiaomi/mimo-v2.6-pro",
442
+ "xiaomi/mimo-v2.6-pro-ultraspeed",
443
+ "z-ai/glm-5.3-flashx",
444
+ "zai-org/GLM-5.2-Fast"
445
+ ]);
446
+ /**
447
+ * Whether the CLI snapshot lists a ZDR-capable upstream for `modelId`. This is
448
+ * informational, never a reason to omit the header when ZDR is enabled: the
449
+ * provider may add coverage or lack capacity after this snapshot was taken.
450
+ */
451
+ function supportsZeroDataRetention(modelId) {
452
+ return !KNOWN_NON_ZDR_MODELS.has(modelId);
453
+ }
454
+ /**
368
455
  * Models the official CLI's model table (command-code@1.53.0) marks
369
456
  * `reasoning:!0` but defines no selectable `reasoning_effort` levels — they
370
457
  * think automatically, with Command Code driving the depth. This is the
@@ -508,6 +595,7 @@ const KNOWN_PLANS = {
508
595
  "deepseek/deepseek-v4-flash-vision-exp": "go",
509
596
  "deepseek/deepseek-v4-pro": "go",
510
597
  "gpt-5.6-luna": "go",
598
+ "gpt-6-luna": "go",
511
599
  "meituan/LongCat-2.0": "go",
512
600
  "inclusionai/ling-3.0-flash-sante:free": "go",
513
601
  "meta/muse-spark-1.2-contributor": "go",
@@ -558,11 +646,13 @@ const KNOWN_PLANS = {
558
646
  "gpt-5.4-mini": "pro",
559
647
  "gpt-5.5": "pro",
560
648
  "gpt-5.6-terra": "pro",
649
+ "gpt-6-sol": "pro",
561
650
  "meta/muse-spark-1.1": "pro",
562
651
  "claude-fable-5-1": "provider",
563
652
  "claude-fable-5": "provider",
564
653
  "claude-opus-4-7": "provider",
565
654
  "claude-opus-4-8": "provider",
655
+ "claude-opus-5-5": "provider",
566
656
  "claude-opus-5": "provider",
567
657
  "gpt-6-astra": "provider",
568
658
  "sakana/fugu-ultra": "provider"
@@ -955,6 +1045,15 @@ const MODEL_PRICE_ROWS = [
955
1045
  6.25
956
1046
  ]
957
1047
  },
1048
+ {
1049
+ id: "claude-opus-5-5",
1050
+ rates: [
1051
+ 4,
1052
+ 20,
1053
+ .2,
1054
+ 5
1055
+ ]
1056
+ },
958
1057
  {
959
1058
  id: "claude-sonnet-4-6",
960
1059
  rates: [
@@ -1274,6 +1373,52 @@ const MODEL_PRICE_ROWS = [
1274
1373
  25
1275
1374
  ] }]
1276
1375
  },
1376
+ {
1377
+ id: "gpt-6-luna",
1378
+ rates: [
1379
+ .1,
1380
+ .5,
1381
+ .01,
1382
+ .125
1383
+ ],
1384
+ contextTiers: [{
1385
+ "maxContext": 272e3,
1386
+ "rates": [
1387
+ .1,
1388
+ .5,
1389
+ .01,
1390
+ .125
1391
+ ]
1392
+ }, { "rates": [
1393
+ .2,
1394
+ .75,
1395
+ .02,
1396
+ .25
1397
+ ] }]
1398
+ },
1399
+ {
1400
+ id: "gpt-6-sol",
1401
+ rates: [
1402
+ 2,
1403
+ 10,
1404
+ .2,
1405
+ 2.5
1406
+ ],
1407
+ contextTiers: [{
1408
+ "maxContext": 272e3,
1409
+ "rates": [
1410
+ 2,
1411
+ 10,
1412
+ .2,
1413
+ 2.5
1414
+ ]
1415
+ }, { "rates": [
1416
+ 4,
1417
+ 15,
1418
+ .4,
1419
+ 5
1420
+ ] }]
1421
+ },
1277
1422
  {
1278
1423
  id: "grok-4.5",
1279
1424
  rates: [
@@ -1856,11 +2001,11 @@ function costGroupKey(group, table) {
1856
2001
  //#endregion
1857
2002
  //#region src/cost-projection.ts
1858
2003
  const record$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
1859
- const finite = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
2004
+ const finite$1 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
1860
2005
  function tokensOf(value) {
1861
2006
  const u = record$2(value);
1862
- if (!u || !finite(u.inputTokens) || !finite(u.outputTokens)) return void 0;
1863
- if (u.cacheReadTokens !== void 0 && !finite(u.cacheReadTokens) || u.cacheWriteTokens !== void 0 && !finite(u.cacheWriteTokens)) return void 0;
2007
+ if (!u || !finite$1(u.inputTokens) || !finite$1(u.outputTokens)) return void 0;
2008
+ if (u.cacheReadTokens !== void 0 && !finite$1(u.cacheReadTokens) || u.cacheWriteTokens !== void 0 && !finite$1(u.cacheWriteTokens)) return void 0;
1864
2009
  return {
1865
2010
  uncachedInputTokens: u.inputTokens,
1866
2011
  outputTokens: u.outputTokens,
@@ -1886,13 +2031,13 @@ function usageOf(type, data) {
1886
2031
  }
1887
2032
  function parseState(value) {
1888
2033
  const s = record$2(value), facts = record$2(s?.facts);
1889
- if (!s || !facts || !finite(facts.pricingKey) || !Array.isArray(facts.groups)) throw new TypeError("invalid commandCodeCost state");
2034
+ if (!s || !facts || !finite$1(facts.pricingKey) || !Array.isArray(facts.groups)) throw new TypeError("invalid commandCodeCost state");
1890
2035
  const group = (v) => {
1891
2036
  const g = record$2(v), t = record$2(g?.tokens);
1892
- return !!g && typeof g.provider === "string" && typeof g.model === "string" && (g.at === null || finite(g.at)) && finite(g.contextTokens) && !!t && COST_TOKEN_KEYS.every((k) => finite(t[k]));
2037
+ return !!g && typeof g.provider === "string" && typeof g.model === "string" && (g.at === null || finite$1(g.at)) && finite$1(g.contextTokens) && !!t && COST_TOKEN_KEYS.every((k) => finite$1(t[k]));
1893
2038
  };
1894
2039
  const sel = record$2(s.selection), last = record$2(s.last);
1895
- if (!(s.selection === null || sel && typeof sel.provider === "string" && typeof sel.model === "string") || !(s.at === null || finite(s.at)) || !facts.groups.every(group) || !(s.last === null || last && finite(last.turn) && finite(last.step) && group(last.group))) throw new TypeError("invalid commandCodeCost state");
2040
+ if (!(s.selection === null || sel && typeof sel.provider === "string" && typeof sel.model === "string") || !(s.at === null || finite$1(s.at)) || !facts.groups.every(group) || !(s.last === null || last && finite$1(last.turn) && finite$1(last.step) && group(last.group))) throw new TypeError("invalid commandCodeCost state");
1896
2041
  return value;
1897
2042
  }
1898
2043
  /** Optional registry uses only synchronous parse/view; no new runtime module is required. */
@@ -1926,11 +2071,11 @@ function createCostProjection(table = modelPriceTable()) {
1926
2071
  }
1927
2072
  if (event.type === "step/start" || event.type === "llm/retry-started") return {
1928
2073
  ...state,
1929
- at: finite(event.time) ? event.time : null,
2074
+ at: finite$1(event.time) ? event.time : null,
1930
2075
  last: null
1931
2076
  };
1932
2077
  const tokens = usageOf(event.type, data);
1933
- if (tokens === void 0 || !finite(data.turn) || !finite(data.step)) return state;
2078
+ if (tokens === void 0 || !finite$1(data.turn) || !finite$1(data.step)) return state;
1934
2079
  const previous = state.last?.turn === data.turn && state.last.step === data.step ? state.last.group : void 0;
1935
2080
  const group = {
1936
2081
  provider: state.selection?.provider ?? "",
@@ -1990,6 +2135,14 @@ function installCostProjection(ctx) {
1990
2135
  });
1991
2136
  }
1992
2137
  //#endregion
2138
+ //#region src/response-encoding.ts
2139
+ /**
2140
+ * Ask Command Code for plain response bodies. A DSH host using an incompatible
2141
+ * undici global dispatcher can lose response headers and leave compressed
2142
+ * JSON, SSE, and pre-stream error bodies undecoded.
2143
+ */
2144
+ const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
2145
+ //#endregion
1993
2146
  //#region src/accounts.ts
1994
2147
  /**
1995
2148
  * Multi-account pool for the Command Code provider (host side).
@@ -2849,7 +3002,7 @@ function openStreamTrace(scope, options = {}) {
2849
3002
  * per-request options thunk and an API-key resolver from the plugin entry
2850
3003
  * (src/index.ts), so a settings change reaches the very next request.
2851
3004
  */
2852
- const COMMAND_CODE_CLI_VERSION = "1.62.0";
3005
+ const COMMAND_CODE_CLI_VERSION = "1.64.0";
2853
3006
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
2854
3007
  const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
2855
3008
  const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
@@ -2973,6 +3126,19 @@ function isContextOverflowDetail(detail) {
2973
3126
  return isContextWindowExceededError(detail) || CLI_CONTEXT_OVERFLOW_PATTERN.test(detail);
2974
3127
  }
2975
3128
  /**
3129
+ * Whether one rejection says "ZDR requested, no ZDR-capable upstream": the
3130
+ * provider spells the code `cmd_zdr_no_providers` in JSON error bodies and
3131
+ * `CMD_ZDR_NO_PROVIDERS` in plain-text ones (the official CLI's own
3132
+ * classifier checks both spellings, and its error text tells the user to
3133
+ * unset `CMD_ZDR`). Underscore-separated matching normalizes the separator, so
3134
+ * one comparison covers the variants — the same normalization the pre-stream
3135
+ * rejection classifier and `hasTerminalStreamMarker` already use.
3136
+ */
3137
+ const ZDR_NO_PROVIDERS_PATTERN = /cmd[_\s-]?zdr[_\s-]?no[_\s-]?providers/i;
3138
+ function isZdrNoProviders(providerCode, providerDetail, errText) {
3139
+ return ZDR_NO_PROVIDERS_PATTERN.test(providerCode ?? "") || ZDR_NO_PROVIDERS_PATTERN.test(providerDetail) || ZDR_NO_PROVIDERS_PATTERN.test(errText);
3140
+ }
3141
+ /**
2976
3142
  * Classify one in-band stream `error` payload into the failure the caller
2977
3143
  * throws. Shared by both transports — the CLI transport delivers it as an
2978
3144
  * `error` event (`{ type: 'error', error }`), the Provider API transport as a
@@ -3835,17 +4001,12 @@ function parseAccountIdentity(whoami) {
3835
4001
  orgId: orgData === void 0 ? void 0 : stringValue(orgData.id)
3836
4002
  };
3837
4003
  }
3838
- /**
3839
- * Classify a TOTAL failure: when every endpoint failed with one class of
3840
- * error, the degraded per-endpoint view would hide the root cause behind
3841
- * a generic "partial data" note — name it instead.
3842
- */
3843
- function classifyTotalFailure(failures, failedStatuses) {
4004
+ function classifyTotalFailure(failures, failed) {
3844
4005
  if (failures.length !== USAGE_ENDPOINT_COUNT) return void 0;
3845
- const codes = failedStatuses.filter((status) => status !== void 0);
3846
- if (codes.length === USAGE_ENDPOINT_COUNT && codes.every((code) => code === 401)) return "invalid-key";
3847
- if (codes.length === USAGE_ENDPOINT_COUNT && codes.every((code) => code >= 500)) return "service-unavailable";
3848
- if (codes.length === 0) return "network";
4006
+ if (failed.every((item) => item.kind === "http" && item.status === 401)) return "invalid-key";
4007
+ if (failed.every((item) => item.kind === "http" && item.status >= 500)) return "service-unavailable";
4008
+ if (failed.every((item) => item.kind === "invalid-response")) return "invalid-response";
4009
+ if (failed.every((item) => item.kind === "transport")) return "network";
3849
4010
  }
3850
4011
  /** Build the legacy CLI (`/alpha/generate`) request body for one call. */
3851
4012
  async function buildCliBody(options, connection, facts) {
@@ -3936,19 +4097,24 @@ async function connectGenerate(deps, key, protocol, body) {
3936
4097
  if (options.signal) options.signal.removeEventListener("abort", onCallerAbort);
3937
4098
  };
3938
4099
  let response;
4100
+ const zdr = connection.zdr === true;
3939
4101
  const headers = protocol === "cli" ? {
3940
4102
  "Content-Type": "application/json",
4103
+ ...IDENTITY_ENCODING_HEADER,
3941
4104
  Authorization: `Bearer ${key}`,
3942
4105
  "x-command-code-version": COMMAND_CODE_CLI_VERSION,
3943
4106
  "x-cli-environment": "production",
3944
4107
  "x-project-slug": projectSlugFromPath(connection.workingDir),
3945
4108
  "x-taste-learning": "true",
3946
4109
  "x-co-flag": "false",
4110
+ ...zdr ? { "x-cmd-zdr": "1" } : {},
3947
4111
  ...attributionHeaders()
3948
4112
  } : {
3949
4113
  "Content-Type": "application/json",
4114
+ ...IDENTITY_ENCODING_HEADER,
3950
4115
  Authorization: `Bearer ${key}`,
3951
4116
  Accept: "text/event-stream",
4117
+ ...zdr ? { "x-cmd-zdr": "1" } : {},
3952
4118
  ...attributionHeaders()
3953
4119
  };
3954
4120
  try {
@@ -4153,6 +4319,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4153
4319
  const response = await this.fetchImpl(`${apiBase}/provider/v1/models`, {
4154
4320
  headers: {
4155
4321
  accept: "application/json",
4322
+ ...IDENTITY_ENCODING_HEADER,
4156
4323
  ...attributionHeaders()
4157
4324
  },
4158
4325
  signal: signal ?? AbortSignal.timeout(1e4)
@@ -4216,6 +4383,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4216
4383
  const named = connection.apiKeyEnv;
4217
4384
  return {
4218
4385
  Authorization: `Bearer ${assertUsableApiKey(raw, "llm-commandcode", typeof named === "string" && named !== "" ? named : "the stored credential")}`,
4386
+ ...IDENTITY_ENCODING_HEADER,
4219
4387
  "x-command-code-version": COMMAND_CODE_CLI_VERSION,
4220
4388
  "x-cli-environment": "production",
4221
4389
  ...attributionHeaders()
@@ -4225,8 +4393,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
4225
4393
  * Fetch one account endpoint and parse its JSON body. Returns the HTTP
4226
4394
  * status alongside the parsed record so each caller applies its own
4227
4395
  * failure accounting: the billing probe fails open silently, the usage
4228
- * report books failures per endpoint. Non-2xx and non-record bodies come
4229
- * back without a record; only a transport throw propagates to the caller.
4396
+ * report books failures per endpoint. Non-2xx and invalid JSON bodies come
4397
+ * back without a record; only a fetch failure propagates to the caller.
4230
4398
  *
4231
4399
  * `timeoutMs` is the caller's budget for this one request. The default is the
4232
4400
  * CATALOG probe's short cap, which fits the picker's fail-open reads; the
@@ -4239,10 +4407,21 @@ var CommandCodeAdapter = class extends LlmAdapter {
4239
4407
  signal: AbortSignal.timeout(timeoutMs)
4240
4408
  });
4241
4409
  if (!response.ok) return { status: response.status };
4242
- const parsed = await response.json();
4243
- return {
4410
+ let parsed;
4411
+ try {
4412
+ parsed = await response.json();
4413
+ } catch {
4414
+ return {
4415
+ status: response.status,
4416
+ invalidResponse: true
4417
+ };
4418
+ }
4419
+ return isRecord(parsed) ? {
4244
4420
  status: response.status,
4245
- ...isRecord(parsed) ? { record: parsed } : {}
4421
+ record: parsed
4422
+ } : {
4423
+ status: response.status,
4424
+ invalidResponse: true
4246
4425
  };
4247
4426
  }
4248
4427
  /**
@@ -4423,19 +4602,25 @@ var CommandCodeAdapter = class extends LlmAdapter {
4423
4602
  throw error;
4424
4603
  }
4425
4604
  const failures = [];
4426
- const failedStatuses = [];
4605
+ const failed = [];
4427
4606
  const getJson = async (path) => {
4428
4607
  try {
4429
- const { status, record } = await this.fetchEndpointJson(`${base}${path}`, headers, connection.requestTimeoutMs);
4608
+ const { status, record, invalidResponse } = await this.fetchEndpointJson(`${base}${path}`, headers, connection.requestTimeoutMs);
4430
4609
  if (record === void 0) {
4431
- failures.push(`${path}: HTTP ${status}`);
4432
- failedStatuses.push(status);
4610
+ failures.push(`${path}: HTTP ${status}${invalidResponse ? " returned an unreadable or invalid JSON body" : ""}`);
4611
+ failed.push(invalidResponse ? {
4612
+ kind: "invalid-response",
4613
+ status
4614
+ } : {
4615
+ kind: "http",
4616
+ status
4617
+ });
4433
4618
  return;
4434
4619
  }
4435
4620
  return record;
4436
4621
  } catch (error) {
4437
4622
  failures.push(`${path}: ${error instanceof Error ? error.message : String(error)}`);
4438
- failedStatuses.push(void 0);
4623
+ failed.push({ kind: "transport" });
4439
4624
  return;
4440
4625
  }
4441
4626
  };
@@ -4464,7 +4649,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4464
4649
  currentPeriodEnd: periodEndValue(subData?.currentPeriodEnd)
4465
4650
  };
4466
4651
  }
4467
- const blocked = classifyTotalFailure(failures, failedStatuses);
4652
+ const blocked = classifyTotalFailure(failures, failed);
4468
4653
  if (blocked !== void 0) report.blocked = blocked;
4469
4654
  return report;
4470
4655
  }
@@ -4804,6 +4989,7 @@ function generateHttpError(status, errText, retryAfterMs) {
4804
4989
  const detail = providerCode ?? `HTTP ${status}`;
4805
4990
  const overflowDetail = providerDetail !== "" ? providerDetail : errText.slice(0, 500);
4806
4991
  if (status < 500 && isContextOverflowDetail(overflowDetail)) return new LlmError(`Command Code API error ${status}: the request exceeds the model's context window — this session is being compacted and the request retried;Command Code API 返回 ${status}:请求内容超出模型上下文窗口——正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, CONTEXT_WINDOW_EXCEEDED_CODE, { status });
4992
+ if (status === 422 && isZdrNoProviders(providerCode, providerDetail, errText)) return new LlmError(`Command Code API error 422 (${detail}): zero data retention is enforced but no ZDR-capable upstream is available for this model — turn the ZDR setting off (or use another model);Command Code API 返回 422:已开启零数据保留(ZDR),但该模型当前没有可用的 ZDR 上游——请关闭 ZDR 开关,或改用其他模型`, "PROVIDER_HTTP_ERROR", { status: 422 });
4807
4993
  if (status === 401) return new LlmError(`Command Code API error 401 (${detail}): the API key is missing or invalid — check the key stored for COMMANDCODE_API_KEY (Models page) or the auth file;Command Code API 返回 401:API 密钥缺失或无效——请在设置页检查 COMMANDCODE_API_KEY 存储的密钥,或检查 auth 文件`, "INVALID_CREDENTIAL", { status: 401 });
4808
4994
  if (status === 413) return new LlmError("Command Code API error 413: the request body exceeds the provider's size limit (the cap is undocumented, measured at about 50 MB) — the session history is too large to send, usually because of accumulated image attachments. Start a new session, or drop the image-heavy part of this one, and retry;Command Code API 返回 413:请求体超过服务端的体积上限(官方未公开,实测约 50 MB)——会话历史过大,通常是历史图片累积所致。请新建会话,或移除本会话中图片较多的部分后重试", "PROVIDER_HTTP_ERROR", { status: 413 });
4809
4995
  return new LlmError(`Command Code API error ${status}${detail === `HTTP ${status}` ? "" : ` (${detail})`}: ${errText.slice(0, 500)}`, httpErrorCode(status), {
@@ -5276,6 +5462,7 @@ const commandcodeCommand = {
5276
5462
  unconfigured: " (未配置 API 密钥)",
5277
5463
  blockedInvalidKey: "⛔ API 密钥无效或已过期 — 服务端拒绝了全部请求(401),请检查该账户的密钥配置",
5278
5464
  blockedServiceUnavailable: "⚠️ Command Code 服务暂时不可用(5xx),稍后重试",
5465
+ blockedInvalidResponse: "⚠️ Command Code 返回了无法读取的响应 — 请检查宿主的 HTTP 代理或响应解压配置",
5279
5466
  blockedNetwork: "⚠️ 无法连接 Command Code 服务 — 请检查网络或 API 地址",
5280
5467
  planLine: " 📦 套餐 {name}{status}{period}",
5281
5468
  planPeriodSuffix: " · 账期截止 {date}",
@@ -5307,6 +5494,7 @@ const commandcodeCommand = {
5307
5494
  unconfigured: " (no API key configured)",
5308
5495
  blockedInvalidKey: "⛔ API key invalid or expired — the server rejected every request (401); check the key configured for this account",
5309
5496
  blockedServiceUnavailable: "⚠️ Command Code service temporarily unavailable (5xx); try again later",
5497
+ blockedInvalidResponse: "⚠️ Command Code returned an unreadable response — check the host HTTP proxy or response decoding",
5310
5498
  blockedNetwork: "⚠️ could not reach the Command Code service — check your network or the API base setting",
5311
5499
  planLine: " 📦 Plan {name}{status}{period}",
5312
5500
  planPeriodSuffix: " · period ends {date}",
@@ -5405,6 +5593,7 @@ function renderReport(report, locale, title) {
5405
5593
  lines.push(title ?? commandCopy(locale, "title").replace("{account}", account), "");
5406
5594
  if (report.blocked === "invalid-key") lines.push(commandCopy(locale, "blockedInvalidKey"), "");
5407
5595
  else if (report.blocked === "service-unavailable") lines.push(commandCopy(locale, "blockedServiceUnavailable"), "");
5596
+ else if (report.blocked === "invalid-response") lines.push(commandCopy(locale, "blockedInvalidResponse"), "");
5408
5597
  else if (report.blocked === "network") lines.push(commandCopy(locale, "blockedNetwork"), "");
5409
5598
  if (report.plan && report.plan.name !== "") {
5410
5599
  const p = report.plan;
@@ -5506,6 +5695,15 @@ function makeBoundaryValidator(prefix) {
5506
5695
  booleanField
5507
5696
  };
5508
5697
  }
5698
+ /** Use the same strict-codec shape for invocation parameters and results. */
5699
+ function makeStrictCodec(typeSymbol, schema) {
5700
+ return {
5701
+ mode: "strict",
5702
+ typeSymbol,
5703
+ schema,
5704
+ create: () => schema
5705
+ };
5706
+ }
5509
5707
  /**
5510
5708
  * Build one strict invocation descriptor. Every Command Code Remote shares the
5511
5709
  * `commandcode` namespace, the `commandcodeUsage` service, and a strict
@@ -5514,12 +5712,7 @@ function makeBoundaryValidator(prefix) {
5514
5712
  * only its own facts.
5515
5713
  */
5516
5714
  function makeRemoteDescriptor(endpoint, method, typeSymbol, schema) {
5517
- const result = {
5518
- mode: "strict",
5519
- typeSymbol,
5520
- schema,
5521
- create: () => schema
5522
- };
5715
+ const result = makeStrictCodec(typeSymbol, schema);
5523
5716
  return {
5524
5717
  id: `${REMOTE_PACKAGE}#${endpoint}`,
5525
5718
  service: REMOTE_SERVICE,
@@ -5572,7 +5765,7 @@ function parseUsageReport(value) {
5572
5765
  const report = { failures };
5573
5766
  if (source.blocked !== void 0) {
5574
5767
  const blocked = source.blocked;
5575
- if (blocked === "invalid-key" || blocked === "service-unavailable" || blocked === "network") report.blocked = blocked;
5768
+ if (blocked === "invalid-key" || blocked === "service-unavailable" || blocked === "invalid-response" || blocked === "network") report.blocked = blocked;
5576
5769
  else reject$1("blocked");
5577
5770
  }
5578
5771
  if (source.account !== void 0) {
@@ -5801,9 +5994,24 @@ function parseLoginStatus(value) {
5801
5994
  }
5802
5995
  /** The strict result codec shared by all three login endpoints. */
5803
5996
  const loginStatusSchema = { parse: parseLoginStatus };
5804
- /** Build one login invocation descriptor (uniform result, no parameters). */
5997
+ /** Optional account target; the Host still checks it against saved slots. */
5998
+ const loginTargetSchema = { parse(value) {
5999
+ if (value === void 0 || typeof value === "string") return value;
6000
+ return reject("targetRef");
6001
+ } };
6002
+ /** Build one login invocation descriptor. Only begin accepts an account ref. */
5805
6003
  function loginDescriptor(endpoint, method) {
5806
- return makeRemoteDescriptor(endpoint, method, `${REMOTE_PACKAGE}#CommandCodeLoginStatus`, loginStatusSchema);
6004
+ const descriptor = makeRemoteDescriptor(endpoint, method, `${REMOTE_PACKAGE}#CommandCodeLoginStatus`, loginStatusSchema);
6005
+ return method === "loginBegin" ? {
6006
+ ...descriptor,
6007
+ parameters: [{
6008
+ name: "targetRef",
6009
+ wire: "targetRef",
6010
+ source: "json",
6011
+ codec: makeStrictCodec(`${REMOTE_PACKAGE}#CommandCodeLoginTargetRef`, loginTargetSchema),
6012
+ acceptsUndefined: true
6013
+ }]
6014
+ } : descriptor;
5807
6015
  }
5808
6016
  /** The three login descriptors, shared verbatim by Host registration and Client mount. */
5809
6017
  const LOGIN_DESCRIPTORS = [
@@ -5871,8 +6079,8 @@ var CommandCodeUsageService = class extends TypertRemoteService {
5871
6079
  * (no free loopback port, disposed plugin); the Gateway folds the throw
5872
6080
  * into the failure branch the page renders.
5873
6081
  */
5874
- async loginBegin() {
5875
- return this.requireLogin().begin();
6082
+ async loginBegin(targetRef) {
6083
+ return this.requireLogin().begin(targetRef);
5876
6084
  }
5877
6085
  /** Poll a login attempt's status. */
5878
6086
  async loginStatus() {
@@ -5959,6 +6167,12 @@ const LOGIN_ALLOWED_ORIGINS = [
5959
6167
  ];
5960
6168
  /** The Studio route that performs the browser-side login. */
5961
6169
  const STUDIO_AUTH_PATH = "/studio/auth/cli";
6170
+ /** Resolve a browser-login destination without allowing arbitrary credential writes. */
6171
+ function loginCredentialRef(targetRef, defaultRef, accounts) {
6172
+ if (targetRef === void 0) return defaultRef;
6173
+ if (targetRef === defaultRef || !accounts.some((account) => account.apiKeyEnv === targetRef)) throw new Error("the target account must be saved before browser sign-in");
6174
+ return targetRef;
6175
+ }
5962
6176
  /** Compose the Studio authorization URL (pure, exported for tests). */
5963
6177
  function buildCommandAuthUrl(options) {
5964
6178
  const callback = `http://localhost:${options.port}/callback`;
@@ -5981,6 +6195,7 @@ async function validateCommandApiKey(fetchImpl, apiBase, apiKey) {
5981
6195
  method: "GET",
5982
6196
  headers: {
5983
6197
  "Content-Type": "application/json",
6198
+ ...IDENTITY_ENCODING_HEADER,
5984
6199
  Authorization: `Bearer ${apiKey}`
5985
6200
  }
5986
6201
  });
@@ -6048,6 +6263,8 @@ var CommandCodeLoginFlow = class {
6048
6263
  * the port window so browser login dies until the Host restarts.
6049
6264
  */
6050
6265
  starting;
6266
+ /** A live attempt's destination; different rows may not rejoin it. */
6267
+ targetRef;
6051
6268
  /** A `cancel()` that arrived while a start was still binding (see {@link CommandCodeLoginFlow.cancel}). */
6052
6269
  cancelPending = false;
6053
6270
  disposed = false;
@@ -6068,12 +6285,20 @@ var CommandCodeLoginFlow = class {
6068
6285
  * `waiting` carrying the Studio URL once the loopback server is up.
6069
6286
  * Rejects only when the flow cannot start at all (no free port, disposed).
6070
6287
  */
6071
- async begin() {
6288
+ async begin(targetRef) {
6072
6289
  if (this.disposed) throw new Error("login flow has been disposed");
6073
- if (this.statusValue.state === "waiting" && this.server !== void 0) return this.statusValue;
6074
- if (this.starting !== void 0) return this.starting;
6290
+ this.deps.validateTargetRef?.(targetRef);
6291
+ if (this.statusValue.state === "waiting" && this.server !== void 0) {
6292
+ if (this.targetRef !== targetRef) throw new Error("another account login is already in progress");
6293
+ return this.statusValue;
6294
+ }
6295
+ if (this.starting !== void 0) {
6296
+ if (this.targetRef !== targetRef) throw new Error("another account login is already in progress");
6297
+ return this.starting;
6298
+ }
6075
6299
  this.cancelPending = false;
6076
- const starting = this.startAttempt();
6300
+ this.targetRef = targetRef;
6301
+ const starting = this.startAttempt(targetRef);
6077
6302
  this.starting = starting;
6078
6303
  try {
6079
6304
  return await starting;
@@ -6086,7 +6311,7 @@ var CommandCodeLoginFlow = class {
6086
6311
  * server, one published `waiting` status. Callers reach it only through
6087
6312
  * `begin()`'s single-flight fence.
6088
6313
  */
6089
- async startAttempt() {
6314
+ async startAttempt(targetRef) {
6090
6315
  this.teardown();
6091
6316
  const attempt = ++this.attemptSeq;
6092
6317
  const port = await this.findPort();
@@ -6126,7 +6351,7 @@ var CommandCodeLoginFlow = class {
6126
6351
  });
6127
6352
  }, this.deps.timeoutMs ?? 12e4);
6128
6353
  this.timer.unref?.();
6129
- settled.then((credentials) => this.complete(attempt, credentials), (failure) => this.failFrom(attempt, failure));
6354
+ settled.then((credentials) => this.complete(attempt, credentials, targetRef), (failure) => this.failFrom(attempt, failure));
6130
6355
  return this.statusValue;
6131
6356
  }
6132
6357
  /**
@@ -6305,7 +6530,7 @@ var CommandCodeLoginFlow = class {
6305
6530
  * would report "cancelled" while the credential landed anyway, and the page
6306
6531
  * would silently flip to success.
6307
6532
  */
6308
- async complete(attempt, credentials) {
6533
+ async complete(attempt, credentials, targetRef) {
6309
6534
  if (!this.ownsAttempt(attempt)) return;
6310
6535
  const validation = await validateCommandApiKey(this.deps.fetchImpl ?? fetch, this.readApiBase(), credentials.apiKey);
6311
6536
  if (!this.ownsAttempt(attempt)) return;
@@ -6320,7 +6545,8 @@ var CommandCodeLoginFlow = class {
6320
6545
  }
6321
6546
  if (!this.ownsAttempt(attempt)) return;
6322
6547
  try {
6323
- await this.deps.storeKey(credentials);
6548
+ this.deps.validateTargetRef?.(targetRef);
6549
+ await this.deps.storeKey(credentials, targetRef);
6324
6550
  } catch (error) {
6325
6551
  if (!this.ownsAttempt(attempt)) return;
6326
6552
  this.setStatus({
@@ -6525,7 +6751,7 @@ function toSource(result) {
6525
6751
  ...snippet !== void 0 && snippet.length > 0 ? { snippet } : {}
6526
6752
  };
6527
6753
  }
6528
- function isAbortError(error) {
6754
+ function isAbortError$1(error) {
6529
6755
  return error instanceof DOMException && error.name === "AbortError";
6530
6756
  }
6531
6757
  /** Build the provider's stable cancellation error while retaining the caller's reason. */
@@ -6570,6 +6796,7 @@ var CommandCodeSearchProvider = class {
6570
6796
  method: "POST",
6571
6797
  headers: {
6572
6798
  "Content-Type": "application/json",
6799
+ ...IDENTITY_ENCODING_HEADER,
6573
6800
  Authorization: `Bearer ${key}`,
6574
6801
  "x-command-code-version": COMMAND_CODE_CLI_VERSION,
6575
6802
  "x-cli-environment": "production",
@@ -6579,7 +6806,7 @@ var CommandCodeSearchProvider = class {
6579
6806
  ...signal !== void 0 ? { signal } : {}
6580
6807
  });
6581
6808
  } catch (error) {
6582
- if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
6809
+ if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
6583
6810
  throw new WebError(`Command Code web search request failed: ${error instanceof Error ? error.message : String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
6584
6811
  }
6585
6812
  if (!response.ok) {
@@ -6594,7 +6821,7 @@ var CommandCodeSearchProvider = class {
6594
6821
  if (typeof code === "string" || typeof inner === "string") message += `: ${typeof code === "string" ? code : ""}${typeof code === "string" && typeof inner === "string" ? " — " : ""}${typeof inner === "string" ? inner : ""}`;
6595
6822
  }
6596
6823
  } catch (error) {
6597
- if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
6824
+ if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
6598
6825
  const slice = (await response.text().catch(() => "")).trim().slice(0, 200);
6599
6826
  if (slice !== "") message += `: ${slice}`;
6600
6827
  }
@@ -6604,7 +6831,7 @@ var CommandCodeSearchProvider = class {
6604
6831
  try {
6605
6832
  payload = await response.json();
6606
6833
  } catch (error) {
6607
- if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
6834
+ if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
6608
6835
  throw new WebError("Command Code web search returned an unparseable response body", "WEB_PROVIDER_ERROR", { cause: error });
6609
6836
  }
6610
6837
  const results = payload?.results;
@@ -6628,7 +6855,7 @@ var CommandCodeSearchProvider = class {
6628
6855
  try {
6629
6856
  key = await this.deps.resolveKey();
6630
6857
  } catch (error) {
6631
- if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
6858
+ if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
6632
6859
  if (error instanceof Error && typeof error.code === "string") {
6633
6860
  const code = error.code;
6634
6861
  if (code === "MISSING_CREDENTIAL") throw new WebError(error.message, "WEB_PROVIDER_CREDENTIAL_MISSING", { cause: error });
@@ -6816,6 +7043,70 @@ function buildCommandCodeTuiSection(deps) {
6816
7043
  };
6817
7044
  }
6818
7045
  },
7046
+ {
7047
+ path: ["commandGuard"],
7048
+ group: "advanced",
7049
+ kind: "boolean",
7050
+ label: "AI command guard",
7051
+ descriptions: { zh: "AI 命令安全预判" },
7052
+ hint: "typesafe/jev judges whether a shell command is safe and auto-approves it; everything else asks as usual. Off by default.",
7053
+ hintDescriptions: { zh: "由 typesafe/jev 判断 shell 命令是否安全,安全则自动放行,其余照常弹窗。默认关闭。" },
7054
+ format: (value) => value === true ? "true" : "false",
7055
+ parse: (text) => ({
7056
+ kind: "set",
7057
+ value: text.trim() === "true"
7058
+ })
7059
+ },
7060
+ {
7061
+ path: ["commandGuardThreshold"],
7062
+ group: "advanced",
7063
+ kind: "number",
7064
+ label: "Auto-approve threshold (0.5-1)",
7065
+ descriptions: { zh: "自动放行阈值(0.5-1)" },
7066
+ hint: "Approve when typesafe/jev rates the command \"safe\" at least this likely; default 0.9.",
7067
+ hintDescriptions: { zh: "typesafe/jev 判定\"安全\"的概率达到该值即放行,默认 0.9。" },
7068
+ parse: (text) => {
7069
+ const trimmed = text.trim();
7070
+ if (trimmed === "") return { kind: "clear" };
7071
+ const parsed = Number(trimmed);
7072
+ return Number.isFinite(parsed) && parsed >= .5 && parsed <= 1 ? {
7073
+ kind: "set",
7074
+ value: parsed
7075
+ } : void 0;
7076
+ }
7077
+ },
7078
+ {
7079
+ path: ["commandGuardTimeoutMs"],
7080
+ group: "advanced",
7081
+ kind: "number",
7082
+ label: "Judgement timeout (ms, 200-10000)",
7083
+ descriptions: { zh: "判定超时(毫秒,200-10000)" },
7084
+ hint: "If typesafe/jev does not answer in time, the normal prompt appears; default 1500.",
7085
+ hintDescriptions: { zh: "typesafe/jev 超过该时间未返回就照常弹窗,默认 1500。" },
7086
+ parse: (text) => {
7087
+ const trimmed = text.trim();
7088
+ if (trimmed === "") return { kind: "clear" };
7089
+ const parsed = Number(trimmed);
7090
+ return Number.isFinite(parsed) && parsed >= 200 && parsed <= 1e4 ? {
7091
+ kind: "set",
7092
+ value: parsed
7093
+ } : void 0;
7094
+ }
7095
+ },
7096
+ {
7097
+ path: ["zdr"],
7098
+ group: "advanced",
7099
+ kind: "boolean",
7100
+ label: "Zero data retention (ZDR)",
7101
+ descriptions: { zh: "零数据保留(ZDR)" },
7102
+ hint: "Route requests only through upstreams that retain nothing and never train on prompts (the CLI's CMD_ZDR=1). Models without an available ZDR upstream fail instead of losing protection; ZDR is usually billed at higher pass-through rates. Off by default.",
7103
+ hintDescriptions: { zh: "请求只经由不留存数据、也不用于训练的上游(等价于 CLI 的 CMD_ZDR=1)。没有可用 ZDR 上游的模型会报错,不会降级为非 ZDR 请求;ZDR 通常按更高的透传价计费。默认关闭。" },
7104
+ format: (value) => value === true ? "true" : "false",
7105
+ parse: (text) => ({
7106
+ kind: "set",
7107
+ value: text.trim() === "true"
7108
+ })
7109
+ },
6819
7110
  {
6820
7111
  path: ["filterModelsByPlan"],
6821
7112
  group: "models",
@@ -7075,6 +7366,737 @@ function transportBudgetMessage(failureMessage, maxRetries) {
7075
7366
  return `${failureMessage};Command Code API 连接在连续重试后仍失败——已停止重试以免整轮卡死。插件已自动重试 ${maxRetries} 次。通常说明当前网络到 api.commandcode.ai 的连接不通,常见原因是代理未生效:DSH 只读取环境变量 HTTPS_PROXY/HTTP_PROXY(系统代理/PAC 不会被读取),请确认启动 DSH 的终端里已导出代理,或先关闭代理直连后重试。网络恢复后直接重发即可 — Command Code API transport still failed after ${maxRetries} automatic retries, which were stopped so the turn could not stall for minutes. This is a connectivity problem between this machine and api.commandcode.ai, not a context-window or request-size rejection. Common cause: the proxy is not reaching the harness — DSH reads HTTPS_PROXY/HTTP_PROXY from the environment only (a Windows system proxy/PAC setting is not consulted), so export it in the launching shell, or turn the proxy off and retry. Resend when the network recovers; the budget is per request and has been reset`;
7076
7367
  }
7077
7368
  //#endregion
7369
+ //#region src/systemone.ts
7370
+ /**
7371
+ * Host-side client for Command Code's System One decision endpoint
7372
+ * (`POST {apiBase}/provider/v1/systemone`).
7373
+ *
7374
+ * `typesafe/jev` is **not a chat model**: it takes a `state` plus typed
7375
+ * questions and answers with probabilities, never text, and it never streams.
7376
+ * It is therefore absent from `/provider/v1/models` (so the picker can never
7377
+ * offer it) and must never be registered as a model route — this plugin uses it
7378
+ * as an internal advisor instead: the command guard in `./command-guard.ts`
7379
+ * asks it whether a shell command is safe to auto-approve.
7380
+ *
7381
+ * The three question types are the whole vocabulary (command-code@1.64.0
7382
+ * `systemOneRequestSchema`):
7383
+ *
7384
+ * - `noul` — "is this true?", optional `criteria: { true, false }` describing
7385
+ * each verdict. Answers with `noul`, the probability of *yes*.
7386
+ * - `choice` — "which one?", `criteria` maps each option key to its
7387
+ * description. Answers with `choice`, plus optional `confidence` and
7388
+ * `probabilities`.
7389
+ * - `score` — "where on this rubric?", `criteria` is an array of at least two
7390
+ * levels, lowest first. Answers with `score`, `legend` and `probabilities`.
7391
+ *
7392
+ * A request carries at most 20 questions and every answer is keyed by the
7393
+ * question id it belongs to. `usage` is reported per request, in tokens.
7394
+ *
7395
+ * Billing: free through 2026-09-24T23:59:59Z, then $0.042 per 1M input tokens
7396
+ * with output free. The model has **no ZDR-capable upstream**, so a request
7397
+ * carrying `x-cmd-zdr: 1` is refused with HTTP 422 `cmd_zdr_no_providers` —
7398
+ * this client never sends that header, and it must stay that way even though
7399
+ * the plugin now ships a ZDR switch for chat (`Config.zdr` → the `x-cmd-zdr: 1`
7400
+ * header per model, gated on `KNOWN_NON_ZDR_MODELS`): the decision endpoint is
7401
+ * reached through THIS module's own request path, never through the adapter's
7402
+ * chat connection, so the two cannot drift — and a deployment that later widens
7403
+ * ZDR must exclude this endpoint rather than route it through the switch.
7404
+ *
7405
+ * Auth is the plugin's ordinary chain (the multi-account pool, the credentials
7406
+ * seam, the CLI auth file) through the same `Authorization: Bearer <key>` and
7407
+ * `x-command-code-version` headers every other endpoint uses.
7408
+ *
7409
+ * @module dsh-commandcode-provider/systemone
7410
+ */
7411
+ /** Path of the decision endpoint, relative to `apiBase`. */
7412
+ const SYSTEMONE_ROUTE = "/provider/v1/systemone";
7413
+ /** Milliseconds a decision may take before the caller gives up on it. */
7414
+ const SYSTEMONE_DEFAULT_TIMEOUT_MS = 1500;
7415
+ /** The one error type this client throws. Callers fail closed on it. */
7416
+ var SystemOneError = class extends Error {
7417
+ kind;
7418
+ status;
7419
+ constructor(message, kind, status, options) {
7420
+ super(message, options);
7421
+ this.kind = kind;
7422
+ this.status = status;
7423
+ this.name = "SystemOneError";
7424
+ }
7425
+ };
7426
+ /**
7427
+ * Combine the caller's signal with a timeout, keeping the two distinguishable:
7428
+ * a timeout is this client's own budget expiring (the guard delegates to the
7429
+ * user) while a caller abort means the approval was already withdrawn (answer
7430
+ * nothing) — both fail closed, but the diagnosis must not lie about which.
7431
+ *
7432
+ * The timer is a plain `setTimeout` rather than `AbortSignal.timeout()`, whose
7433
+ * timer Node unrefs: with nothing else pending (a test double, a socket that
7434
+ * never opened) an unref'd timer lets the process exit while the promise is
7435
+ * still pending, which reads as a hang rather than as the timeout it is.
7436
+ */
7437
+ function deadlineOf(signal, timeoutMs) {
7438
+ const controller = new AbortController();
7439
+ let timedOut = false;
7440
+ const timer = setTimeout(() => {
7441
+ timedOut = true;
7442
+ controller.abort();
7443
+ }, timeoutMs);
7444
+ const onCallerAbort = () => controller.abort();
7445
+ if (signal !== void 0) {
7446
+ if (signal.aborted) controller.abort();
7447
+ else signal.addEventListener("abort", onCallerAbort, { once: true });
7448
+ }
7449
+ return {
7450
+ signal: controller.signal,
7451
+ callerAborted: () => signal?.aborted === true,
7452
+ timedOut: () => timedOut,
7453
+ dispose: () => {
7454
+ clearTimeout(timer);
7455
+ signal?.removeEventListener("abort", onCallerAbort);
7456
+ }
7457
+ };
7458
+ }
7459
+ /** Whether a caught value is an abort-shaped failure. */
7460
+ function isAbortError(error) {
7461
+ return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
7462
+ }
7463
+ /** Stop waiting for an operation that does not accept an AbortSignal. */
7464
+ function withinDeadline(operation, deadline, timeoutMs) {
7465
+ return new Promise((resolve, reject) => {
7466
+ const onAbort = () => {
7467
+ reject(deadline.callerAborted() ? new SystemOneError("decision request aborted", "aborted") : new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout"));
7468
+ };
7469
+ deadline.signal.addEventListener("abort", onAbort, { once: true });
7470
+ operation.then((value) => {
7471
+ deadline.signal.removeEventListener("abort", onAbort);
7472
+ resolve(value);
7473
+ }, (error) => {
7474
+ deadline.signal.removeEventListener("abort", onAbort);
7475
+ reject(error);
7476
+ });
7477
+ if (deadline.signal.aborted) onAbort();
7478
+ });
7479
+ }
7480
+ /** A finite number in `[0, 1]`, or undefined. */
7481
+ function probability(value) {
7482
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : void 0;
7483
+ }
7484
+ /** A finite number, or undefined. */
7485
+ function finite(value) {
7486
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
7487
+ }
7488
+ /** A record of finite numbers (probabilities), or undefined when the shape is off. */
7489
+ function numberRecord(value) {
7490
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
7491
+ const out = {};
7492
+ for (const [key, entry] of Object.entries(value)) {
7493
+ const parsed = finite(entry);
7494
+ if (parsed === void 0) return void 0;
7495
+ out[key] = parsed;
7496
+ }
7497
+ return out;
7498
+ }
7499
+ /**
7500
+ * Parse one answer entry strictly by its own `type`. A body whose answer does
7501
+ * not carry the member its type promises is rejected rather than defaulted:
7502
+ * an absent probability is exactly the case the caller must not read as "safe".
7503
+ *
7504
+ * @param value - one raw entry from the response's `answers` object.
7505
+ * @returns the parsed answer, or undefined when the entry is malformed.
7506
+ */
7507
+ function parseSystemOneAnswer(value) {
7508
+ if (typeof value !== "object" || value === null) return void 0;
7509
+ const raw = value;
7510
+ if (raw.type === "noul") {
7511
+ const noul = probability(raw.noul);
7512
+ return noul === void 0 ? void 0 : {
7513
+ type: "noul",
7514
+ noul
7515
+ };
7516
+ }
7517
+ if (raw.type === "choice") {
7518
+ if (typeof raw.choice !== "string" || raw.choice === "") return void 0;
7519
+ const confidence = raw.confidence === void 0 ? void 0 : probability(raw.confidence);
7520
+ if (raw.confidence !== void 0 && confidence === void 0) return void 0;
7521
+ const probabilities = raw.probabilities === void 0 ? void 0 : numberRecord(raw.probabilities);
7522
+ if (raw.probabilities !== void 0 && probabilities === void 0) return void 0;
7523
+ return {
7524
+ type: "choice",
7525
+ choice: raw.choice,
7526
+ ...confidence === void 0 ? {} : { confidence },
7527
+ ...probabilities === void 0 ? {} : { probabilities }
7528
+ };
7529
+ }
7530
+ if (raw.type === "score") {
7531
+ const score = finite(raw.score);
7532
+ if (score === void 0) return void 0;
7533
+ const confidence = raw.confidence === void 0 ? void 0 : probability(raw.confidence);
7534
+ if (raw.confidence !== void 0 && confidence === void 0) return void 0;
7535
+ const probabilities = raw.probabilities === void 0 ? void 0 : numberRecord(raw.probabilities);
7536
+ if (raw.probabilities !== void 0 && probabilities === void 0) return void 0;
7537
+ return {
7538
+ type: "score",
7539
+ score,
7540
+ ...confidence === void 0 ? {} : { confidence },
7541
+ ...probabilities === void 0 ? {} : { probabilities }
7542
+ };
7543
+ }
7544
+ }
7545
+ /**
7546
+ * Parse a decision response body. Every answer entry must be well formed — one
7547
+ * malformed entry fails the whole response, because a partially understood
7548
+ * decision is not a decision. Unknown extra members are ignored so a server-side
7549
+ * addition cannot break the guard.
7550
+ *
7551
+ * @param value - the decoded JSON body.
7552
+ * @returns the parsed response, or undefined when the body is not one.
7553
+ */
7554
+ function parseSystemOneResponse(value) {
7555
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
7556
+ const raw = value;
7557
+ const rawAnswers = raw.answers;
7558
+ if (typeof rawAnswers !== "object" || rawAnswers === null || Array.isArray(rawAnswers)) return void 0;
7559
+ const entries = Object.entries(rawAnswers);
7560
+ if (entries.length === 0) return void 0;
7561
+ const answers = {};
7562
+ for (const [id, entry] of entries) {
7563
+ const answer = parseSystemOneAnswer(entry);
7564
+ if (answer === void 0) return void 0;
7565
+ answers[id] = answer;
7566
+ }
7567
+ const usage = raw.usage === void 0 ? void 0 : parseUsage(raw.usage);
7568
+ if (raw.usage !== void 0 && usage === void 0) return void 0;
7569
+ return {
7570
+ ...typeof raw.model === "string" ? { model: raw.model } : {},
7571
+ answers,
7572
+ ...usage === void 0 ? {} : { usage }
7573
+ };
7574
+ }
7575
+ /** Parse the optional `usage` member; an unreadable one is a malformed body. */
7576
+ function parseUsage(value) {
7577
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
7578
+ const raw = value;
7579
+ const inputTokens = raw.input_tokens === void 0 ? void 0 : finite(raw.input_tokens);
7580
+ if (raw.input_tokens !== void 0 && inputTokens === void 0) return void 0;
7581
+ const outputTokens = raw.output_tokens === void 0 ? void 0 : finite(raw.output_tokens);
7582
+ if (raw.output_tokens !== void 0 && outputTokens === void 0) return void 0;
7583
+ return {
7584
+ ...inputTokens === void 0 ? {} : { inputTokens },
7585
+ ...outputTokens === void 0 ? {} : { outputTokens }
7586
+ };
7587
+ }
7588
+ /** Validate the outgoing request where the server would only reject it with a 400. */
7589
+ function assertRequest(request) {
7590
+ const ids = Object.keys(request.questions);
7591
+ if (ids.length === 0) throw new SystemOneError("a decision request needs at least one question", "invalid-request");
7592
+ if (ids.length > 20) throw new SystemOneError(`a decision request carries at most 20 questions (got ${ids.length})`, "invalid-request");
7593
+ for (const id of ids) if (id.trim() === "") throw new SystemOneError("every decision question needs a non-empty id", "invalid-request");
7594
+ }
7595
+ /** Read the endpoint's error body into a one-line diagnosis. */
7596
+ async function httpFailure(response) {
7597
+ let detail = "";
7598
+ try {
7599
+ const slice = (await response.text()).trim().slice(0, 200);
7600
+ if (slice !== "") detail = `: ${slice}`;
7601
+ } catch {}
7602
+ return new SystemOneError(`Command Code decision request failed (HTTP ${response.status})${detail}`, "http", response.status);
7603
+ }
7604
+ /**
7605
+ * Run one decision request.
7606
+ *
7607
+ * @param deps - connection facts, credential chain and fetch seam.
7608
+ * @param request - the state and typed questions.
7609
+ * @param options - caller cancellation and the per-call budget.
7610
+ * @returns the parsed decision.
7611
+ * @throws {SystemOneError} on every failure path; callers fail closed.
7612
+ */
7613
+ async function runSystemOne(deps, request, options = {}) {
7614
+ assertRequest(request);
7615
+ const apiBase = deps.apiBase();
7616
+ if (!URL.canParse(apiBase)) throw new SystemOneError(`Command Code decision request is misconfigured: apiBase ${JSON.stringify(apiBase)} is not a valid URL`, "invalid-request");
7617
+ if (options.signal?.aborted === true) throw new SystemOneError("decision request aborted", "aborted");
7618
+ const timeoutMs = options.timeoutMs ?? 1500;
7619
+ const deadline = deadlineOf(options.signal, timeoutMs);
7620
+ try {
7621
+ let key;
7622
+ try {
7623
+ key = await withinDeadline(Promise.resolve().then(() => deps.resolveApiKey()), deadline, timeoutMs);
7624
+ } catch (error) {
7625
+ if (error instanceof SystemOneError && (error.kind === "aborted" || error.kind === "timeout")) throw error;
7626
+ throw new SystemOneError(`Command Code decision request has no usable credential: ${error instanceof Error ? error.message : String(error)}`, "credential", void 0, { cause: error });
7627
+ }
7628
+ const body = {
7629
+ model: request.model ?? "typesafe/jev",
7630
+ state: request.state,
7631
+ questions: request.questions
7632
+ };
7633
+ if (deadline.signal.aborted) throw deadline.callerAborted() ? new SystemOneError("decision request aborted", "aborted") : new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout");
7634
+ let response;
7635
+ try {
7636
+ response = await (deps.fetchImpl ?? fetch)(`${apiBase.replace(/\/$/, "")}${SYSTEMONE_ROUTE}`, {
7637
+ method: "POST",
7638
+ headers: {
7639
+ "Content-Type": "application/json",
7640
+ ...IDENTITY_ENCODING_HEADER,
7641
+ Authorization: `Bearer ${key}`,
7642
+ "x-command-code-version": COMMAND_CODE_CLI_VERSION,
7643
+ ...attributionHeaders()
7644
+ },
7645
+ body: JSON.stringify(body),
7646
+ signal: deadline.signal
7647
+ });
7648
+ } catch (error) {
7649
+ if (deadline.callerAborted()) throw new SystemOneError("decision request aborted", "aborted", void 0, { cause: error });
7650
+ if (deadline.timedOut() || isAbortError(error)) throw new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout", void 0, { cause: error });
7651
+ throw new SystemOneError(`Command Code decision request failed: ${error instanceof Error ? error.message : String(error)}`, "network", void 0, { cause: error });
7652
+ }
7653
+ if (!response.ok) throw await httpFailure(response);
7654
+ let payload;
7655
+ try {
7656
+ payload = await response.json();
7657
+ } catch (error) {
7658
+ if (deadline.callerAborted()) throw new SystemOneError("decision request aborted", "aborted", void 0, { cause: error });
7659
+ throw new SystemOneError("Command Code returned an unparseable decision body", "invalid-response", void 0, { cause: error });
7660
+ }
7661
+ const parsed = parseSystemOneResponse(payload);
7662
+ if (parsed === void 0) throw new SystemOneError("Command Code returned a decision body this plugin cannot read", "invalid-response");
7663
+ return parsed;
7664
+ } finally {
7665
+ deadline.dispose();
7666
+ }
7667
+ }
7668
+ //#endregion
7669
+ //#region src/command-guard.ts
7670
+ /** Tool names whose `arguments.command` this guard judges. */
7671
+ const COMMAND_GUARD_SHELL_TOOLS = ["bash", "pwsh"];
7672
+ /** Question id the safety verdict is keyed by. */
7673
+ const COMMAND_GUARD_QUESTION_ID = "safe";
7674
+ /** Decision budget; the approval prompt must not wait longer than this. */
7675
+ const COMMAND_GUARD_DEFAULT_TIMEOUT_MS = SYSTEMONE_DEFAULT_TIMEOUT_MS;
7676
+ /** Bounds of the configurable probability, mirrored by the settings schemas. */
7677
+ const COMMAND_GUARD_MIN_THRESHOLD = .5;
7678
+ const COMMAND_GUARD_MAX_TIMEOUT_MS = 1e4;
7679
+ /**
7680
+ * Longest command the guard will send for judgement. A longer one is delegated
7681
+ * instead of truncated: a verdict on the first N characters of a command is a
7682
+ * verdict on a different command.
7683
+ */
7684
+ const COMMAND_GUARD_MAX_COMMAND_CHARS = 6e3;
7685
+ /**
7686
+ * Hard denylist: commands the guard never auto-approves, and never even sends
7687
+ * for judgement. Each entry is a deliberate, narrow reading of one destructive
7688
+ * family — the goal is not to classify every dangerous command (that is the
7689
+ * model's job) but to make sure the classics cannot be talked into a grant by a
7690
+ * persuasive-looking command string.
7691
+ *
7692
+ * Every pattern is allowed to OVER-match: a false positive costs one ordinary
7693
+ * human prompt, while a false negative would hand a destructive command to a
7694
+ * model that has to guess. That asymmetry is why `sudo` is matched anywhere in
7695
+ * the line rather than only in command position, and why `fdisk` is listed even
7696
+ * though `fdisk -l` only lists partitions.
7697
+ */
7698
+ const COMMAND_GUARD_DENY_LIST = [
7699
+ {
7700
+ id: "rm-root",
7701
+ pattern: /\brm\s+(?:-[a-zA-Z]*[rR][a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*[rR][a-zA-Z]*|--recursive[^\n]*--force|--force[^\n]*--recursive)\s+(?:\/|\/\*|~|~\/\*|\$HOME(?:\/\*)?|\*|\.\/\*|\.)(?:\s|$)/
7702
+ },
7703
+ {
7704
+ id: "rm-home",
7705
+ pattern: /\brm\s+-[a-zA-Z]*[rR][a-zA-Z]*\s+(?:~|\$HOME)\//
7706
+ },
7707
+ {
7708
+ id: "filesystem-write",
7709
+ pattern: /\b(?:mkfs(?:\.\w+)?|wipefs|fdisk|parted|shred)\b/
7710
+ },
7711
+ {
7712
+ id: "raw-device-write",
7713
+ pattern: /\bdd\b[^\n]*\bof=\/dev\//
7714
+ },
7715
+ {
7716
+ id: "device-redirect",
7717
+ pattern: /(?:^|[^0-9\w])>{1,2}\s*\/dev\/(?:sd|disk|nvme|hd)/
7718
+ },
7719
+ {
7720
+ id: "fork-bomb",
7721
+ pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/
7722
+ },
7723
+ {
7724
+ id: "privilege-escalation",
7725
+ pattern: /\b(?:sudo|doas)\b/
7726
+ },
7727
+ {
7728
+ id: "su",
7729
+ pattern: /\bsu\s/
7730
+ },
7731
+ {
7732
+ id: "chmod-world-write",
7733
+ pattern: /\bchmod\b[^\n]*\b(?:777|a\+w|o\+w)\b/
7734
+ },
7735
+ {
7736
+ id: "chown-root",
7737
+ pattern: /\bchown\b[^\n]*\b(?:root|0):/
7738
+ },
7739
+ {
7740
+ id: "git-push",
7741
+ pattern: /\bgit\s+push\b/
7742
+ },
7743
+ {
7744
+ id: "git-history-rewrite",
7745
+ pattern: /\bgit\s+(?:filter-branch|filter-repo)\b/
7746
+ },
7747
+ {
7748
+ id: "git-hard-reset",
7749
+ pattern: /\bgit\s+reset\s+--hard\b/
7750
+ },
7751
+ {
7752
+ id: "git-clean-force",
7753
+ pattern: /\bgit\s+clean\b[^\n]*\s-[a-zA-Z]*f/
7754
+ },
7755
+ {
7756
+ id: "git-discard-worktree",
7757
+ pattern: /\bgit\s+(?:checkout|restore)\s+(?:--\s+)?\.(?:\s|$)/
7758
+ },
7759
+ {
7760
+ id: "git-stash-drop",
7761
+ pattern: /\bgit\s+stash\s+(?:drop|clear)\b/
7762
+ },
7763
+ {
7764
+ id: "publish",
7765
+ pattern: /\b(?:npm|pnpm|yarn|bun)\s+publish\b/
7766
+ },
7767
+ {
7768
+ id: "release-upload",
7769
+ pattern: /\b(?:gh\s+release|twine\s+upload)\b/
7770
+ },
7771
+ {
7772
+ id: "docker-push",
7773
+ pattern: /\bdocker\s+push\b/
7774
+ },
7775
+ {
7776
+ id: "cloud-upload",
7777
+ pattern: /\b(?:aws\s+s3|gsutil|gcloud\s+storage|az\s+storage)\b[^\n]*\b(?:cp|sync|mv|rm|rb)\b/
7778
+ },
7779
+ {
7780
+ id: "remote-copy",
7781
+ pattern: /\b(?:scp|rsync)\b[^\n]*(?::|@)/
7782
+ },
7783
+ {
7784
+ id: "pipe-to-shell",
7785
+ pattern: /\b(?:curl|wget|fetch)\b[^\n|]*\|\s*(?:sudo\s+)?(?:sh|bash|zsh|dash|ksh|python\d*|node|ruby|perl)\b/
7786
+ },
7787
+ {
7788
+ id: "power",
7789
+ pattern: /\b(?:shutdown|reboot|halt|poweroff)\b/
7790
+ },
7791
+ {
7792
+ id: "kill-everything",
7793
+ pattern: /\bkill\s+(?:-9\s+)?-1\b|\b(?:killall|pkill)\s+-9\b/
7794
+ },
7795
+ {
7796
+ id: "credential-write",
7797
+ pattern: /(?:>{1,2}|tee|cp|mv|rm|chmod|chown)\s[^\n]*(?:\.ssh\b|\.aws\b|\.gnupg\b|\.commandcode\/auth\.json|id_rsa|id_ed25519)/
7798
+ },
7799
+ {
7800
+ id: "history-clear",
7801
+ pattern: /\bhistory\s+-c\b|\bunset\s+HISTFILE\b/
7802
+ },
7803
+ {
7804
+ id: "global-install",
7805
+ pattern: /\b(?:npm|pnpm|yarn|bun)\s+(?:i|install|add)\s+(?:-g\b|--global\b)/
7806
+ },
7807
+ {
7808
+ id: "system-install",
7809
+ pattern: /\b(?:apt|apt-get|dnf|yum|zypper|apk|pacman|brew)\s+(?:install|remove|purge|uninstall|upgrade)\b/
7810
+ },
7811
+ {
7812
+ id: "language-install",
7813
+ pattern: /\b(?:pip3?|pipx|gem|cargo)\s+(?:install|add)\b/
7814
+ }
7815
+ ];
7816
+ /**
7817
+ * Why the guard refuses to judge a command, when it does.
7818
+ *
7819
+ * @param command - the raw command text.
7820
+ * @returns the matching denylist id, or undefined.
7821
+ */
7822
+ function denyReasonOf(command) {
7823
+ for (const entry of COMMAND_GUARD_DENY_LIST) if (entry.pattern.test(command)) return entry.id;
7824
+ }
7825
+ /** A non-empty string member, or undefined. */
7826
+ function stringMember(value, key) {
7827
+ if (typeof value !== "object" || value === null) return void 0;
7828
+ const member = value[key];
7829
+ return typeof member === "string" && member.trim() !== "" ? member : void 0;
7830
+ }
7831
+ /**
7832
+ * Read a shell command out of a tool execution, or undefined when this call is
7833
+ * not one the guard judges (a different tool, or arguments without a command).
7834
+ *
7835
+ * @param name - the executed tool's name.
7836
+ * @param args - the execution's parsed `arguments`.
7837
+ * @returns the fact to judge, without the asker's reason (that arrives later).
7838
+ */
7839
+ function shellCommandOf(name, args) {
7840
+ if (!COMMAND_GUARD_SHELL_TOOLS.includes(name)) return void 0;
7841
+ const command = stringMember(args, "command");
7842
+ if (command === void 0) return void 0;
7843
+ const description = stringMember(args, "description");
7844
+ const workdir = stringMember(args, "workdir");
7845
+ const sandboxPermissions = stringMember(args, "sandbox_permissions");
7846
+ return {
7847
+ tool: name,
7848
+ command,
7849
+ ...description === void 0 ? {} : { description },
7850
+ ...workdir === void 0 ? {} : { workdir },
7851
+ ...sandboxPermissions === void 0 ? {} : { sandboxPermissions }
7852
+ };
7853
+ }
7854
+ /**
7855
+ * Build the decision request for one command.
7856
+ *
7857
+ * The question is deliberately one `noul`: a single probability is a decision
7858
+ * rule that can be explained, thresholded and logged in one line, and the
7859
+ * criteria carry the whole distinction between "reads, or writes reversibly
7860
+ * inside the project" and "everything else". The agent's own `description` is
7861
+ * included as context, but the criteria say the command text itself decides — a
7862
+ * model-written description is a claim, not evidence.
7863
+ *
7864
+ * @param fact - the command and its context.
7865
+ * @returns the request to send.
7866
+ */
7867
+ function buildSafetyRequest(fact) {
7868
+ const lines = [
7869
+ `Tool: ${fact.tool}`,
7870
+ "Command:",
7871
+ fact.command,
7872
+ ...fact.description === void 0 ? [] : ["", `The agent describes this command as: ${fact.description}`],
7873
+ ...fact.workdir === void 0 ? [] : ["", `Working directory: ${fact.workdir}`],
7874
+ ...fact.sandboxPermissions === void 0 ? [] : ["", `The harness is asking because this call requests a wider sandbox mode (${fact.sandboxPermissions}) than the one currently in force.`],
7875
+ ...fact.reason === void 0 ? [] : ["", `The asker's reason for needing approval: ${fact.reason}`]
7876
+ ];
7877
+ const question = {
7878
+ type: "noul",
7879
+ 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.",
7880
+ criteria: {
7881
+ 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.",
7882
+ 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."
7883
+ }
7884
+ };
7885
+ return {
7886
+ state: lines.join("\n"),
7887
+ questions: { [COMMAND_GUARD_QUESTION_ID]: question }
7888
+ };
7889
+ }
7890
+ /**
7891
+ * Read the safety probability out of a decision response.
7892
+ *
7893
+ * @param response - the parsed response.
7894
+ * @param questionId - the question id to read (defaults to the guard's own).
7895
+ * @returns the probability of "safe", or undefined when the answer is missing
7896
+ * or is not a `noul` verdict.
7897
+ */
7898
+ function safeProbabilityOf(response, questionId = COMMAND_GUARD_QUESTION_ID) {
7899
+ const answer = response.answers[questionId];
7900
+ return answer !== void 0 && answer.type === "noul" ? answer.noul : void 0;
7901
+ }
7902
+ /**
7903
+ * One-line, length-bounded excerpt of a command for the log. The full text is
7904
+ * already in the session's tool call and was already sent to the decision
7905
+ * endpoint, so this adds no exposure — it is what makes "why did this run
7906
+ * without asking?" answerable from the log alone.
7907
+ */
7908
+ function commandExcerpt(command, limit = 160) {
7909
+ const flat = command.replace(/\s+/g, " ").trim();
7910
+ return flat.length <= limit ? flat : `${flat.slice(0, limit - 1)}…`;
7911
+ }
7912
+ /**
7913
+ * The guard's stateful half: what was executed (by call id) and what has
7914
+ * already been decided (by agent and complete decision context), both bounded.
7915
+ *
7916
+ * The execution cache is the bridge between the two seams — `tools/pre-execute`
7917
+ * knows the arguments, `approval/request` knows the call id — and it is keyed by
7918
+ * call id with an agent-identity check when read: an entry for an execution
7919
+ * that never asked is never read, so there is nothing to clean up on the result
7920
+ * side (and the ask happens before the result anyway).
7921
+ */
7922
+ var CommandGuard = class {
7923
+ deps;
7924
+ executions = /* @__PURE__ */ new Map();
7925
+ decisions = /* @__PURE__ */ new WeakMap();
7926
+ constructor(deps) {
7927
+ this.deps = deps;
7928
+ }
7929
+ /**
7930
+ * Remember one execution's command. Never throws, never changes a decision.
7931
+ *
7932
+ * @param execution - the execution about to run.
7933
+ */
7934
+ noteExecution(execution) {
7935
+ const callId = execution?.callId;
7936
+ if (typeof callId !== "string" || callId === "") return;
7937
+ const fact = shellCommandOf(execution.name, execution.arguments);
7938
+ if (fact === void 0) return;
7939
+ this.executions.delete(callId);
7940
+ this.executions.set(callId, {
7941
+ fact,
7942
+ agent: execution.agent
7943
+ });
7944
+ while (this.executions.size > 64) {
7945
+ const oldest = this.executions.keys().next();
7946
+ if (oldest.done === true) break;
7947
+ this.executions.delete(oldest.value);
7948
+ }
7949
+ }
7950
+ /** Forget every remembered execution and decision (disposal, tests). */
7951
+ clear() {
7952
+ this.executions.clear();
7953
+ this.decisions = /* @__PURE__ */ new WeakMap();
7954
+ }
7955
+ /**
7956
+ * Judge one approval request.
7957
+ *
7958
+ * @param request - the pending approval.
7959
+ * @returns the verdict; `approve` is the only outcome that grants anything.
7960
+ */
7961
+ async judge(request) {
7962
+ const settings = this.deps.settings();
7963
+ if (!settings.enabled) return {
7964
+ kind: "delegate",
7965
+ reason: "guard disabled"
7966
+ };
7967
+ const callId = request.callId;
7968
+ if (callId === void 0) return {
7969
+ kind: "delegate",
7970
+ reason: "approval carries no call id"
7971
+ };
7972
+ const observed = this.executions.get(callId);
7973
+ if (observed === void 0 || observed.agent !== request.agent) return {
7974
+ kind: "delegate",
7975
+ reason: "the guard never saw this call for this agent"
7976
+ };
7977
+ const fact = observed.fact;
7978
+ const excerpt = commandExcerpt(fact.command);
7979
+ if (!COMMAND_GUARD_SHELL_TOOLS.includes(request.toolName)) return {
7980
+ kind: "delegate",
7981
+ reason: `not a shell tool (${request.toolName})`
7982
+ };
7983
+ if (fact.command.length > 6e3) return {
7984
+ kind: "delegate",
7985
+ reason: `command longer than ${COMMAND_GUARD_MAX_COMMAND_CHARS} characters`,
7986
+ command: excerpt
7987
+ };
7988
+ const denied = denyReasonOf(fact.command);
7989
+ if (denied !== void 0) return {
7990
+ kind: "delegate",
7991
+ reason: `denylisted command (${denied})`,
7992
+ command: excerpt
7993
+ };
7994
+ const full = {
7995
+ ...fact,
7996
+ ...request.reason === void 0 ? {} : { reason: request.reason }
7997
+ };
7998
+ const memoKey = JSON.stringify(full);
7999
+ const now = this.deps.now?.() ?? Date.now();
8000
+ const agentDecisions = request.agent === void 0 ? void 0 : this.decisions.get(request.agent);
8001
+ const memo = agentDecisions?.get(memoKey);
8002
+ if (memo !== void 0) {
8003
+ if (now - memo.at < 6e5) return this.verdict(memo.probability, settings.threshold, "memo", excerpt);
8004
+ agentDecisions?.delete(memoKey);
8005
+ }
8006
+ let response;
8007
+ try {
8008
+ response = await this.deps.decide(buildSafetyRequest(full), {
8009
+ ...request.signal === void 0 ? {} : { signal: request.signal },
8010
+ timeoutMs: settings.timeoutMs
8011
+ });
8012
+ } catch (error) {
8013
+ return {
8014
+ kind: "delegate",
8015
+ reason: `decision unavailable: ${error instanceof Error ? error.message : String(error)}`,
8016
+ command: excerpt
8017
+ };
8018
+ }
8019
+ const probability = safeProbabilityOf(response);
8020
+ if (probability === void 0) return {
8021
+ kind: "delegate",
8022
+ reason: "the decision carried no safe verdict",
8023
+ command: excerpt
8024
+ };
8025
+ if (request.agent !== void 0) {
8026
+ const decisions = agentDecisions ?? /* @__PURE__ */ new Map();
8027
+ decisions.set(memoKey, {
8028
+ probability,
8029
+ at: now
8030
+ });
8031
+ while (decisions.size > 64) {
8032
+ const oldest = decisions.keys().next();
8033
+ if (oldest.done === true) break;
8034
+ decisions.delete(oldest.value);
8035
+ }
8036
+ this.decisions.set(request.agent, decisions);
8037
+ }
8038
+ return this.verdict(probability, settings.threshold, "model", excerpt);
8039
+ }
8040
+ /** Apply the threshold to one probability. */
8041
+ verdict(probability, threshold, source, command) {
8042
+ return probability >= threshold ? {
8043
+ kind: "approve",
8044
+ probability,
8045
+ source,
8046
+ command
8047
+ } : {
8048
+ kind: "delegate",
8049
+ reason: `safe probability ${probability.toFixed(3)} below ${threshold}`,
8050
+ probability,
8051
+ command
8052
+ };
8053
+ }
8054
+ };
8055
+ /**
8056
+ * Register the guard on a context that has the approval seam.
8057
+ *
8058
+ * Call this from inside `ctx.inject(['approval'], …)`: the seam's presence is
8059
+ * what makes the feature meaningful (no approval service means no asks), and
8060
+ * tying the listeners to that injected fiber is what makes them go away with it.
8061
+ *
8062
+ * @param ctx - the injected context (or a test double with `on`).
8063
+ * @param deps - settings, the decision client, clock and logger.
8064
+ * @returns a disposer for both listeners.
8065
+ */
8066
+ function applyCommandGuard(ctx, deps) {
8067
+ const guard = new CommandGuard(deps);
8068
+ const events = ctx;
8069
+ const offPreExecute = events.on("tools/pre-execute", (execution, next) => {
8070
+ guard.noteExecution(execution);
8071
+ return next();
8072
+ }, { prepend: true });
8073
+ const offApproval = events.on("approval/request", async (request, next) => {
8074
+ try {
8075
+ if (!deps.settings().enabled) return next();
8076
+ } catch (error) {
8077
+ deps.log?.(`command guard failed: ${error instanceof Error ? error.message : String(error)}`);
8078
+ return next();
8079
+ }
8080
+ let verdict;
8081
+ try {
8082
+ verdict = await guard.judge(request);
8083
+ } catch (error) {
8084
+ deps.log?.(`command guard failed: ${error instanceof Error ? error.message : String(error)}`);
8085
+ return next();
8086
+ }
8087
+ if (verdict.kind === "approve") {
8088
+ deps.log?.(`command guard auto-approved a ${request.toolName} call (safe probability ${verdict.probability.toFixed(3)}, ${verdict.source}): ${verdict.command ?? "(command unknown)"}`);
8089
+ return "allowed-once";
8090
+ }
8091
+ deps.log?.(`command guard delegated a ${request.toolName} call: ${verdict.reason}${verdict.command === void 0 ? "" : ` — ${verdict.command}`}`);
8092
+ return next();
8093
+ }, { prepend: true });
8094
+ return () => {
8095
+ guard.clear();
8096
+ for (const off of [offPreExecute, offApproval]) if (typeof off === "function") off();
8097
+ };
8098
+ }
8099
+ //#endregion
7078
8100
  //#region src/config-volatile.ts
7079
8101
  /**
7080
8102
  * Mark one schema field volatile when the installed schemastery supports it.
@@ -7219,6 +8241,10 @@ function configFields() {
7219
8241
  models: z.array(z.string()),
7220
8242
  account: z.string()
7221
8243
  })),
8244
+ commandGuard: z.boolean().default(false),
8245
+ commandGuardThreshold: z.number().min(COMMAND_GUARD_MIN_THRESHOLD).max(1),
8246
+ commandGuardTimeoutMs: z.number().min(200).max(COMMAND_GUARD_MAX_TIMEOUT_MS),
8247
+ zdr: z.boolean().default(false),
7222
8248
  lang: z.string().pattern(/^(zh|en)$/).default("zh")
7223
8249
  };
7224
8250
  }
@@ -7267,7 +8293,8 @@ function resolveAdapterOptions(config) {
7267
8293
  streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
7268
8294
  filterModelsByPlan: config.filterModelsByPlan ?? true,
7269
8295
  visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : void 0,
7270
- modelVisibility: readModelVisibility(config.modelVisibility)
8296
+ modelVisibility: readModelVisibility(config.modelVisibility),
8297
+ zdr: config.zdr === true
7271
8298
  };
7272
8299
  }
7273
8300
  /**
@@ -7437,8 +8464,11 @@ function apply(ctx, config) {
7437
8464
  });
7438
8465
  const loginFlow = new CommandCodeLoginFlow({
7439
8466
  apiBase: () => options().apiBase,
7440
- storeKey: async ({ apiKey }) => {
7441
- const ref = credentialRef(current().apiKeyEnv ?? DEFAULT_API_KEY_ENV);
8467
+ validateTargetRef: (targetRef) => {
8468
+ loginCredentialRef(targetRef, current().apiKeyEnv ?? DEFAULT_API_KEY_ENV, current().accounts ?? []);
8469
+ },
8470
+ storeKey: async ({ apiKey }, targetRef) => {
8471
+ const ref = credentialRef(loginCredentialRef(targetRef, current().apiKeyEnv ?? DEFAULT_API_KEY_ENV, current().accounts ?? []));
7442
8472
  const credentials = ctx.get("credentials");
7443
8473
  if (credentials === void 0) throw new Error("the credentials service is unavailable in this profile; paste the key manually");
7444
8474
  await credentials.set(ref, apiKey);
@@ -7481,6 +8511,26 @@ function apply(ctx, config) {
7481
8511
  applyCommandCodeSearchSelection(webCtx.web, searchSelection, false);
7482
8512
  }, "dsh-commandcode-provider: web search selection");
7483
8513
  });
8514
+ const commandGuardSettings = () => {
8515
+ const raw = current();
8516
+ return {
8517
+ enabled: raw.commandGuard === true,
8518
+ threshold: raw.commandGuardThreshold ?? .9,
8519
+ timeoutMs: raw.commandGuardTimeoutMs ?? COMMAND_GUARD_DEFAULT_TIMEOUT_MS
8520
+ };
8521
+ };
8522
+ ctx.inject(["approval"], (approvalCtx) => {
8523
+ applyCommandGuard(approvalCtx, {
8524
+ settings: commandGuardSettings,
8525
+ decide: (request, decideOptions) => runSystemOne({
8526
+ apiBase: () => options().apiBase,
8527
+ resolveApiKey: () => resolveApiKey(options())
8528
+ }, request, decideOptions),
8529
+ log: (message) => {
8530
+ ctx.logger.info(`llm-commandcode: ${message}`);
8531
+ }
8532
+ });
8533
+ });
7484
8534
  let refreshTuiSettings;
7485
8535
  ctx.inject(["tuiSettingsSections"], (tuiCtx) => {
7486
8536
  refreshTuiSettings = applyCommandCodeTuiSettings(tuiCtx, {
@@ -7530,6 +8580,6 @@ function apply(ctx, config) {
7530
8580
  }, "dsh-commandcode-provider: volatile config updates");
7531
8581
  }
7532
8582
  //#endregion
7533
- export { ACTIVE_ACCOUNT_AUTO, BILLING_ACCESS_TTL_MS, COMMANDCODE_SEARCH_PROVIDER_ID, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeLoginFlow, CommandCodeSearchProvider, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_WEB_SEARCH_PROVIDER_ID, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, LANG_AUTO, LOGIN_ALLOWED_ORIGINS, LOGIN_BEGIN_ENDPOINT, LOGIN_BODY_LIMIT_BYTES, LOGIN_CANCEL_ENDPOINT, LOGIN_MAX_PORT_ATTEMPTS, LOGIN_START_PORT, LOGIN_STATUS_ENDPOINT, LOGIN_TIMEOUT_MS, LegacySettingsSchema, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommandCodeSearchSelection, applyCommandCodeTuiSettings, applyCommands, applyUsageRemote, buildCommandAuthUrl, buildCommandCodeTuiSection, capabilityDescription, commandCodeSearchSelection, commandDefinition, compareByPlan, dealLabel, formatContext, inject, loginStatusSchema, matchModelRule, modelVisibleInPlan, name, parseLoginStatus, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectAccountForModel, selectActiveAccount, selectCommandCodeSearchProvider, studioBaseForApiBase, subscriptionPlanInfo, usageReportSchema, validateCommandApiKey };
8583
+ export { ACTIVE_ACCOUNT_AUTO, BILLING_ACCESS_TTL_MS, COMMANDCODE_SEARCH_PROVIDER_ID, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeLoginFlow, CommandCodeSearchProvider, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_WEB_SEARCH_PROVIDER_ID, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_NON_ZDR_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, LANG_AUTO, LOGIN_ALLOWED_ORIGINS, LOGIN_BEGIN_ENDPOINT, LOGIN_BODY_LIMIT_BYTES, LOGIN_CANCEL_ENDPOINT, LOGIN_MAX_PORT_ATTEMPTS, LOGIN_START_PORT, LOGIN_STATUS_ENDPOINT, LOGIN_TIMEOUT_MS, LegacySettingsSchema, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommandCodeSearchSelection, applyCommandCodeTuiSettings, applyCommands, applyUsageRemote, buildCommandAuthUrl, buildCommandCodeTuiSection, capabilityDescription, commandCodeSearchSelection, commandDefinition, compareByPlan, dealLabel, formatContext, inject, loginStatusSchema, matchModelRule, modelVisibleInPlan, name, parseLoginStatus, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectAccountForModel, selectActiveAccount, selectCommandCodeSearchProvider, studioBaseForApiBase, subscriptionPlanInfo, supportsZeroDataRetention, usageReportSchema, validateCommandApiKey };
7534
8584
 
7535
8585
  //# sourceMappingURL=index.js.map