@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/CHANGELOG.md +24 -1
- package/README.md +36 -6
- package/README.zh-CN.md +36 -6
- package/icon.svg +1 -5
- package/lib/client.js +244 -63
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +104 -11
- package/lib/index.js +1108 -58
- 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 ?? "",
|
|
@@ -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.
|
|
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
|
-
|
|
3846
|
-
if (
|
|
3847
|
-
if (
|
|
3848
|
-
if (
|
|
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
|
|
4229
|
-
* back without a record; only a
|
|
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
|
-
|
|
4243
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
6074
|
-
if (this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7441
|
-
|
|
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
|