@mars-sea/dsh-commandcode-provider 0.11.9 → 0.11.10
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/CHANGELOG.md +16 -1
- package/README.md +35 -5
- package/README.zh-CN.md +35 -5
- package/icon.svg +1 -5
- package/lib/client.js +79 -2
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +90 -2
- package/lib/index.js +997 -16
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
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 ?? "",
|
|
@@ -2849,7 +2994,7 @@ function openStreamTrace(scope, options = {}) {
|
|
|
2849
2994
|
* per-request options thunk and an API-key resolver from the plugin entry
|
|
2850
2995
|
* (src/index.ts), so a settings change reaches the very next request.
|
|
2851
2996
|
*/
|
|
2852
|
-
const COMMAND_CODE_CLI_VERSION = "1.
|
|
2997
|
+
const COMMAND_CODE_CLI_VERSION = "1.64.0";
|
|
2853
2998
|
const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
2854
2999
|
const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
|
|
2855
3000
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
|
|
@@ -2973,6 +3118,19 @@ function isContextOverflowDetail(detail) {
|
|
|
2973
3118
|
return isContextWindowExceededError(detail) || CLI_CONTEXT_OVERFLOW_PATTERN.test(detail);
|
|
2974
3119
|
}
|
|
2975
3120
|
/**
|
|
3121
|
+
* Whether one rejection says "ZDR requested, no ZDR-capable upstream": the
|
|
3122
|
+
* provider spells the code `cmd_zdr_no_providers` in JSON error bodies and
|
|
3123
|
+
* `CMD_ZDR_NO_PROVIDERS` in plain-text ones (the official CLI's own
|
|
3124
|
+
* classifier checks both spellings, and its error text tells the user to
|
|
3125
|
+
* unset `CMD_ZDR`). Underscore-separated matching normalizes the separator, so
|
|
3126
|
+
* one comparison covers the variants — the same normalization the pre-stream
|
|
3127
|
+
* rejection classifier and `hasTerminalStreamMarker` already use.
|
|
3128
|
+
*/
|
|
3129
|
+
const ZDR_NO_PROVIDERS_PATTERN = /cmd[_\s-]?zdr[_\s-]?no[_\s-]?providers/i;
|
|
3130
|
+
function isZdrNoProviders(providerCode, providerDetail, errText) {
|
|
3131
|
+
return ZDR_NO_PROVIDERS_PATTERN.test(providerCode ?? "") || ZDR_NO_PROVIDERS_PATTERN.test(providerDetail) || ZDR_NO_PROVIDERS_PATTERN.test(errText);
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
2976
3134
|
* Classify one in-band stream `error` payload into the failure the caller
|
|
2977
3135
|
* throws. Shared by both transports — the CLI transport delivers it as an
|
|
2978
3136
|
* `error` event (`{ type: 'error', error }`), the Provider API transport as a
|
|
@@ -3936,6 +4094,7 @@ async function connectGenerate(deps, key, protocol, body) {
|
|
|
3936
4094
|
if (options.signal) options.signal.removeEventListener("abort", onCallerAbort);
|
|
3937
4095
|
};
|
|
3938
4096
|
let response;
|
|
4097
|
+
const zdr = connection.zdr === true;
|
|
3939
4098
|
const headers = protocol === "cli" ? {
|
|
3940
4099
|
"Content-Type": "application/json",
|
|
3941
4100
|
Authorization: `Bearer ${key}`,
|
|
@@ -3944,11 +4103,13 @@ async function connectGenerate(deps, key, protocol, body) {
|
|
|
3944
4103
|
"x-project-slug": projectSlugFromPath(connection.workingDir),
|
|
3945
4104
|
"x-taste-learning": "true",
|
|
3946
4105
|
"x-co-flag": "false",
|
|
4106
|
+
...zdr ? { "x-cmd-zdr": "1" } : {},
|
|
3947
4107
|
...attributionHeaders()
|
|
3948
4108
|
} : {
|
|
3949
4109
|
"Content-Type": "application/json",
|
|
3950
4110
|
Authorization: `Bearer ${key}`,
|
|
3951
4111
|
Accept: "text/event-stream",
|
|
4112
|
+
...zdr ? { "x-cmd-zdr": "1" } : {},
|
|
3952
4113
|
...attributionHeaders()
|
|
3953
4114
|
};
|
|
3954
4115
|
try {
|
|
@@ -4804,6 +4965,7 @@ function generateHttpError(status, errText, retryAfterMs) {
|
|
|
4804
4965
|
const detail = providerCode ?? `HTTP ${status}`;
|
|
4805
4966
|
const overflowDetail = providerDetail !== "" ? providerDetail : errText.slice(0, 500);
|
|
4806
4967
|
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 });
|
|
4968
|
+
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
4969
|
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
4970
|
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
4971
|
return new LlmError(`Command Code API error ${status}${detail === `HTTP ${status}` ? "" : ` (${detail})`}: ${errText.slice(0, 500)}`, httpErrorCode(status), {
|
|
@@ -6525,7 +6687,7 @@ function toSource(result) {
|
|
|
6525
6687
|
...snippet !== void 0 && snippet.length > 0 ? { snippet } : {}
|
|
6526
6688
|
};
|
|
6527
6689
|
}
|
|
6528
|
-
function isAbortError(error) {
|
|
6690
|
+
function isAbortError$1(error) {
|
|
6529
6691
|
return error instanceof DOMException && error.name === "AbortError";
|
|
6530
6692
|
}
|
|
6531
6693
|
/** Build the provider's stable cancellation error while retaining the caller's reason. */
|
|
@@ -6579,7 +6741,7 @@ var CommandCodeSearchProvider = class {
|
|
|
6579
6741
|
...signal !== void 0 ? { signal } : {}
|
|
6580
6742
|
});
|
|
6581
6743
|
} catch (error) {
|
|
6582
|
-
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
6744
|
+
if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
|
|
6583
6745
|
throw new WebError(`Command Code web search request failed: ${error instanceof Error ? error.message : String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
6584
6746
|
}
|
|
6585
6747
|
if (!response.ok) {
|
|
@@ -6594,7 +6756,7 @@ var CommandCodeSearchProvider = class {
|
|
|
6594
6756
|
if (typeof code === "string" || typeof inner === "string") message += `: ${typeof code === "string" ? code : ""}${typeof code === "string" && typeof inner === "string" ? " — " : ""}${typeof inner === "string" ? inner : ""}`;
|
|
6595
6757
|
}
|
|
6596
6758
|
} catch (error) {
|
|
6597
|
-
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
6759
|
+
if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
|
|
6598
6760
|
const slice = (await response.text().catch(() => "")).trim().slice(0, 200);
|
|
6599
6761
|
if (slice !== "") message += `: ${slice}`;
|
|
6600
6762
|
}
|
|
@@ -6604,7 +6766,7 @@ var CommandCodeSearchProvider = class {
|
|
|
6604
6766
|
try {
|
|
6605
6767
|
payload = await response.json();
|
|
6606
6768
|
} catch (error) {
|
|
6607
|
-
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
6769
|
+
if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
|
|
6608
6770
|
throw new WebError("Command Code web search returned an unparseable response body", "WEB_PROVIDER_ERROR", { cause: error });
|
|
6609
6771
|
}
|
|
6610
6772
|
const results = payload?.results;
|
|
@@ -6628,7 +6790,7 @@ var CommandCodeSearchProvider = class {
|
|
|
6628
6790
|
try {
|
|
6629
6791
|
key = await this.deps.resolveKey();
|
|
6630
6792
|
} catch (error) {
|
|
6631
|
-
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
6793
|
+
if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
|
|
6632
6794
|
if (error instanceof Error && typeof error.code === "string") {
|
|
6633
6795
|
const code = error.code;
|
|
6634
6796
|
if (code === "MISSING_CREDENTIAL") throw new WebError(error.message, "WEB_PROVIDER_CREDENTIAL_MISSING", { cause: error });
|
|
@@ -6816,6 +6978,70 @@ function buildCommandCodeTuiSection(deps) {
|
|
|
6816
6978
|
};
|
|
6817
6979
|
}
|
|
6818
6980
|
},
|
|
6981
|
+
{
|
|
6982
|
+
path: ["commandGuard"],
|
|
6983
|
+
group: "advanced",
|
|
6984
|
+
kind: "boolean",
|
|
6985
|
+
label: "AI command guard",
|
|
6986
|
+
descriptions: { zh: "AI 命令安全预判" },
|
|
6987
|
+
hint: "typesafe/jev judges whether a shell command is safe and auto-approves it; everything else asks as usual. Off by default.",
|
|
6988
|
+
hintDescriptions: { zh: "由 typesafe/jev 判断 shell 命令是否安全,安全则自动放行,其余照常弹窗。默认关闭。" },
|
|
6989
|
+
format: (value) => value === true ? "true" : "false",
|
|
6990
|
+
parse: (text) => ({
|
|
6991
|
+
kind: "set",
|
|
6992
|
+
value: text.trim() === "true"
|
|
6993
|
+
})
|
|
6994
|
+
},
|
|
6995
|
+
{
|
|
6996
|
+
path: ["commandGuardThreshold"],
|
|
6997
|
+
group: "advanced",
|
|
6998
|
+
kind: "number",
|
|
6999
|
+
label: "Auto-approve threshold (0.5-1)",
|
|
7000
|
+
descriptions: { zh: "自动放行阈值(0.5-1)" },
|
|
7001
|
+
hint: "Approve when typesafe/jev rates the command \"safe\" at least this likely; default 0.9.",
|
|
7002
|
+
hintDescriptions: { zh: "typesafe/jev 判定\"安全\"的概率达到该值即放行,默认 0.9。" },
|
|
7003
|
+
parse: (text) => {
|
|
7004
|
+
const trimmed = text.trim();
|
|
7005
|
+
if (trimmed === "") return { kind: "clear" };
|
|
7006
|
+
const parsed = Number(trimmed);
|
|
7007
|
+
return Number.isFinite(parsed) && parsed >= .5 && parsed <= 1 ? {
|
|
7008
|
+
kind: "set",
|
|
7009
|
+
value: parsed
|
|
7010
|
+
} : void 0;
|
|
7011
|
+
}
|
|
7012
|
+
},
|
|
7013
|
+
{
|
|
7014
|
+
path: ["commandGuardTimeoutMs"],
|
|
7015
|
+
group: "advanced",
|
|
7016
|
+
kind: "number",
|
|
7017
|
+
label: "Judgement timeout (ms, 200-10000)",
|
|
7018
|
+
descriptions: { zh: "判定超时(毫秒,200-10000)" },
|
|
7019
|
+
hint: "If typesafe/jev does not answer in time, the normal prompt appears; default 1500.",
|
|
7020
|
+
hintDescriptions: { zh: "typesafe/jev 超过该时间未返回就照常弹窗,默认 1500。" },
|
|
7021
|
+
parse: (text) => {
|
|
7022
|
+
const trimmed = text.trim();
|
|
7023
|
+
if (trimmed === "") return { kind: "clear" };
|
|
7024
|
+
const parsed = Number(trimmed);
|
|
7025
|
+
return Number.isFinite(parsed) && parsed >= 200 && parsed <= 1e4 ? {
|
|
7026
|
+
kind: "set",
|
|
7027
|
+
value: parsed
|
|
7028
|
+
} : void 0;
|
|
7029
|
+
}
|
|
7030
|
+
},
|
|
7031
|
+
{
|
|
7032
|
+
path: ["zdr"],
|
|
7033
|
+
group: "advanced",
|
|
7034
|
+
kind: "boolean",
|
|
7035
|
+
label: "Zero data retention (ZDR)",
|
|
7036
|
+
descriptions: { zh: "零数据保留(ZDR)" },
|
|
7037
|
+
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.",
|
|
7038
|
+
hintDescriptions: { zh: "请求只经由不留存数据、也不用于训练的上游(等价于 CLI 的 CMD_ZDR=1)。没有可用 ZDR 上游的模型会报错,不会降级为非 ZDR 请求;ZDR 通常按更高的透传价计费。默认关闭。" },
|
|
7039
|
+
format: (value) => value === true ? "true" : "false",
|
|
7040
|
+
parse: (text) => ({
|
|
7041
|
+
kind: "set",
|
|
7042
|
+
value: text.trim() === "true"
|
|
7043
|
+
})
|
|
7044
|
+
},
|
|
6819
7045
|
{
|
|
6820
7046
|
path: ["filterModelsByPlan"],
|
|
6821
7047
|
group: "models",
|
|
@@ -7075,6 +7301,736 @@ function transportBudgetMessage(failureMessage, maxRetries) {
|
|
|
7075
7301
|
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
7302
|
}
|
|
7077
7303
|
//#endregion
|
|
7304
|
+
//#region src/systemone.ts
|
|
7305
|
+
/**
|
|
7306
|
+
* Host-side client for Command Code's System One decision endpoint
|
|
7307
|
+
* (`POST {apiBase}/provider/v1/systemone`).
|
|
7308
|
+
*
|
|
7309
|
+
* `typesafe/jev` is **not a chat model**: it takes a `state` plus typed
|
|
7310
|
+
* questions and answers with probabilities, never text, and it never streams.
|
|
7311
|
+
* It is therefore absent from `/provider/v1/models` (so the picker can never
|
|
7312
|
+
* offer it) and must never be registered as a model route — this plugin uses it
|
|
7313
|
+
* as an internal advisor instead: the command guard in `./command-guard.ts`
|
|
7314
|
+
* asks it whether a shell command is safe to auto-approve.
|
|
7315
|
+
*
|
|
7316
|
+
* The three question types are the whole vocabulary (command-code@1.64.0
|
|
7317
|
+
* `systemOneRequestSchema`):
|
|
7318
|
+
*
|
|
7319
|
+
* - `noul` — "is this true?", optional `criteria: { true, false }` describing
|
|
7320
|
+
* each verdict. Answers with `noul`, the probability of *yes*.
|
|
7321
|
+
* - `choice` — "which one?", `criteria` maps each option key to its
|
|
7322
|
+
* description. Answers with `choice`, plus optional `confidence` and
|
|
7323
|
+
* `probabilities`.
|
|
7324
|
+
* - `score` — "where on this rubric?", `criteria` is an array of at least two
|
|
7325
|
+
* levels, lowest first. Answers with `score`, `legend` and `probabilities`.
|
|
7326
|
+
*
|
|
7327
|
+
* A request carries at most 20 questions and every answer is keyed by the
|
|
7328
|
+
* question id it belongs to. `usage` is reported per request, in tokens.
|
|
7329
|
+
*
|
|
7330
|
+
* Billing: free through 2026-09-24T23:59:59Z, then $0.042 per 1M input tokens
|
|
7331
|
+
* with output free. The model has **no ZDR-capable upstream**, so a request
|
|
7332
|
+
* carrying `x-cmd-zdr: 1` is refused with HTTP 422 `cmd_zdr_no_providers` —
|
|
7333
|
+
* this client never sends that header, and it must stay that way even though
|
|
7334
|
+
* the plugin now ships a ZDR switch for chat (`Config.zdr` → the `x-cmd-zdr: 1`
|
|
7335
|
+
* header per model, gated on `KNOWN_NON_ZDR_MODELS`): the decision endpoint is
|
|
7336
|
+
* reached through THIS module's own request path, never through the adapter's
|
|
7337
|
+
* chat connection, so the two cannot drift — and a deployment that later widens
|
|
7338
|
+
* ZDR must exclude this endpoint rather than route it through the switch.
|
|
7339
|
+
*
|
|
7340
|
+
* Auth is the plugin's ordinary chain (the multi-account pool, the credentials
|
|
7341
|
+
* seam, the CLI auth file) through the same `Authorization: Bearer <key>` and
|
|
7342
|
+
* `x-command-code-version` headers every other endpoint uses.
|
|
7343
|
+
*
|
|
7344
|
+
* @module dsh-commandcode-provider/systemone
|
|
7345
|
+
*/
|
|
7346
|
+
/** Path of the decision endpoint, relative to `apiBase`. */
|
|
7347
|
+
const SYSTEMONE_ROUTE = "/provider/v1/systemone";
|
|
7348
|
+
/** Milliseconds a decision may take before the caller gives up on it. */
|
|
7349
|
+
const SYSTEMONE_DEFAULT_TIMEOUT_MS = 1500;
|
|
7350
|
+
/** The one error type this client throws. Callers fail closed on it. */
|
|
7351
|
+
var SystemOneError = class extends Error {
|
|
7352
|
+
kind;
|
|
7353
|
+
status;
|
|
7354
|
+
constructor(message, kind, status, options) {
|
|
7355
|
+
super(message, options);
|
|
7356
|
+
this.kind = kind;
|
|
7357
|
+
this.status = status;
|
|
7358
|
+
this.name = "SystemOneError";
|
|
7359
|
+
}
|
|
7360
|
+
};
|
|
7361
|
+
/**
|
|
7362
|
+
* Combine the caller's signal with a timeout, keeping the two distinguishable:
|
|
7363
|
+
* a timeout is this client's own budget expiring (the guard delegates to the
|
|
7364
|
+
* user) while a caller abort means the approval was already withdrawn (answer
|
|
7365
|
+
* nothing) — both fail closed, but the diagnosis must not lie about which.
|
|
7366
|
+
*
|
|
7367
|
+
* The timer is a plain `setTimeout` rather than `AbortSignal.timeout()`, whose
|
|
7368
|
+
* timer Node unrefs: with nothing else pending (a test double, a socket that
|
|
7369
|
+
* never opened) an unref'd timer lets the process exit while the promise is
|
|
7370
|
+
* still pending, which reads as a hang rather than as the timeout it is.
|
|
7371
|
+
*/
|
|
7372
|
+
function deadlineOf(signal, timeoutMs) {
|
|
7373
|
+
const controller = new AbortController();
|
|
7374
|
+
let timedOut = false;
|
|
7375
|
+
const timer = setTimeout(() => {
|
|
7376
|
+
timedOut = true;
|
|
7377
|
+
controller.abort();
|
|
7378
|
+
}, timeoutMs);
|
|
7379
|
+
const onCallerAbort = () => controller.abort();
|
|
7380
|
+
if (signal !== void 0) {
|
|
7381
|
+
if (signal.aborted) controller.abort();
|
|
7382
|
+
else signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
7383
|
+
}
|
|
7384
|
+
return {
|
|
7385
|
+
signal: controller.signal,
|
|
7386
|
+
callerAborted: () => signal?.aborted === true,
|
|
7387
|
+
timedOut: () => timedOut,
|
|
7388
|
+
dispose: () => {
|
|
7389
|
+
clearTimeout(timer);
|
|
7390
|
+
signal?.removeEventListener("abort", onCallerAbort);
|
|
7391
|
+
}
|
|
7392
|
+
};
|
|
7393
|
+
}
|
|
7394
|
+
/** Whether a caught value is an abort-shaped failure. */
|
|
7395
|
+
function isAbortError(error) {
|
|
7396
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
7397
|
+
}
|
|
7398
|
+
/** Stop waiting for an operation that does not accept an AbortSignal. */
|
|
7399
|
+
function withinDeadline(operation, deadline, timeoutMs) {
|
|
7400
|
+
return new Promise((resolve, reject) => {
|
|
7401
|
+
const onAbort = () => {
|
|
7402
|
+
reject(deadline.callerAborted() ? new SystemOneError("decision request aborted", "aborted") : new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout"));
|
|
7403
|
+
};
|
|
7404
|
+
deadline.signal.addEventListener("abort", onAbort, { once: true });
|
|
7405
|
+
operation.then((value) => {
|
|
7406
|
+
deadline.signal.removeEventListener("abort", onAbort);
|
|
7407
|
+
resolve(value);
|
|
7408
|
+
}, (error) => {
|
|
7409
|
+
deadline.signal.removeEventListener("abort", onAbort);
|
|
7410
|
+
reject(error);
|
|
7411
|
+
});
|
|
7412
|
+
if (deadline.signal.aborted) onAbort();
|
|
7413
|
+
});
|
|
7414
|
+
}
|
|
7415
|
+
/** A finite number in `[0, 1]`, or undefined. */
|
|
7416
|
+
function probability(value) {
|
|
7417
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : void 0;
|
|
7418
|
+
}
|
|
7419
|
+
/** A finite number, or undefined. */
|
|
7420
|
+
function finite(value) {
|
|
7421
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
7422
|
+
}
|
|
7423
|
+
/** A record of finite numbers (probabilities), or undefined when the shape is off. */
|
|
7424
|
+
function numberRecord(value) {
|
|
7425
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
7426
|
+
const out = {};
|
|
7427
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
7428
|
+
const parsed = finite(entry);
|
|
7429
|
+
if (parsed === void 0) return void 0;
|
|
7430
|
+
out[key] = parsed;
|
|
7431
|
+
}
|
|
7432
|
+
return out;
|
|
7433
|
+
}
|
|
7434
|
+
/**
|
|
7435
|
+
* Parse one answer entry strictly by its own `type`. A body whose answer does
|
|
7436
|
+
* not carry the member its type promises is rejected rather than defaulted:
|
|
7437
|
+
* an absent probability is exactly the case the caller must not read as "safe".
|
|
7438
|
+
*
|
|
7439
|
+
* @param value - one raw entry from the response's `answers` object.
|
|
7440
|
+
* @returns the parsed answer, or undefined when the entry is malformed.
|
|
7441
|
+
*/
|
|
7442
|
+
function parseSystemOneAnswer(value) {
|
|
7443
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
7444
|
+
const raw = value;
|
|
7445
|
+
if (raw.type === "noul") {
|
|
7446
|
+
const noul = probability(raw.noul);
|
|
7447
|
+
return noul === void 0 ? void 0 : {
|
|
7448
|
+
type: "noul",
|
|
7449
|
+
noul
|
|
7450
|
+
};
|
|
7451
|
+
}
|
|
7452
|
+
if (raw.type === "choice") {
|
|
7453
|
+
if (typeof raw.choice !== "string" || raw.choice === "") return void 0;
|
|
7454
|
+
const confidence = raw.confidence === void 0 ? void 0 : probability(raw.confidence);
|
|
7455
|
+
if (raw.confidence !== void 0 && confidence === void 0) return void 0;
|
|
7456
|
+
const probabilities = raw.probabilities === void 0 ? void 0 : numberRecord(raw.probabilities);
|
|
7457
|
+
if (raw.probabilities !== void 0 && probabilities === void 0) return void 0;
|
|
7458
|
+
return {
|
|
7459
|
+
type: "choice",
|
|
7460
|
+
choice: raw.choice,
|
|
7461
|
+
...confidence === void 0 ? {} : { confidence },
|
|
7462
|
+
...probabilities === void 0 ? {} : { probabilities }
|
|
7463
|
+
};
|
|
7464
|
+
}
|
|
7465
|
+
if (raw.type === "score") {
|
|
7466
|
+
const score = finite(raw.score);
|
|
7467
|
+
if (score === void 0) return void 0;
|
|
7468
|
+
const confidence = raw.confidence === void 0 ? void 0 : probability(raw.confidence);
|
|
7469
|
+
if (raw.confidence !== void 0 && confidence === void 0) return void 0;
|
|
7470
|
+
const probabilities = raw.probabilities === void 0 ? void 0 : numberRecord(raw.probabilities);
|
|
7471
|
+
if (raw.probabilities !== void 0 && probabilities === void 0) return void 0;
|
|
7472
|
+
return {
|
|
7473
|
+
type: "score",
|
|
7474
|
+
score,
|
|
7475
|
+
...confidence === void 0 ? {} : { confidence },
|
|
7476
|
+
...probabilities === void 0 ? {} : { probabilities }
|
|
7477
|
+
};
|
|
7478
|
+
}
|
|
7479
|
+
}
|
|
7480
|
+
/**
|
|
7481
|
+
* Parse a decision response body. Every answer entry must be well formed — one
|
|
7482
|
+
* malformed entry fails the whole response, because a partially understood
|
|
7483
|
+
* decision is not a decision. Unknown extra members are ignored so a server-side
|
|
7484
|
+
* addition cannot break the guard.
|
|
7485
|
+
*
|
|
7486
|
+
* @param value - the decoded JSON body.
|
|
7487
|
+
* @returns the parsed response, or undefined when the body is not one.
|
|
7488
|
+
*/
|
|
7489
|
+
function parseSystemOneResponse(value) {
|
|
7490
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
7491
|
+
const raw = value;
|
|
7492
|
+
const rawAnswers = raw.answers;
|
|
7493
|
+
if (typeof rawAnswers !== "object" || rawAnswers === null || Array.isArray(rawAnswers)) return void 0;
|
|
7494
|
+
const entries = Object.entries(rawAnswers);
|
|
7495
|
+
if (entries.length === 0) return void 0;
|
|
7496
|
+
const answers = {};
|
|
7497
|
+
for (const [id, entry] of entries) {
|
|
7498
|
+
const answer = parseSystemOneAnswer(entry);
|
|
7499
|
+
if (answer === void 0) return void 0;
|
|
7500
|
+
answers[id] = answer;
|
|
7501
|
+
}
|
|
7502
|
+
const usage = raw.usage === void 0 ? void 0 : parseUsage(raw.usage);
|
|
7503
|
+
if (raw.usage !== void 0 && usage === void 0) return void 0;
|
|
7504
|
+
return {
|
|
7505
|
+
...typeof raw.model === "string" ? { model: raw.model } : {},
|
|
7506
|
+
answers,
|
|
7507
|
+
...usage === void 0 ? {} : { usage }
|
|
7508
|
+
};
|
|
7509
|
+
}
|
|
7510
|
+
/** Parse the optional `usage` member; an unreadable one is a malformed body. */
|
|
7511
|
+
function parseUsage(value) {
|
|
7512
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
7513
|
+
const raw = value;
|
|
7514
|
+
const inputTokens = raw.input_tokens === void 0 ? void 0 : finite(raw.input_tokens);
|
|
7515
|
+
if (raw.input_tokens !== void 0 && inputTokens === void 0) return void 0;
|
|
7516
|
+
const outputTokens = raw.output_tokens === void 0 ? void 0 : finite(raw.output_tokens);
|
|
7517
|
+
if (raw.output_tokens !== void 0 && outputTokens === void 0) return void 0;
|
|
7518
|
+
return {
|
|
7519
|
+
...inputTokens === void 0 ? {} : { inputTokens },
|
|
7520
|
+
...outputTokens === void 0 ? {} : { outputTokens }
|
|
7521
|
+
};
|
|
7522
|
+
}
|
|
7523
|
+
/** Validate the outgoing request where the server would only reject it with a 400. */
|
|
7524
|
+
function assertRequest(request) {
|
|
7525
|
+
const ids = Object.keys(request.questions);
|
|
7526
|
+
if (ids.length === 0) throw new SystemOneError("a decision request needs at least one question", "invalid-request");
|
|
7527
|
+
if (ids.length > 20) throw new SystemOneError(`a decision request carries at most 20 questions (got ${ids.length})`, "invalid-request");
|
|
7528
|
+
for (const id of ids) if (id.trim() === "") throw new SystemOneError("every decision question needs a non-empty id", "invalid-request");
|
|
7529
|
+
}
|
|
7530
|
+
/** Read the endpoint's error body into a one-line diagnosis. */
|
|
7531
|
+
async function httpFailure(response) {
|
|
7532
|
+
let detail = "";
|
|
7533
|
+
try {
|
|
7534
|
+
const slice = (await response.text()).trim().slice(0, 200);
|
|
7535
|
+
if (slice !== "") detail = `: ${slice}`;
|
|
7536
|
+
} catch {}
|
|
7537
|
+
return new SystemOneError(`Command Code decision request failed (HTTP ${response.status})${detail}`, "http", response.status);
|
|
7538
|
+
}
|
|
7539
|
+
/**
|
|
7540
|
+
* Run one decision request.
|
|
7541
|
+
*
|
|
7542
|
+
* @param deps - connection facts, credential chain and fetch seam.
|
|
7543
|
+
* @param request - the state and typed questions.
|
|
7544
|
+
* @param options - caller cancellation and the per-call budget.
|
|
7545
|
+
* @returns the parsed decision.
|
|
7546
|
+
* @throws {SystemOneError} on every failure path; callers fail closed.
|
|
7547
|
+
*/
|
|
7548
|
+
async function runSystemOne(deps, request, options = {}) {
|
|
7549
|
+
assertRequest(request);
|
|
7550
|
+
const apiBase = deps.apiBase();
|
|
7551
|
+
if (!URL.canParse(apiBase)) throw new SystemOneError(`Command Code decision request is misconfigured: apiBase ${JSON.stringify(apiBase)} is not a valid URL`, "invalid-request");
|
|
7552
|
+
if (options.signal?.aborted === true) throw new SystemOneError("decision request aborted", "aborted");
|
|
7553
|
+
const timeoutMs = options.timeoutMs ?? 1500;
|
|
7554
|
+
const deadline = deadlineOf(options.signal, timeoutMs);
|
|
7555
|
+
try {
|
|
7556
|
+
let key;
|
|
7557
|
+
try {
|
|
7558
|
+
key = await withinDeadline(Promise.resolve().then(() => deps.resolveApiKey()), deadline, timeoutMs);
|
|
7559
|
+
} catch (error) {
|
|
7560
|
+
if (error instanceof SystemOneError && (error.kind === "aborted" || error.kind === "timeout")) throw error;
|
|
7561
|
+
throw new SystemOneError(`Command Code decision request has no usable credential: ${error instanceof Error ? error.message : String(error)}`, "credential", void 0, { cause: error });
|
|
7562
|
+
}
|
|
7563
|
+
const body = {
|
|
7564
|
+
model: request.model ?? "typesafe/jev",
|
|
7565
|
+
state: request.state,
|
|
7566
|
+
questions: request.questions
|
|
7567
|
+
};
|
|
7568
|
+
if (deadline.signal.aborted) throw deadline.callerAborted() ? new SystemOneError("decision request aborted", "aborted") : new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout");
|
|
7569
|
+
let response;
|
|
7570
|
+
try {
|
|
7571
|
+
response = await (deps.fetchImpl ?? fetch)(`${apiBase.replace(/\/$/, "")}${SYSTEMONE_ROUTE}`, {
|
|
7572
|
+
method: "POST",
|
|
7573
|
+
headers: {
|
|
7574
|
+
"Content-Type": "application/json",
|
|
7575
|
+
Authorization: `Bearer ${key}`,
|
|
7576
|
+
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
7577
|
+
...attributionHeaders()
|
|
7578
|
+
},
|
|
7579
|
+
body: JSON.stringify(body),
|
|
7580
|
+
signal: deadline.signal
|
|
7581
|
+
});
|
|
7582
|
+
} catch (error) {
|
|
7583
|
+
if (deadline.callerAborted()) throw new SystemOneError("decision request aborted", "aborted", void 0, { cause: error });
|
|
7584
|
+
if (deadline.timedOut() || isAbortError(error)) throw new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout", void 0, { cause: error });
|
|
7585
|
+
throw new SystemOneError(`Command Code decision request failed: ${error instanceof Error ? error.message : String(error)}`, "network", void 0, { cause: error });
|
|
7586
|
+
}
|
|
7587
|
+
if (!response.ok) throw await httpFailure(response);
|
|
7588
|
+
let payload;
|
|
7589
|
+
try {
|
|
7590
|
+
payload = await response.json();
|
|
7591
|
+
} catch (error) {
|
|
7592
|
+
if (deadline.callerAborted()) throw new SystemOneError("decision request aborted", "aborted", void 0, { cause: error });
|
|
7593
|
+
throw new SystemOneError("Command Code returned an unparseable decision body", "invalid-response", void 0, { cause: error });
|
|
7594
|
+
}
|
|
7595
|
+
const parsed = parseSystemOneResponse(payload);
|
|
7596
|
+
if (parsed === void 0) throw new SystemOneError("Command Code returned a decision body this plugin cannot read", "invalid-response");
|
|
7597
|
+
return parsed;
|
|
7598
|
+
} finally {
|
|
7599
|
+
deadline.dispose();
|
|
7600
|
+
}
|
|
7601
|
+
}
|
|
7602
|
+
//#endregion
|
|
7603
|
+
//#region src/command-guard.ts
|
|
7604
|
+
/** Tool names whose `arguments.command` this guard judges. */
|
|
7605
|
+
const COMMAND_GUARD_SHELL_TOOLS = ["bash", "pwsh"];
|
|
7606
|
+
/** Question id the safety verdict is keyed by. */
|
|
7607
|
+
const COMMAND_GUARD_QUESTION_ID = "safe";
|
|
7608
|
+
/** Decision budget; the approval prompt must not wait longer than this. */
|
|
7609
|
+
const COMMAND_GUARD_DEFAULT_TIMEOUT_MS = SYSTEMONE_DEFAULT_TIMEOUT_MS;
|
|
7610
|
+
/** Bounds of the configurable probability, mirrored by the settings schemas. */
|
|
7611
|
+
const COMMAND_GUARD_MIN_THRESHOLD = .5;
|
|
7612
|
+
const COMMAND_GUARD_MAX_TIMEOUT_MS = 1e4;
|
|
7613
|
+
/**
|
|
7614
|
+
* Longest command the guard will send for judgement. A longer one is delegated
|
|
7615
|
+
* instead of truncated: a verdict on the first N characters of a command is a
|
|
7616
|
+
* verdict on a different command.
|
|
7617
|
+
*/
|
|
7618
|
+
const COMMAND_GUARD_MAX_COMMAND_CHARS = 6e3;
|
|
7619
|
+
/**
|
|
7620
|
+
* Hard denylist: commands the guard never auto-approves, and never even sends
|
|
7621
|
+
* for judgement. Each entry is a deliberate, narrow reading of one destructive
|
|
7622
|
+
* family — the goal is not to classify every dangerous command (that is the
|
|
7623
|
+
* model's job) but to make sure the classics cannot be talked into a grant by a
|
|
7624
|
+
* persuasive-looking command string.
|
|
7625
|
+
*
|
|
7626
|
+
* Every pattern is allowed to OVER-match: a false positive costs one ordinary
|
|
7627
|
+
* human prompt, while a false negative would hand a destructive command to a
|
|
7628
|
+
* model that has to guess. That asymmetry is why `sudo` is matched anywhere in
|
|
7629
|
+
* the line rather than only in command position, and why `fdisk` is listed even
|
|
7630
|
+
* though `fdisk -l` only lists partitions.
|
|
7631
|
+
*/
|
|
7632
|
+
const COMMAND_GUARD_DENY_LIST = [
|
|
7633
|
+
{
|
|
7634
|
+
id: "rm-root",
|
|
7635
|
+
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|$)/
|
|
7636
|
+
},
|
|
7637
|
+
{
|
|
7638
|
+
id: "rm-home",
|
|
7639
|
+
pattern: /\brm\s+-[a-zA-Z]*[rR][a-zA-Z]*\s+(?:~|\$HOME)\//
|
|
7640
|
+
},
|
|
7641
|
+
{
|
|
7642
|
+
id: "filesystem-write",
|
|
7643
|
+
pattern: /\b(?:mkfs(?:\.\w+)?|wipefs|fdisk|parted|shred)\b/
|
|
7644
|
+
},
|
|
7645
|
+
{
|
|
7646
|
+
id: "raw-device-write",
|
|
7647
|
+
pattern: /\bdd\b[^\n]*\bof=\/dev\//
|
|
7648
|
+
},
|
|
7649
|
+
{
|
|
7650
|
+
id: "device-redirect",
|
|
7651
|
+
pattern: /(?:^|[^0-9\w])>{1,2}\s*\/dev\/(?:sd|disk|nvme|hd)/
|
|
7652
|
+
},
|
|
7653
|
+
{
|
|
7654
|
+
id: "fork-bomb",
|
|
7655
|
+
pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/
|
|
7656
|
+
},
|
|
7657
|
+
{
|
|
7658
|
+
id: "privilege-escalation",
|
|
7659
|
+
pattern: /\b(?:sudo|doas)\b/
|
|
7660
|
+
},
|
|
7661
|
+
{
|
|
7662
|
+
id: "su",
|
|
7663
|
+
pattern: /\bsu\s/
|
|
7664
|
+
},
|
|
7665
|
+
{
|
|
7666
|
+
id: "chmod-world-write",
|
|
7667
|
+
pattern: /\bchmod\b[^\n]*\b(?:777|a\+w|o\+w)\b/
|
|
7668
|
+
},
|
|
7669
|
+
{
|
|
7670
|
+
id: "chown-root",
|
|
7671
|
+
pattern: /\bchown\b[^\n]*\b(?:root|0):/
|
|
7672
|
+
},
|
|
7673
|
+
{
|
|
7674
|
+
id: "git-push",
|
|
7675
|
+
pattern: /\bgit\s+push\b/
|
|
7676
|
+
},
|
|
7677
|
+
{
|
|
7678
|
+
id: "git-history-rewrite",
|
|
7679
|
+
pattern: /\bgit\s+(?:filter-branch|filter-repo)\b/
|
|
7680
|
+
},
|
|
7681
|
+
{
|
|
7682
|
+
id: "git-hard-reset",
|
|
7683
|
+
pattern: /\bgit\s+reset\s+--hard\b/
|
|
7684
|
+
},
|
|
7685
|
+
{
|
|
7686
|
+
id: "git-clean-force",
|
|
7687
|
+
pattern: /\bgit\s+clean\b[^\n]*\s-[a-zA-Z]*f/
|
|
7688
|
+
},
|
|
7689
|
+
{
|
|
7690
|
+
id: "git-discard-worktree",
|
|
7691
|
+
pattern: /\bgit\s+(?:checkout|restore)\s+(?:--\s+)?\.(?:\s|$)/
|
|
7692
|
+
},
|
|
7693
|
+
{
|
|
7694
|
+
id: "git-stash-drop",
|
|
7695
|
+
pattern: /\bgit\s+stash\s+(?:drop|clear)\b/
|
|
7696
|
+
},
|
|
7697
|
+
{
|
|
7698
|
+
id: "publish",
|
|
7699
|
+
pattern: /\b(?:npm|pnpm|yarn|bun)\s+publish\b/
|
|
7700
|
+
},
|
|
7701
|
+
{
|
|
7702
|
+
id: "release-upload",
|
|
7703
|
+
pattern: /\b(?:gh\s+release|twine\s+upload)\b/
|
|
7704
|
+
},
|
|
7705
|
+
{
|
|
7706
|
+
id: "docker-push",
|
|
7707
|
+
pattern: /\bdocker\s+push\b/
|
|
7708
|
+
},
|
|
7709
|
+
{
|
|
7710
|
+
id: "cloud-upload",
|
|
7711
|
+
pattern: /\b(?:aws\s+s3|gsutil|gcloud\s+storage|az\s+storage)\b[^\n]*\b(?:cp|sync|mv|rm|rb)\b/
|
|
7712
|
+
},
|
|
7713
|
+
{
|
|
7714
|
+
id: "remote-copy",
|
|
7715
|
+
pattern: /\b(?:scp|rsync)\b[^\n]*(?::|@)/
|
|
7716
|
+
},
|
|
7717
|
+
{
|
|
7718
|
+
id: "pipe-to-shell",
|
|
7719
|
+
pattern: /\b(?:curl|wget|fetch)\b[^\n|]*\|\s*(?:sudo\s+)?(?:sh|bash|zsh|dash|ksh|python\d*|node|ruby|perl)\b/
|
|
7720
|
+
},
|
|
7721
|
+
{
|
|
7722
|
+
id: "power",
|
|
7723
|
+
pattern: /\b(?:shutdown|reboot|halt|poweroff)\b/
|
|
7724
|
+
},
|
|
7725
|
+
{
|
|
7726
|
+
id: "kill-everything",
|
|
7727
|
+
pattern: /\bkill\s+(?:-9\s+)?-1\b|\b(?:killall|pkill)\s+-9\b/
|
|
7728
|
+
},
|
|
7729
|
+
{
|
|
7730
|
+
id: "credential-write",
|
|
7731
|
+
pattern: /(?:>{1,2}|tee|cp|mv|rm|chmod|chown)\s[^\n]*(?:\.ssh\b|\.aws\b|\.gnupg\b|\.commandcode\/auth\.json|id_rsa|id_ed25519)/
|
|
7732
|
+
},
|
|
7733
|
+
{
|
|
7734
|
+
id: "history-clear",
|
|
7735
|
+
pattern: /\bhistory\s+-c\b|\bunset\s+HISTFILE\b/
|
|
7736
|
+
},
|
|
7737
|
+
{
|
|
7738
|
+
id: "global-install",
|
|
7739
|
+
pattern: /\b(?:npm|pnpm|yarn|bun)\s+(?:i|install|add)\s+(?:-g\b|--global\b)/
|
|
7740
|
+
},
|
|
7741
|
+
{
|
|
7742
|
+
id: "system-install",
|
|
7743
|
+
pattern: /\b(?:apt|apt-get|dnf|yum|zypper|apk|pacman|brew)\s+(?:install|remove|purge|uninstall|upgrade)\b/
|
|
7744
|
+
},
|
|
7745
|
+
{
|
|
7746
|
+
id: "language-install",
|
|
7747
|
+
pattern: /\b(?:pip3?|pipx|gem|cargo)\s+(?:install|add)\b/
|
|
7748
|
+
}
|
|
7749
|
+
];
|
|
7750
|
+
/**
|
|
7751
|
+
* Why the guard refuses to judge a command, when it does.
|
|
7752
|
+
*
|
|
7753
|
+
* @param command - the raw command text.
|
|
7754
|
+
* @returns the matching denylist id, or undefined.
|
|
7755
|
+
*/
|
|
7756
|
+
function denyReasonOf(command) {
|
|
7757
|
+
for (const entry of COMMAND_GUARD_DENY_LIST) if (entry.pattern.test(command)) return entry.id;
|
|
7758
|
+
}
|
|
7759
|
+
/** A non-empty string member, or undefined. */
|
|
7760
|
+
function stringMember(value, key) {
|
|
7761
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
7762
|
+
const member = value[key];
|
|
7763
|
+
return typeof member === "string" && member.trim() !== "" ? member : void 0;
|
|
7764
|
+
}
|
|
7765
|
+
/**
|
|
7766
|
+
* Read a shell command out of a tool execution, or undefined when this call is
|
|
7767
|
+
* not one the guard judges (a different tool, or arguments without a command).
|
|
7768
|
+
*
|
|
7769
|
+
* @param name - the executed tool's name.
|
|
7770
|
+
* @param args - the execution's parsed `arguments`.
|
|
7771
|
+
* @returns the fact to judge, without the asker's reason (that arrives later).
|
|
7772
|
+
*/
|
|
7773
|
+
function shellCommandOf(name, args) {
|
|
7774
|
+
if (!COMMAND_GUARD_SHELL_TOOLS.includes(name)) return void 0;
|
|
7775
|
+
const command = stringMember(args, "command");
|
|
7776
|
+
if (command === void 0) return void 0;
|
|
7777
|
+
const description = stringMember(args, "description");
|
|
7778
|
+
const workdir = stringMember(args, "workdir");
|
|
7779
|
+
const sandboxPermissions = stringMember(args, "sandbox_permissions");
|
|
7780
|
+
return {
|
|
7781
|
+
tool: name,
|
|
7782
|
+
command,
|
|
7783
|
+
...description === void 0 ? {} : { description },
|
|
7784
|
+
...workdir === void 0 ? {} : { workdir },
|
|
7785
|
+
...sandboxPermissions === void 0 ? {} : { sandboxPermissions }
|
|
7786
|
+
};
|
|
7787
|
+
}
|
|
7788
|
+
/**
|
|
7789
|
+
* Build the decision request for one command.
|
|
7790
|
+
*
|
|
7791
|
+
* The question is deliberately one `noul`: a single probability is a decision
|
|
7792
|
+
* rule that can be explained, thresholded and logged in one line, and the
|
|
7793
|
+
* criteria carry the whole distinction between "reads, or writes reversibly
|
|
7794
|
+
* inside the project" and "everything else". The agent's own `description` is
|
|
7795
|
+
* included as context, but the criteria say the command text itself decides — a
|
|
7796
|
+
* model-written description is a claim, not evidence.
|
|
7797
|
+
*
|
|
7798
|
+
* @param fact - the command and its context.
|
|
7799
|
+
* @returns the request to send.
|
|
7800
|
+
*/
|
|
7801
|
+
function buildSafetyRequest(fact) {
|
|
7802
|
+
const lines = [
|
|
7803
|
+
`Tool: ${fact.tool}`,
|
|
7804
|
+
"Command:",
|
|
7805
|
+
fact.command,
|
|
7806
|
+
...fact.description === void 0 ? [] : ["", `The agent describes this command as: ${fact.description}`],
|
|
7807
|
+
...fact.workdir === void 0 ? [] : ["", `Working directory: ${fact.workdir}`],
|
|
7808
|
+
...fact.sandboxPermissions === void 0 ? [] : ["", `The harness is asking because this call requests a wider sandbox mode (${fact.sandboxPermissions}) than the one currently in force.`],
|
|
7809
|
+
...fact.reason === void 0 ? [] : ["", `The asker's reason for needing approval: ${fact.reason}`]
|
|
7810
|
+
];
|
|
7811
|
+
const question = {
|
|
7812
|
+
type: "noul",
|
|
7813
|
+
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.",
|
|
7814
|
+
criteria: {
|
|
7815
|
+
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.",
|
|
7816
|
+
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."
|
|
7817
|
+
}
|
|
7818
|
+
};
|
|
7819
|
+
return {
|
|
7820
|
+
state: lines.join("\n"),
|
|
7821
|
+
questions: { [COMMAND_GUARD_QUESTION_ID]: question }
|
|
7822
|
+
};
|
|
7823
|
+
}
|
|
7824
|
+
/**
|
|
7825
|
+
* Read the safety probability out of a decision response.
|
|
7826
|
+
*
|
|
7827
|
+
* @param response - the parsed response.
|
|
7828
|
+
* @param questionId - the question id to read (defaults to the guard's own).
|
|
7829
|
+
* @returns the probability of "safe", or undefined when the answer is missing
|
|
7830
|
+
* or is not a `noul` verdict.
|
|
7831
|
+
*/
|
|
7832
|
+
function safeProbabilityOf(response, questionId = COMMAND_GUARD_QUESTION_ID) {
|
|
7833
|
+
const answer = response.answers[questionId];
|
|
7834
|
+
return answer !== void 0 && answer.type === "noul" ? answer.noul : void 0;
|
|
7835
|
+
}
|
|
7836
|
+
/**
|
|
7837
|
+
* One-line, length-bounded excerpt of a command for the log. The full text is
|
|
7838
|
+
* already in the session's tool call and was already sent to the decision
|
|
7839
|
+
* endpoint, so this adds no exposure — it is what makes "why did this run
|
|
7840
|
+
* without asking?" answerable from the log alone.
|
|
7841
|
+
*/
|
|
7842
|
+
function commandExcerpt(command, limit = 160) {
|
|
7843
|
+
const flat = command.replace(/\s+/g, " ").trim();
|
|
7844
|
+
return flat.length <= limit ? flat : `${flat.slice(0, limit - 1)}…`;
|
|
7845
|
+
}
|
|
7846
|
+
/**
|
|
7847
|
+
* The guard's stateful half: what was executed (by call id) and what has
|
|
7848
|
+
* already been decided (by agent and complete decision context), both bounded.
|
|
7849
|
+
*
|
|
7850
|
+
* The execution cache is the bridge between the two seams — `tools/pre-execute`
|
|
7851
|
+
* knows the arguments, `approval/request` knows the call id — and it is keyed by
|
|
7852
|
+
* call id with an agent-identity check when read: an entry for an execution
|
|
7853
|
+
* that never asked is never read, so there is nothing to clean up on the result
|
|
7854
|
+
* side (and the ask happens before the result anyway).
|
|
7855
|
+
*/
|
|
7856
|
+
var CommandGuard = class {
|
|
7857
|
+
deps;
|
|
7858
|
+
executions = /* @__PURE__ */ new Map();
|
|
7859
|
+
decisions = /* @__PURE__ */ new WeakMap();
|
|
7860
|
+
constructor(deps) {
|
|
7861
|
+
this.deps = deps;
|
|
7862
|
+
}
|
|
7863
|
+
/**
|
|
7864
|
+
* Remember one execution's command. Never throws, never changes a decision.
|
|
7865
|
+
*
|
|
7866
|
+
* @param execution - the execution about to run.
|
|
7867
|
+
*/
|
|
7868
|
+
noteExecution(execution) {
|
|
7869
|
+
const callId = execution?.callId;
|
|
7870
|
+
if (typeof callId !== "string" || callId === "") return;
|
|
7871
|
+
const fact = shellCommandOf(execution.name, execution.arguments);
|
|
7872
|
+
if (fact === void 0) return;
|
|
7873
|
+
this.executions.delete(callId);
|
|
7874
|
+
this.executions.set(callId, {
|
|
7875
|
+
fact,
|
|
7876
|
+
agent: execution.agent
|
|
7877
|
+
});
|
|
7878
|
+
while (this.executions.size > 64) {
|
|
7879
|
+
const oldest = this.executions.keys().next();
|
|
7880
|
+
if (oldest.done === true) break;
|
|
7881
|
+
this.executions.delete(oldest.value);
|
|
7882
|
+
}
|
|
7883
|
+
}
|
|
7884
|
+
/** Forget every remembered execution and decision (disposal, tests). */
|
|
7885
|
+
clear() {
|
|
7886
|
+
this.executions.clear();
|
|
7887
|
+
this.decisions = /* @__PURE__ */ new WeakMap();
|
|
7888
|
+
}
|
|
7889
|
+
/**
|
|
7890
|
+
* Judge one approval request.
|
|
7891
|
+
*
|
|
7892
|
+
* @param request - the pending approval.
|
|
7893
|
+
* @returns the verdict; `approve` is the only outcome that grants anything.
|
|
7894
|
+
*/
|
|
7895
|
+
async judge(request) {
|
|
7896
|
+
const settings = this.deps.settings();
|
|
7897
|
+
if (!settings.enabled) return {
|
|
7898
|
+
kind: "delegate",
|
|
7899
|
+
reason: "guard disabled"
|
|
7900
|
+
};
|
|
7901
|
+
const callId = request.callId;
|
|
7902
|
+
if (callId === void 0) return {
|
|
7903
|
+
kind: "delegate",
|
|
7904
|
+
reason: "approval carries no call id"
|
|
7905
|
+
};
|
|
7906
|
+
const observed = this.executions.get(callId);
|
|
7907
|
+
if (observed === void 0 || observed.agent !== request.agent) return {
|
|
7908
|
+
kind: "delegate",
|
|
7909
|
+
reason: "the guard never saw this call for this agent"
|
|
7910
|
+
};
|
|
7911
|
+
const fact = observed.fact;
|
|
7912
|
+
const excerpt = commandExcerpt(fact.command);
|
|
7913
|
+
if (!COMMAND_GUARD_SHELL_TOOLS.includes(request.toolName)) return {
|
|
7914
|
+
kind: "delegate",
|
|
7915
|
+
reason: `not a shell tool (${request.toolName})`
|
|
7916
|
+
};
|
|
7917
|
+
if (fact.command.length > 6e3) return {
|
|
7918
|
+
kind: "delegate",
|
|
7919
|
+
reason: `command longer than ${COMMAND_GUARD_MAX_COMMAND_CHARS} characters`,
|
|
7920
|
+
command: excerpt
|
|
7921
|
+
};
|
|
7922
|
+
const denied = denyReasonOf(fact.command);
|
|
7923
|
+
if (denied !== void 0) return {
|
|
7924
|
+
kind: "delegate",
|
|
7925
|
+
reason: `denylisted command (${denied})`,
|
|
7926
|
+
command: excerpt
|
|
7927
|
+
};
|
|
7928
|
+
const full = {
|
|
7929
|
+
...fact,
|
|
7930
|
+
...request.reason === void 0 ? {} : { reason: request.reason }
|
|
7931
|
+
};
|
|
7932
|
+
const memoKey = JSON.stringify(full);
|
|
7933
|
+
const now = this.deps.now?.() ?? Date.now();
|
|
7934
|
+
const agentDecisions = request.agent === void 0 ? void 0 : this.decisions.get(request.agent);
|
|
7935
|
+
const memo = agentDecisions?.get(memoKey);
|
|
7936
|
+
if (memo !== void 0) {
|
|
7937
|
+
if (now - memo.at < 6e5) return this.verdict(memo.probability, settings.threshold, "memo", excerpt);
|
|
7938
|
+
agentDecisions?.delete(memoKey);
|
|
7939
|
+
}
|
|
7940
|
+
let response;
|
|
7941
|
+
try {
|
|
7942
|
+
response = await this.deps.decide(buildSafetyRequest(full), {
|
|
7943
|
+
...request.signal === void 0 ? {} : { signal: request.signal },
|
|
7944
|
+
timeoutMs: settings.timeoutMs
|
|
7945
|
+
});
|
|
7946
|
+
} catch (error) {
|
|
7947
|
+
return {
|
|
7948
|
+
kind: "delegate",
|
|
7949
|
+
reason: `decision unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
7950
|
+
command: excerpt
|
|
7951
|
+
};
|
|
7952
|
+
}
|
|
7953
|
+
const probability = safeProbabilityOf(response);
|
|
7954
|
+
if (probability === void 0) return {
|
|
7955
|
+
kind: "delegate",
|
|
7956
|
+
reason: "the decision carried no safe verdict",
|
|
7957
|
+
command: excerpt
|
|
7958
|
+
};
|
|
7959
|
+
if (request.agent !== void 0) {
|
|
7960
|
+
const decisions = agentDecisions ?? /* @__PURE__ */ new Map();
|
|
7961
|
+
decisions.set(memoKey, {
|
|
7962
|
+
probability,
|
|
7963
|
+
at: now
|
|
7964
|
+
});
|
|
7965
|
+
while (decisions.size > 64) {
|
|
7966
|
+
const oldest = decisions.keys().next();
|
|
7967
|
+
if (oldest.done === true) break;
|
|
7968
|
+
decisions.delete(oldest.value);
|
|
7969
|
+
}
|
|
7970
|
+
this.decisions.set(request.agent, decisions);
|
|
7971
|
+
}
|
|
7972
|
+
return this.verdict(probability, settings.threshold, "model", excerpt);
|
|
7973
|
+
}
|
|
7974
|
+
/** Apply the threshold to one probability. */
|
|
7975
|
+
verdict(probability, threshold, source, command) {
|
|
7976
|
+
return probability >= threshold ? {
|
|
7977
|
+
kind: "approve",
|
|
7978
|
+
probability,
|
|
7979
|
+
source,
|
|
7980
|
+
command
|
|
7981
|
+
} : {
|
|
7982
|
+
kind: "delegate",
|
|
7983
|
+
reason: `safe probability ${probability.toFixed(3)} below ${threshold}`,
|
|
7984
|
+
probability,
|
|
7985
|
+
command
|
|
7986
|
+
};
|
|
7987
|
+
}
|
|
7988
|
+
};
|
|
7989
|
+
/**
|
|
7990
|
+
* Register the guard on a context that has the approval seam.
|
|
7991
|
+
*
|
|
7992
|
+
* Call this from inside `ctx.inject(['approval'], …)`: the seam's presence is
|
|
7993
|
+
* what makes the feature meaningful (no approval service means no asks), and
|
|
7994
|
+
* tying the listeners to that injected fiber is what makes them go away with it.
|
|
7995
|
+
*
|
|
7996
|
+
* @param ctx - the injected context (or a test double with `on`).
|
|
7997
|
+
* @param deps - settings, the decision client, clock and logger.
|
|
7998
|
+
* @returns a disposer for both listeners.
|
|
7999
|
+
*/
|
|
8000
|
+
function applyCommandGuard(ctx, deps) {
|
|
8001
|
+
const guard = new CommandGuard(deps);
|
|
8002
|
+
const events = ctx;
|
|
8003
|
+
const offPreExecute = events.on("tools/pre-execute", (execution, next) => {
|
|
8004
|
+
guard.noteExecution(execution);
|
|
8005
|
+
return next();
|
|
8006
|
+
}, { prepend: true });
|
|
8007
|
+
const offApproval = events.on("approval/request", async (request, next) => {
|
|
8008
|
+
try {
|
|
8009
|
+
if (!deps.settings().enabled) return next();
|
|
8010
|
+
} catch (error) {
|
|
8011
|
+
deps.log?.(`command guard failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
8012
|
+
return next();
|
|
8013
|
+
}
|
|
8014
|
+
let verdict;
|
|
8015
|
+
try {
|
|
8016
|
+
verdict = await guard.judge(request);
|
|
8017
|
+
} catch (error) {
|
|
8018
|
+
deps.log?.(`command guard failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
8019
|
+
return next();
|
|
8020
|
+
}
|
|
8021
|
+
if (verdict.kind === "approve") {
|
|
8022
|
+
deps.log?.(`command guard auto-approved a ${request.toolName} call (safe probability ${verdict.probability.toFixed(3)}, ${verdict.source}): ${verdict.command ?? "(command unknown)"}`);
|
|
8023
|
+
return "allowed-once";
|
|
8024
|
+
}
|
|
8025
|
+
deps.log?.(`command guard delegated a ${request.toolName} call: ${verdict.reason}${verdict.command === void 0 ? "" : ` — ${verdict.command}`}`);
|
|
8026
|
+
return next();
|
|
8027
|
+
}, { prepend: true });
|
|
8028
|
+
return () => {
|
|
8029
|
+
guard.clear();
|
|
8030
|
+
for (const off of [offPreExecute, offApproval]) if (typeof off === "function") off();
|
|
8031
|
+
};
|
|
8032
|
+
}
|
|
8033
|
+
//#endregion
|
|
7078
8034
|
//#region src/config-volatile.ts
|
|
7079
8035
|
/**
|
|
7080
8036
|
* Mark one schema field volatile when the installed schemastery supports it.
|
|
@@ -7219,6 +8175,10 @@ function configFields() {
|
|
|
7219
8175
|
models: z.array(z.string()),
|
|
7220
8176
|
account: z.string()
|
|
7221
8177
|
})),
|
|
8178
|
+
commandGuard: z.boolean().default(false),
|
|
8179
|
+
commandGuardThreshold: z.number().min(COMMAND_GUARD_MIN_THRESHOLD).max(1),
|
|
8180
|
+
commandGuardTimeoutMs: z.number().min(200).max(COMMAND_GUARD_MAX_TIMEOUT_MS),
|
|
8181
|
+
zdr: z.boolean().default(false),
|
|
7222
8182
|
lang: z.string().pattern(/^(zh|en)$/).default("zh")
|
|
7223
8183
|
};
|
|
7224
8184
|
}
|
|
@@ -7267,7 +8227,8 @@ function resolveAdapterOptions(config) {
|
|
|
7267
8227
|
streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
|
|
7268
8228
|
filterModelsByPlan: config.filterModelsByPlan ?? true,
|
|
7269
8229
|
visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : void 0,
|
|
7270
|
-
modelVisibility: readModelVisibility(config.modelVisibility)
|
|
8230
|
+
modelVisibility: readModelVisibility(config.modelVisibility),
|
|
8231
|
+
zdr: config.zdr === true
|
|
7271
8232
|
};
|
|
7272
8233
|
}
|
|
7273
8234
|
/**
|
|
@@ -7481,6 +8442,26 @@ function apply(ctx, config) {
|
|
|
7481
8442
|
applyCommandCodeSearchSelection(webCtx.web, searchSelection, false);
|
|
7482
8443
|
}, "dsh-commandcode-provider: web search selection");
|
|
7483
8444
|
});
|
|
8445
|
+
const commandGuardSettings = () => {
|
|
8446
|
+
const raw = current();
|
|
8447
|
+
return {
|
|
8448
|
+
enabled: raw.commandGuard === true,
|
|
8449
|
+
threshold: raw.commandGuardThreshold ?? .9,
|
|
8450
|
+
timeoutMs: raw.commandGuardTimeoutMs ?? COMMAND_GUARD_DEFAULT_TIMEOUT_MS
|
|
8451
|
+
};
|
|
8452
|
+
};
|
|
8453
|
+
ctx.inject(["approval"], (approvalCtx) => {
|
|
8454
|
+
applyCommandGuard(approvalCtx, {
|
|
8455
|
+
settings: commandGuardSettings,
|
|
8456
|
+
decide: (request, decideOptions) => runSystemOne({
|
|
8457
|
+
apiBase: () => options().apiBase,
|
|
8458
|
+
resolveApiKey: () => resolveApiKey(options())
|
|
8459
|
+
}, request, decideOptions),
|
|
8460
|
+
log: (message) => {
|
|
8461
|
+
ctx.logger.info(`llm-commandcode: ${message}`);
|
|
8462
|
+
}
|
|
8463
|
+
});
|
|
8464
|
+
});
|
|
7484
8465
|
let refreshTuiSettings;
|
|
7485
8466
|
ctx.inject(["tuiSettingsSections"], (tuiCtx) => {
|
|
7486
8467
|
refreshTuiSettings = applyCommandCodeTuiSettings(tuiCtx, {
|
|
@@ -7530,6 +8511,6 @@ function apply(ctx, config) {
|
|
|
7530
8511
|
}, "dsh-commandcode-provider: volatile config updates");
|
|
7531
8512
|
}
|
|
7532
8513
|
//#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 };
|
|
8514
|
+
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
8515
|
|
|
7535
8516
|
//# sourceMappingURL=index.js.map
|