dsh-tap 0.20.0

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.
Files changed (121) hide show
  1. package/AGENTS.md +99 -0
  2. package/CHANGELOG.md +751 -0
  3. package/LICENSE +21 -0
  4. package/README.en.md +59 -0
  5. package/README.md +256 -0
  6. package/cordis.patch.yml +385 -0
  7. package/core/bridge.js +698 -0
  8. package/core/json-store.js +91 -0
  9. package/core/rotation.js +108 -0
  10. package/core/usage-meter.js +176 -0
  11. package/docs/diagnosis-cache-quota.md +181 -0
  12. package/docs/diagnosis-qoder-flash.md +67 -0
  13. package/docs/diagnosis-trae-3003.md +302 -0
  14. package/docs/goals/bridge-port-host-split.md +91 -0
  15. package/docs/goals/desktop-adaptation.md +106 -0
  16. package/docs/goals/qoder-cn-provider-design.md +308 -0
  17. package/docs/goals/settings-card-ux-redesign-plan.md +1680 -0
  18. package/docs/goals/settings-card-ux-redesign.md +162 -0
  19. package/docs/goals/trae-agent-v3.md +41 -0
  20. package/docs/goals/trae-work-cn-repair.md +44 -0
  21. package/docs/goals/v0.8-/351/242/235/345/272/246/345/217/257/350/247/201-/346/250/241/345/236/213/345/212/250/346/200/201/345/214/226-/345/244/232/346/234/215/345/212/241/345/225/206.md +85 -0
  22. package/docs/pitfalls.md +105 -0
  23. package/docs/reverse/trae-cloud-api.md +218 -0
  24. package/docs/reverse/trae-model-catalog.md +129 -0
  25. package/docs/reverse/traework-cn.md +520 -0
  26. package/docs/rules/STATE.md +198 -0
  27. package/docs/rules/content-moderation.md +54 -0
  28. package/docs/rules/dev-role-boundary.md +94 -0
  29. package/docs/rules/extra-providers.md +42 -0
  30. package/docs/rules/gateway-facts.md +91 -0
  31. package/docs/rules/oauth-handshake.md +76 -0
  32. package/docs/rules/prompt-cache.md +93 -0
  33. package/docs/rules/quota-signals.md +125 -0
  34. package/docs/rules/routing.md +84 -0
  35. package/docs/rules/templates/oauth-reverse-checklist.md +42 -0
  36. package/docs/rules/trae-surface.md +242 -0
  37. package/docs/rules/ua-validation.md +81 -0
  38. package/host-config.js +282 -0
  39. package/index.js +2306 -0
  40. package/lib/client.js +2893 -0
  41. package/local-scan.js +104 -0
  42. package/package.json +82 -0
  43. package/providers/ark/index.js +11 -0
  44. package/providers/bailian/index.js +10 -0
  45. package/providers/bigmodel/index.js +11 -0
  46. package/providers/codebuddy/agenttool.js +122 -0
  47. package/providers/codebuddy/catalog.js +227 -0
  48. package/providers/codebuddy/errors.js +44 -0
  49. package/providers/codebuddy/headers.js +36 -0
  50. package/providers/codebuddy/images.js +125 -0
  51. package/providers/codebuddy/index.js +123 -0
  52. package/providers/codebuddy/oauth.js +279 -0
  53. package/providers/deepseek/index.js +11 -0
  54. package/providers/moonshot/index.js +11 -0
  55. package/providers/openai-compat.js +177 -0
  56. package/providers/openrouter/index.js +27 -0
  57. package/providers/qoder/catalog.js +145 -0
  58. package/providers/qoder/cosy.js +419 -0
  59. package/providers/qoder/gateway.js +563 -0
  60. package/providers/qoder/index.js +116 -0
  61. package/providers/qoder/oauth.js +364 -0
  62. package/providers/qoder/qoder_auth.wasm +0 -0
  63. package/providers/qoder/quota.js +56 -0
  64. package/providers/qwen/index.js +15 -0
  65. package/providers/tool-pairing.js +129 -0
  66. package/providers/trae/catalog.js +103 -0
  67. package/providers/trae/errors.js +85 -0
  68. package/providers/trae/gateway.js +853 -0
  69. package/providers/trae/index.js +126 -0
  70. package/providers/trae/oauth.js +443 -0
  71. package/providers/trae/quota.js +75 -0
  72. package/providers/trae/remote.js +365 -0
  73. package/scripts/capture-cache.mjs +65 -0
  74. package/scripts/capture-traffic.mjs +83 -0
  75. package/scripts/hermes-probe-dev-role.mjs +263 -0
  76. package/scripts/measure-latency.mjs +253 -0
  77. package/scripts/probe-ark-thinking.mjs +298 -0
  78. package/scripts/probe-cache-decline.mjs +292 -0
  79. package/scripts/probe-cache-ttl.mjs +221 -0
  80. package/scripts/probe-cache.mjs +156 -0
  81. package/scripts/probe-codebuddy-efforts.mjs +355 -0
  82. package/scripts/probe-codebuddy-tier-wiring.mjs +244 -0
  83. package/scripts/probe-effort-gaps.mjs +133 -0
  84. package/scripts/probe-media.mjs +115 -0
  85. package/scripts/probe-moderation.mjs +159 -0
  86. package/scripts/probe-oauth.mjs +617 -0
  87. package/scripts/probe-qoder-attribution-arm8.mjs +92 -0
  88. package/scripts/probe-qoder-attribution-arm9.mjs +102 -0
  89. package/scripts/probe-qoder-attribution.mjs +266 -0
  90. package/scripts/probe-qoder-flash-confirm.mjs +94 -0
  91. package/scripts/probe-qoder-live.mjs +344 -0
  92. package/scripts/probe-qoder-matrix.mjs +274 -0
  93. package/scripts/probe-qoder-null-content.mjs +153 -0
  94. package/scripts/probe-qoder-pairing.mjs +308 -0
  95. package/scripts/probe-qoder-quota.mjs +162 -0
  96. package/scripts/probe-qoder-thinking-config.mjs +52 -0
  97. package/scripts/probe-qoder-thinking-efforts.mjs +269 -0
  98. package/scripts/probe-quota.mjs +136 -0
  99. package/scripts/probe-quota2.mjs +144 -0
  100. package/scripts/probe-routing.mjs +226 -0
  101. package/scripts/probe-trae-3003-diagnosis.mjs +139 -0
  102. package/scripts/probe-trae-agent-v3.mjs +399 -0
  103. package/scripts/probe-trae-efforts.mjs +149 -0
  104. package/scripts/probe-trae-live.mjs +147 -0
  105. package/scripts/probe-trae-max-effort.mjs +179 -0
  106. package/scripts/probe-trae-model-routing.mjs +381 -0
  107. package/scripts/probe-trae-thinking-scene.mjs +238 -0
  108. package/scripts/probe-trae-transport-outage.mjs +176 -0
  109. package/scripts/probe-ua.mjs +508 -0
  110. package/scripts/trae-model-catalog.mjs +632 -0
  111. package/scripts/verify-agents-md.mjs +45 -0
  112. package/scripts/verify-bridge.mjs +1041 -0
  113. package/scripts/verify-core-generic.mjs +302 -0
  114. package/scripts/verify-desktop-acceptance.mjs +184 -0
  115. package/scripts/verify-host-config.mjs +265 -0
  116. package/scripts/verify-models.mjs +390 -0
  117. package/scripts/verify-providers.mjs +255 -0
  118. package/scripts/verify-qoder-provider.mjs +1020 -0
  119. package/scripts/verify-rotation.mjs +308 -0
  120. package/scripts/verify-trae-model-catalog.mjs +284 -0
  121. package/scripts/verify-trae-provider.mjs +1451 -0
@@ -0,0 +1,508 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * UA-validation probe (topic 1 of docs/rules/): which UA fields does the
4
+ * gateway actually check, and what is the minimal mutation that triggers
5
+ * error 12403 ("check ua")?
6
+ *
7
+ * Hypotheses under test (formed 2026-08-19 from the upstream spell "UA 必须
8
+ * 是 CLI/unknown CodeBuddy/2.136.0,CodeBuddyCode/1.0 被拒 12403"):
9
+ * H1 the validator parses UA into `<name>/<version>` tokens and requires
10
+ * a product token `CodeBuddy/<x.y.z>`; other tokens are decorative
11
+ * H2 the version value itself is not validated (any semver passes)
12
+ * H3 `CodeBuddyCode/1.0` is rejected because the product NAME differs,
13
+ * not because of the version or the missing CLI prefix
14
+ * H4 companion headers (X-Product, X-IDE-*) are not checked on /v3/config
15
+ * H5 /v2/chat/completions does not validate UA at all (real-byte evidence
16
+ * already: docs/probes/bridge-flash-ab-2026-08-18.jsonl shows a
17
+ * deepseek-harness/0.1.0-rc.6 UA succeeding)
18
+ *
19
+ * Method: GET /v3/config (free, non-chat) with one mutated UA per request,
20
+ * 1.5s spacing, single account, read-only. Chat phase costs one max_tokens=1
21
+ * completion per arm (≈0.001 credit each) — disable with --skip-chat.
22
+ *
23
+ * Baseline UA strings come from shipped code / real captures, not from
24
+ * hand reconstruction: the CLI shape is what index.js sends (USER_AGENT),
25
+ * the /v2 baseline is what the bridge dump recorded on the wire.
26
+ *
27
+ * Usage: node scripts/probe-ua.mjs [--only name1,name2] [--skip-chat]
28
+ * [--out docs/probes/ua-YYYY-MM-DD.jsonl]
29
+ * Prediction round: edit the PREDICT_VARIANTS block after analysing the
30
+ * matrix, then run `node scripts/probe-ua.mjs --set predict`.
31
+ */
32
+
33
+ import { readFileSync, existsSync, writeFileSync, mkdirSync, appendFileSync } from 'node:fs'
34
+ import { homedir } from 'node:os'
35
+ import { join, dirname } from 'node:path'
36
+
37
+ const DSH_HOME = process.env.DSH_HOME ?? join(homedir(), '.dsh')
38
+ const GATEWAY = 'https://copilot.tencent.com'
39
+ const BASELINE_UA = 'CLI/unknown CodeBuddy/2.136.0' // index.js USER_AGENT
40
+ const SPACING_MS = 1500
41
+
42
+ const argValue = (flag) => {
43
+ const i = process.argv.indexOf(flag)
44
+ return i > 0 ? process.argv[i + 1] : null
45
+ }
46
+ const ONLY = argValue('--only')?.split(',') ?? null
47
+ const SET = argValue('--set') ?? 'matrix' // matrix | predict | all
48
+ const SKIP_CHAT = process.argv.includes('--skip-chat')
49
+ const OUT = argValue('--out')
50
+ ?? join('docs', 'probes', `ua-${new Date().toISOString().slice(0, 10)}.jsonl`)
51
+
52
+ // --- credential resolution: identical to the plugin's (api-key mode) -------
53
+ function resolveCredential() {
54
+ try {
55
+ const cfg = JSON.parse(readFileSync(join(DSH_HOME, 'codebuddy-plugin.json'), 'utf8'))
56
+ if (cfg.authMode === 'oauth') {
57
+ const auth = JSON.parse(readFileSync(join(DSH_HOME, 'codebuddy-plugin-auth.json'), 'utf8'))
58
+ if (auth?.auth?.accessToken) return { authorization: `Bearer ${auth.auth.accessToken}`, apiKey: null }
59
+ }
60
+ const active = (cfg.apiKeys ?? []).find((k) => k.name === cfg.activeApiKey)
61
+ if (active?.key) return { authorization: `Bearer ${active.key}`, apiKey: active.key }
62
+ } catch { /* fall through */ }
63
+ if (process.env.CODEBUDDY_API_KEY) {
64
+ return { authorization: `Bearer ${process.env.CODEBUDDY_API_KEY}`, apiKey: process.env.CODEBUDDY_API_KEY }
65
+ }
66
+ const credFile = join(DSH_HOME, '.credentials.yaml')
67
+ if (existsSync(credFile)) {
68
+ const m = readFileSync(credFile, 'utf8').match(/^\s*CODEBUDDY_API_KEY:\s*["']?([^"'\s]+)["']?\s*$/m)
69
+ if (m) return { authorization: `Bearer ${m[1]}`, apiKey: m[1] }
70
+ }
71
+ throw new Error('no CodeBuddy credential found')
72
+ }
73
+
74
+ // --- variant table -----------------------------------------------------------
75
+ // ua: null → omit the User-Agent header entirely; headers: extra/drop marks.
76
+ // dropXProduct removes the X-Product: SaaS header the catalog call normally
77
+ // carries (fetchModelCatalog dialect).
78
+ const MATRIX_VARIANTS = [
79
+ // group 0 — controls
80
+ { name: 'control-baseline', ua: BASELINE_UA, note: 'shipped UA, expect 200' },
81
+ { name: 'spell-rejected', ua: 'CodeBuddyCode/1.0', note: 'the spell says 12403' },
82
+ // group 1 — product token (H1/H3)
83
+ { name: 'product-only', ua: 'CodeBuddy/2.136.0', note: 'no CLI prefix at all' },
84
+ { name: 'product-other', ua: 'CLI/unknown OtherBuddy/2.136.0', note: 'same shape, wrong product' },
85
+ { name: 'cli-only', ua: 'CLI/unknown', note: 'no product token' },
86
+ // group 2 — version value (H2)
87
+ { name: 'version-major-bump', ua: 'CLI/unknown CodeBuddy/9.9.9', note: 'absurd future version' },
88
+ { name: 'version-patch-bump', ua: 'CLI/unknown CodeBuddy/2.136.1', note: 'patch +1' },
89
+ { name: 'version-low', ua: 'CLI/unknown CodeBuddy/0.0.1', note: 'ancient version' },
90
+ { name: 'version-two-part', ua: 'CLI/unknown CodeBuddy/2.136', note: 'not full semver' },
91
+ { name: 'version-nonnumeric', ua: 'CLI/unknown CodeBuddy/abc', note: 'not a number' },
92
+ // group 3 — prefix token shape
93
+ { name: 'ide-ver-filled', ua: 'CLI/1.0 CodeBuddy/2.136.0', note: 'unknown → 1.0' },
94
+ { name: 'ide-other', ua: 'VSC/unknown CodeBuddy/2.136.0', note: 'CLI → VSC' },
95
+ // group 4 — extra tokens / case
96
+ { name: 'suffix-added', ua: 'CLI/unknown CodeBuddy/2.136.0 Extra/1.0', note: 'trailing token' },
97
+ { name: 'case-cli-lower', ua: 'cli/unknown CodeBuddy/2.136.0', note: 'lowercase cli' },
98
+ { name: 'case-product-lower', ua: 'CLI/unknown codebuddy/2.136.0', note: 'lowercase product' },
99
+ // group 5 — companion headers (H4)
100
+ { name: 'no-xproduct', ua: BASELINE_UA, dropXProduct: true, note: 'baseline UA, X-Product dropped' },
101
+ { name: 'no-ua-header', ua: null, note: 'User-Agent omitted entirely' },
102
+ ]
103
+
104
+ // Pre-registered predictions, derived from the matrix results (2026-08-19).
105
+ // Rule candidate after the matrix: "the UA must contain the substring
106
+ // `codebuddy/` (case-insensitive); every other field is decorative".
107
+ // Evidence: product-only passed (no CLI prefix needed), CodeBuddy/abc and
108
+ // CodeBuddy/9.9.9 passed (no version check), codebuddy/2.136.0 passed
109
+ // (case-insensitive), CodeBuddyCode/1.0 FAILED though it contains
110
+ // `codebuddy` → the trailing slash must be part of the match.
111
+ // Discriminators:
112
+ // - notcodebuddy: substring rule → 200; token-exact-name rule → 12403
113
+ // - bare-product: substring-with-slash rule → 12403; substring-without-slash
114
+ // rule (`codebuddy` anywhere) → 200
115
+ const PREDICT_VARIANTS = [
116
+ { name: 'predict-notcodebuddy', ua: 'NotCodeBuddy/1.0', expect: 200, rule: 'R1', note: 'substring rule predicts pass' },
117
+ { name: 'predict-bare-product', ua: 'CodeBuddy', expect: 12403, rule: 'R1', note: 'no slash → substring absent' },
118
+ { name: 'predict-uppercase', ua: 'X/y CODEBUDDY/1', expect: 200, rule: 'R1', note: 'uppercase token, minimal version' },
119
+ ]
120
+
121
+ // Round 2 (2026-08-19): predict-uppercase MISSED (12403) while matrix
122
+ // `codebuddy/2.136.0` passed and `CodeBuddy/abc` passed — so neither full
123
+ // case-insensitivity nor a version-shape rule explains it. Refined rule
124
+ // R1′: the UA must contain one of the exact-case substrings `CodeBuddy/` or
125
+ // `codebuddy/` (the casings the official CLI / IDE plugins actually send).
126
+ // Pre-registered discriminators:
127
+ // - mixedcase-lowb / caps-b: R1′ → 12403; full-CI rule → 200
128
+ // - uppercase-fullver: isolates casing from the version `1` used in the
129
+ // missed arm — R1′ → 12403 even with the known-good version string
130
+ const PREDICT_VARIANTS_R2 = [
131
+ { name: 'predict-mixedcase-lowb', ua: 'Codebuddy/1.0', expect: 12403, rule: 'R1′', note: 'lowercase b is not whitelisted' },
132
+ { name: 'predict-caps-b', ua: 'codeBuddy/1.0', expect: 12403, rule: 'R1′', note: 'capital B only is not whitelisted' },
133
+ { name: 'predict-uppercase-fullver', ua: 'X/y CODEBUDDY/2.136.0', expect: 12403, rule: 'R1′', note: 'case isolated from version shape' },
134
+ ]
135
+
136
+ // Round 3 (2026-08-19): all R1′ predictions MISSED — Codebuddy/1.0,
137
+ // codeBuddy/1.0, CODEBUDDY/2.136.0 (with the X/y prefix!) all PASSED.
138
+ // Casing and prefix are therefore irrelevant. The only remaining difference
139
+ // in the failed arm `X/y CODEBUDDY/1` is the one-character version `1`.
140
+ // Matrix cross-check: CodeBuddy/abc (3) ✓, CodeBuddy/1.0 (3) ✓,
141
+ // CodeBuddy/2.136 (5) ✓, CodeBuddy/ (0, matrix product-only… no — bare
142
+ // `CodeBuddy` without slash failed). Candidate rule R2:
143
+ // UA must match /codebuddy\/\S{2,}/i — product token + version ≥ 2 chars.
144
+ // Pre-registered discriminators:
145
+ // - verlen1 vs verlen2: isolates the boundary exactly
146
+ // - verlen1-space: tokenized parser (\S) → 12403; raw substring (.) → 200
147
+ const PREDICT_VARIANTS_R3 = [
148
+ { name: 'predict-verlen1', ua: 'CodeBuddy/a', expect: 12403, rule: 'R2', note: 'version length 1 → reject' },
149
+ { name: 'predict-verlen2', ua: 'CodeBuddy/ab', expect: 200, rule: 'R2', note: 'version length 2 → pass' },
150
+ { name: 'predict-verlen1-space', ua: 'CodeBuddy/1 ', expect: 12403, rule: 'R2', note: 'trailing space: \\S rule rejects, . rule passes' },
151
+ ]
152
+
153
+ // Round 4 (2026-08-19): verlen1 (12403 HIT) and verlen1-space (12403 HIT)
154
+ // confirmed the lower boundary; verlen2 MISSED — `ab` (2 chars) was also
155
+ // rejected while matrix `abc` (3 chars) passed. Candidate rule R3:
156
+ // UA must match /codebuddy\/\S{3,}/i — version part ≥ 3 non-space chars.
157
+ // Pre-registered discriminators:
158
+ // - verlen3-digit: all-digit 3-char version → R3 says pass
159
+ // - verlen4: sanity confirm above the boundary
160
+ // - verlen2-trailspace: \S-rule → 12403; any-char(.{3,}) rule → 200
161
+ const PREDICT_VARIANTS_R4 = [
162
+ { name: 'predict-verlen3-digit', ua: 'CodeBuddy/111', expect: 200, rule: 'R3', note: '3-char numeric version passes' },
163
+ { name: 'predict-verlen4', ua: 'CodeBuddy/abcd', expect: 200, rule: 'R3', note: '4-char version passes' },
164
+ { name: 'predict-verlen2-trailspace', ua: 'CodeBuddy/ab ', expect: 12403, rule: 'R3', note: 'space does not count toward the 3 chars' },
165
+ ]
166
+
167
+ // Round 5 (2026-08-19): R3 collapsed — CodeBuddy/111 (3 chars) and
168
+ // CodeBuddy/abcd (4 chars) were BOTH rejected while matrix
169
+ // `CLI/unknown CodeBuddy/abc` passed. Re-tabulating all 28 observations
170
+ // yields a disjunctive rule R6 that fits every one:
171
+ // R6: UA passes iff it matches /cli\/\S+\s+codebuddy\/\S+/i (CLI form —
172
+ // any version string) OR /codebuddy\/\d+\.\d+/i (any UA carrying a
173
+ // semver-looking CodeBuddy/x.y token, dot required)
174
+ // Key fits: CLI/unknown CodeBuddy/abc ✓ (CLI form, version-free),
175
+ // CodeBuddy/abcd ✗ (no dot, no CLI prefix), X/y CODEBUDDY/1 ✗ (prefix not
176
+ // "cli", version has no dot), NotCodeBuddy/1.0 ✓ (substring match, token
177
+ // start not anchored).
178
+ // Pre-registered discriminators:
179
+ // - cli-anyver: CLI form frees the version grammar → 200 (a "version
180
+ // grammar" rule would say 12403 — standalone abcd failed)
181
+ // - cli-minimal: both \S+ slots minimal → 200
182
+ // - semver2-standalone: 2-part semver WITHOUT the CLI prefix (unobserved:
183
+ // matrix only tried 2.136 inside the CLI form) → 200
184
+ // - noncli-anyver: prefix word must be exactly CLI → 12403
185
+ // - cli-noslash: "CLI" without its slash breaks the CLI form → 12403
186
+ const PREDICT_VARIANTS_R5 = [
187
+ { name: 'predict-cli-anyver', ua: 'CLI/unknown CodeBuddy/abcd', expect: 200, rule: 'R6', note: 'CLI form frees version grammar' },
188
+ { name: 'predict-cli-minimal', ua: 'CLI/x CodeBuddy/z', expect: 200, rule: 'R6', note: 'minimal CLI form' },
189
+ { name: 'predict-semver2-standalone', ua: 'CodeBuddy/2.136', expect: 200, rule: 'R6', note: '2-part semver, no CLI prefix' },
190
+ { name: 'predict-noncli-anyver', ua: 'Wat/unknown CodeBuddy/zzz', expect: 12403, rule: 'R6', note: 'prefix must be CLI, version grammar not freed' },
191
+ { name: 'predict-cli-noslash', ua: 'CLI CodeBuddy/abcd', expect: 12403, rule: 'R6', note: 'CLI without slash is not the CLI form' },
192
+ ]
193
+
194
+ // agenttool scope round: the matrix showed /agenttool accepts even
195
+ // `CodeBuddy/ab` (predict-agenttool-bad-ua MISS) — the 12403 UA gate does
196
+ // NOT fire there for api-key auth, contradicting the spell. Re-test with
197
+ // the exact spell-rejected UA and a totally alien one.
198
+ // Round 6 (2026-08-19): R6's CLI-form disjunct collapsed (cli-anyver and
199
+ // cli-minimal both MISSED) while semver2-standalone / noncli-anyver /
200
+ // cli-noslash all HIT. The surviving rule R7 explains 37/38 observations:
201
+ // R7: UA passes iff it contains a substring matching /codebuddy\/\d+\.\d+/i
202
+ // (product token + at least x.y numeric version, dot mandatory)
203
+ // Sole contradiction: matrix `CLI/unknown CodeBuddy/abc` PASSED (R7 says
204
+ // reject). Retest it both ways; if it rejects now, the matrix row was an
205
+ // anomaly. Also probe the grammar edges: digit must follow the slash
206
+ // immediately (v-prefix), digit must follow the dot (1.x), and the token
207
+ // may sit anywhere in the UA.
208
+ const PREDICT_VARIANTS_R6 = [
209
+ { name: 'retest-cli-abc', ua: 'CLI/unknown CodeBuddy/abc', expect: 12403, rule: 'R7', note: 'matrix anomaly retest, CLI form' },
210
+ { name: 'retest-standalone-abc', ua: 'CodeBuddy/abc', expect: 12403, rule: 'R7', note: 'matrix anomaly retest, standalone' },
211
+ { name: 'predict-vprefix', ua: 'CodeBuddy/v1.2', expect: 12403, rule: 'R7', note: 'digit must immediately follow the slash' },
212
+ { name: 'predict-dot-nondigit', ua: 'CodeBuddy/1.x', expect: 12403, rule: 'R7', note: 'digit must immediately follow the dot' },
213
+ { name: 'predict-embedded', ua: 'MyApp CodeBuddy/3.14 xyz', expect: 200, rule: 'R7', note: 'token may sit anywhere in the UA' },
214
+ { name: 'predict-zerozero', ua: 'CodeBuddy/0.0', expect: 200, rule: 'R7', note: 'minimal semver token' },
215
+ ]
216
+
217
+ // Round 7 (2026-08-19): five HITs, one MISS — CodeBuddy/1.x PASSED, so the
218
+ // character after the dot is unconstrained. Rule R8:
219
+ // R8: UA passes iff it contains /codebuddy\/\d+\./i
220
+ // (product token, ≥1 digit, a literal dot; nothing after the dot is
221
+ // checked; token position and casing unconstrained)
222
+ // The earlier `CLI/unknown CodeBuddy/abc` matrix pass did NOT reproduce in
223
+ // either retest (12403 both ways) — recorded as a transient anomaly, not
224
+ // part of the rule.
225
+ // Final boundary confirmations: is anything at all required after the dot,
226
+ // and may the version start with a dot?
227
+ const PREDICT_VARIANTS_R7 = [
228
+ { name: 'predict-dot-terminal', ua: 'CodeBuddy/1.', expect: 200, rule: 'R8', note: 'nothing needed after the dot' },
229
+ { name: 'predict-dot-first', ua: 'CodeBuddy/.5', expect: 12403, rule: 'R8', note: 'version must start with a digit' },
230
+ { name: 'predict-multidigit-dot', ua: 'CodeBuddy/12.', expect: 200, rule: 'R8', note: 'multi-digit major, terminal dot' },
231
+ ]
232
+
233
+ // Round 8 (2026-08-19): dot-terminal and multidigit-dot HIT, but dot-first
234
+ // (.5) MISSED my rejection prediction — a version may START with a dot.
235
+ // Combined with v1.2 (reject: dot exists but first char is `v`) and 111
236
+ // (reject: digits but no dot), the rule that fits all 41 observations:
237
+ // R9: UA passes iff it contains /codebuddy\/[\d.]*\./i
238
+ // i.e. immediately after the slash: a run of digits/dots containing
239
+ // at least one literal dot; anything may follow; token position and
240
+ // casing unconstrained.
241
+ // Pre-registered final discriminators:
242
+ // - baredot / doubledot: absurd per semver intuition, R9 says pass
243
+ // - xdot: dot present but first char outside [\d.] → reject (separates
244
+ // "starts with [\d.]" from "contains a dot anywhere")
245
+ // - dashdot: first char `-` → reject
246
+ const PREDICT_VARIANTS_R8 = [
247
+ { name: 'predict-baredot', ua: 'CodeBuddy/.', expect: 200, rule: 'R9', note: 'a lone dot is a valid version' },
248
+ { name: 'predict-doubledot', ua: 'CodeBuddy/..', expect: 200, rule: 'R9', note: 'two dots also valid' },
249
+ { name: 'predict-xdot', ua: 'CodeBuddy/x.', expect: 12403, rule: 'R9', note: 'dot present but first char not in [\\d.]' },
250
+ { name: 'predict-dashdot', ua: 'CodeBuddy/-.5', expect: 12403, rule: 'R9', note: 'first char `-` breaks the class' },
251
+ ]
252
+
253
+ // Round 9 (2026-08-19): baredot/doubledot/xdot all HIT; dashdot MISSED my
254
+ // rejection prediction — `-` is accepted, pointing at the semver character
255
+ // class. Rule R10:
256
+ // R10: UA passes iff it contains /codebuddy\/[\d.-]*\./i
257
+ // (a run of SEMVER chars — digits, dots, hyphens — containing at
258
+ // least one dot, immediately after the product token)
259
+ // Pre-registered class boundary: `+` (build metadata) and `_` outside the
260
+ // class → reject; `1-2.3` inside → pass; dashdot retest guards against
261
+ // another abc-style transient.
262
+ const PREDICT_VARIANTS_R9 = [
263
+ { name: 'predict-plusdot', ua: 'CodeBuddy/+.5', expect: 12403, rule: 'R10', note: '+ is not a semver version char here' },
264
+ { name: 'predict-underscoredot', ua: 'CodeBuddy/_.5', expect: 12403, rule: 'R10', note: '_ outside the class' },
265
+ { name: 'predict-hyphen-mid', ua: 'CodeBuddy/1-2.3', expect: 200, rule: 'R10', note: 'hyphen inside the run, dot present' },
266
+ { name: 'retest-dashdot', ua: 'CodeBuddy/-.5', expect: 200, rule: 'R10', note: 'stability retest after the abc anomaly' },
267
+ ]
268
+
269
+ // Round 10 (2026-08-19): plusdot and underscoredot both MISSED (passed),
270
+ // hyphen-mid and the dashdot retest HIT. The accepted pre-dot character
271
+ // class is wider than semver: every non-letter works, every ASCII letter
272
+ // fails. Rule R11, fitting all 45 observations:
273
+ // R11: UA passes iff it contains /codebuddy\/[^a-z\s]*\./i
274
+ // after the product token, a run of NON-ASCII-letter, non-space
275
+ // chars containing at least one literal dot; anything may follow
276
+ // (1.x passed); token position and casing unconstrained.
277
+ // Final discriminators: letter AFTER the dot is fine; letter BEFORE the
278
+ // dot fails; CJK separates "ASCII-letter class" from Unicode letters;
279
+ // a space before the dot breaks the run.
280
+ const PREDICT_VARIANTS_R10 = [
281
+ { name: 'predict-symbol-letter-dot', ua: 'CodeBuddy/-.x', expect: 200, rule: 'R11', note: 'letter after the dot is unconstrained' },
282
+ { name: 'predict-letter-before-dot', ua: 'CodeBuddy/a.1', expect: 12403, rule: 'R11', note: 'letter before the dot breaks the run' },
283
+ { name: 'predict-cjk', ua: 'CodeBuddy/汉.5', expect: 200, rule: 'R11', note: 'CJK is outside the ASCII-letter class' },
284
+ { name: 'predict-space-before-dot', ua: 'CodeBuddy/ .', expect: 12403, rule: 'R11', note: 'space breaks the run' },
285
+ ]
286
+
287
+ // Round 11 (2026-08-19): symbol-letter-dot and space-before-dot HIT; the CJK
288
+ // arm is untestable (HTTP headers are ByteString — recorded as n/a); and
289
+ // letter-before-dot MISSED: `a.1` PASSED while matrix `v1.2` and `x.`
290
+ // FAILED. All three are "letter before a dot", so the run-class theory is
291
+ // dead. Fit across all 48 observations — R12:
292
+ // R12: the version after codebuddy/ must contain a dot AND satisfy one of
293
+ // (a) first char is not an ASCII letter (1.x, -.5, ., 12.)
294
+ // (b) it starts with <single letter>.<digit> (a.1)
295
+ // v1.2 fails (letter then digit before the dot), x. fails (letter, dot,
296
+ // no digit after), abcd/111/abc fail (no dot at all).
297
+ // Pre-registered discriminators over the before-dot grammar:
298
+ const PREDICT_VARIANTS_R11 = [
299
+ { name: 'predict-two-letters-dot', ua: 'CodeBuddy/ab.1', expect: 12403, rule: 'R12', note: 'two letters before dot → reject' },
300
+ { name: 'predict-letter-digit-dot', ua: 'CodeBuddy/a1.2', expect: 12403, rule: 'R12', note: 'letter+digit before dot → reject (v1.2 pattern)' },
301
+ { name: 'predict-digit-letter-dot', ua: 'CodeBuddy/1a.2', expect: 200, rule: 'R12', note: 'digit first → (a) applies' },
302
+ { name: 'predict-v-dot', ua: 'CodeBuddy/v.2', expect: 200, rule: 'R12', note: 'v alone before dot matches (b)' },
303
+ { name: 'predict-x-dot-digit', ua: 'CodeBuddy/x.5', expect: 200, rule: 'R12', note: 'x. failed only because nothing followed the dot' },
304
+ { name: 'predict-upper-dot-digit', ua: 'CodeBuddy/Z.9', expect: 200, rule: 'R12', note: 'uppercase letter also matches (b)' },
305
+ ]
306
+
307
+ // Round 12 (2026-08-19): ab.1 and a1.2 HIT; 1a.2, v.2, x.5, Z.9 all MISSED
308
+ // (rejected). So EVERY letter-before-dot version rejects — except the
309
+ // one-off `a.1` pass, which now looks like the second anomaly of the same
310
+ // kind as the matrix `abc` pass (both reproduced as 12403 on retest).
311
+ // Hypothesis: the gateway fronts MULTIPLE validator builds; a minority node
312
+ // intermittently accepts looser UAs. Final flapping test: repeat `a.1`
313
+ // four times spaced 2s — mixed results confirm multi-validator flapping;
314
+ // uniform 12403 means the earlier pass was a transient; uniform 200 would
315
+ // force yet another rule revision (not expected).
316
+ const PREDICT_VARIANTS_R12 = [
317
+ { name: 'flap-a1-1', ua: 'CodeBuddy/a.1', expect: 12403, rule: 'R13', note: 'flapping test 1/4' },
318
+ { name: 'flap-a1-2', ua: 'CodeBuddy/a.1', expect: 12403, rule: 'R13', note: 'flapping test 2/4' },
319
+ { name: 'flap-a1-3', ua: 'CodeBuddy/a.1', expect: 12403, rule: 'R13', note: 'flapping test 3/4' },
320
+ { name: 'flap-a1-4', ua: 'CodeBuddy/a.1', expect: 12403, rule: 'R13', note: 'flapping test 4/4' },
321
+ { name: 'control-good', ua: 'CodeBuddy/1.0', expect: 200, rule: 'R13', note: 'known-good control' },
322
+ { name: 'retest-abc-final', ua: 'CodeBuddy/abc', expect: 12403, rule: 'R13', note: 'second anomaly confirmation' },
323
+ ]
324
+
325
+ // agenttool scope round: the matrix showed /agenttool accepts even
326
+ // `CodeBuddy/ab` (predict-agenttool-bad-ua MISS) — the 12403 UA gate does
327
+ // NOT fire there for api-key auth, contradicting the spell. Re-test with
328
+ // the exact spell-rejected UA and a totally alien one.
329
+ const AGENTTOOL_ARMS_R2 = [
330
+ { name: 'predict-agenttool-spell-ua', ua: 'CodeBuddyCode/1.0', expect: 200, rule: 'R-scope', note: 'spell claims 12403 on /agenttool; probe predicts the gate is absent' },
331
+ { name: 'predict-agenttool-garbage', ua: 'Garbage/0.0', expect: 200, rule: 'R-scope', note: 'alien UA still passes /agenttool' },
332
+ ]
333
+
334
+ // Endpoint-scope arms: does /agenttool share the /v3/config UA validator?
335
+ // bad-ua arm runs first — if the gate fires (12403) no search is executed.
336
+ const AGENTTOOL_ARMS = [
337
+ { name: 'predict-agenttool-bad-ua', ua: 'CodeBuddy/ab', expect: 12403, rule: 'R3', note: 'same validator → 12403 before any search work' },
338
+ { name: 'agenttool-baseline', ua: BASELINE_UA, expect: 0, rule: null, note: 'control: real tiny search through the shipped UA' },
339
+ ]
340
+
341
+ const CHAT_ARMS = [
342
+ { name: 'chat-garbage-ua', ua: 'Garbage/0.0', note: 'H5: /v2 ignores UA, expect 200' },
343
+ { name: 'chat-cli-ua', ua: BASELINE_UA, note: 'control, expect 200' },
344
+ ]
345
+
346
+ const CHAT_ARMS_PREDICT = [
347
+ // 11101 on the matrix run proved garbage UA passes the /v2 UA gate (it
348
+ // failed only on stream:false). This arm completes H5 with a real 200.
349
+ { name: 'predict-chat-stream-garbage-ua', ua: 'Garbage/0.0', stream: true, expect: 0, rule: 'R2', note: 'stream:true + garbage UA → 200/code 0' },
350
+ ]
351
+
352
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
353
+
354
+ function redact(value) {
355
+ if (typeof value !== 'string') return value
356
+ return value
357
+ .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<redacted>')
358
+ .replace(/\b\d{6,}\b/g, '<redacted>')
359
+ }
360
+
361
+ async function probeConfig(cred, v) {
362
+ const headers = { Accept: 'application/json', Authorization: cred.authorization }
363
+ if (cred.apiKey) headers['x-api-key'] = cred.apiKey
364
+ if (!v.dropXProduct) headers['X-Product'] = 'SaaS'
365
+ if (v.ua !== null) headers['User-Agent'] = v.ua
366
+ const t0 = Date.now()
367
+ try {
368
+ const res = await fetch(`${GATEWAY}/v3/config`, { headers })
369
+ const text = await res.text()
370
+ let json = null
371
+ try { json = JSON.parse(text) } catch { /* non-JSON */ }
372
+ return {
373
+ name: v.name, ua: v.ua, note: v.note, expect: v.expect,
374
+ status: res.status, code: json?.code ?? null,
375
+ msg: redact(json?.msg ?? text.slice(0, 120)), ms: Date.now() - t0,
376
+ }
377
+ } catch (err) {
378
+ return { name: v.name, ua: v.ua, note: v.note, expect: v.expect, error: String(err?.message ?? err), ms: Date.now() - t0 }
379
+ }
380
+ }
381
+
382
+ async function probeChat(cred, arm) {
383
+ const t0 = Date.now()
384
+ try {
385
+ const res = await fetch(`${GATEWAY}/v2/chat/completions`, {
386
+ method: 'POST',
387
+ headers: {
388
+ Authorization: cred.authorization,
389
+ 'Content-Type': 'application/json',
390
+ Accept: 'application/json',
391
+ 'User-Agent': arm.ua,
392
+ },
393
+ body: JSON.stringify({
394
+ model: 'deepseek-v3', stream: arm.stream === true, max_tokens: 1,
395
+ messages: [{ role: 'user', content: 'hi' }],
396
+ }),
397
+ })
398
+ if (arm.stream === true) {
399
+ // SSE: we only need the HTTP status — UA rejection (12403) would arrive
400
+ // as a JSON error body instead of an event stream.
401
+ const snippet = await res.text()
402
+ return {
403
+ name: arm.name, ua: arm.ua, note: arm.note, expect: arm.expect,
404
+ status: res.status, code: res.status === 200 ? 0 : null,
405
+ msg: redact(snippet.slice(0, 120)), ms: Date.now() - t0,
406
+ }
407
+ }
408
+ const text = await res.text()
409
+ let json = null
410
+ try { json = JSON.parse(text) } catch { /* stream or non-JSON */ }
411
+ return {
412
+ name: arm.name, ua: arm.ua, note: arm.note, expect: arm.expect,
413
+ status: res.status, code: json?.code ?? null,
414
+ msg: redact(json?.msg ?? json?.error?.message ?? text.slice(0, 120)),
415
+ ms: Date.now() - t0,
416
+ }
417
+ } catch (err) {
418
+ return { name: arm.name, ua: arm.ua, note: arm.note, error: String(err?.message ?? err), ms: Date.now() - t0 }
419
+ }
420
+ }
421
+
422
+ async function probeAgentTool(cred, arm) {
423
+ const t0 = Date.now()
424
+ try {
425
+ const res = await fetch(`${GATEWAY}/agenttool/v1/search`, {
426
+ method: 'POST',
427
+ headers: {
428
+ Authorization: cred.authorization,
429
+ 'Content-Type': 'application/json',
430
+ Accept: 'application/json',
431
+ 'User-Agent': arm.ua,
432
+ 'X-Requested-With': 'XMLHttpRequest',
433
+ },
434
+ body: JSON.stringify({ query: 'kimi', type: 'text2text', max_results: 1 }),
435
+ })
436
+ const text = await res.text()
437
+ let json = null
438
+ try { json = JSON.parse(text) } catch { /* non-JSON */ }
439
+ return {
440
+ name: arm.name, ua: arm.ua, note: arm.note, expect: arm.expect,
441
+ status: res.status, code: json?.code ?? null,
442
+ msg: redact(json?.msg ?? text.slice(0, 120)), ms: Date.now() - t0,
443
+ }
444
+ } catch (err) {
445
+ return { name: arm.name, ua: arm.ua, note: arm.note, expect: arm.expect, error: String(err?.message ?? err), ms: Date.now() - t0 }
446
+ }
447
+ }
448
+
449
+ // --- driver ------------------------------------------------------------------
450
+ const cred = resolveCredential()
451
+ mkdirSync(dirname(OUT), { recursive: true })
452
+ const stamp = new Date().toISOString()
453
+ appendFileSync(OUT, JSON.stringify({ type: 'run', at: stamp, set: SET, only: ONLY, gateway: GATEWAY }) + '\n')
454
+ console.log(`evidence → ${OUT}`)
455
+
456
+ const variants = SET === 'predict' ? PREDICT_VARIANTS
457
+ : SET === 'predict2' ? PREDICT_VARIANTS_R2
458
+ : SET === 'predict3' ? PREDICT_VARIANTS_R3
459
+ : SET === 'predict4' ? PREDICT_VARIANTS_R4
460
+ : SET === 'predict5' ? PREDICT_VARIANTS_R5
461
+ : SET === 'predict6' ? PREDICT_VARIANTS_R6
462
+ : SET === 'predict7' ? PREDICT_VARIANTS_R7
463
+ : SET === 'predict8' ? PREDICT_VARIANTS_R8
464
+ : SET === 'predict9' ? PREDICT_VARIANTS_R9
465
+ : SET === 'predict10' ? PREDICT_VARIANTS_R10
466
+ : SET === 'predict11' ? PREDICT_VARIANTS_R11
467
+ : SET === 'predict12' ? PREDICT_VARIANTS_R12
468
+ : SET === 'all' ? [...MATRIX_VARIANTS, ...PREDICT_VARIANTS, ...PREDICT_VARIANTS_R2, ...PREDICT_VARIANTS_R3, ...PREDICT_VARIANTS_R4, ...PREDICT_VARIANTS_R5, ...PREDICT_VARIANTS_R6]
469
+ : MATRIX_VARIANTS
470
+ const picked = ONLY ? variants.filter((v) => ONLY.includes(v.name)) : variants
471
+
472
+ // expect 200 = the request passed the UA gate (HTTP 200; body shape differs
473
+ // per endpoint: /v3/config wraps code:0, /agenttool returns bare results).
474
+ // expect 12403 = rejected at the UA gate with that error code.
475
+ function judge(rec) {
476
+ if (rec.expect == null) return ''
477
+ const pass = rec.expect === 200 ? rec.status === 200 : rec.code === rec.expect
478
+ return pass ? ' HIT' : ' MISS'
479
+ }
480
+
481
+ if (picked.length === 0 && SET !== 'predict') console.log('no variants selected')
482
+ for (const v of picked) {
483
+ const rec = { type: 'probe', endpoint: '/v3/config', ...await probeConfig(cred, v) }
484
+ appendFileSync(OUT, JSON.stringify(rec) + '\n')
485
+ console.log(`${v.name.padEnd(20)} status=${rec.status ?? '-'} code=${rec.code ?? '-'} msg=${rec.msg ?? rec.error ?? ''}${judge(rec)}`)
486
+ await sleep(SPACING_MS)
487
+ }
488
+
489
+ if (!SKIP_CHAT) {
490
+ const chatArms = SET === 'predict' ? CHAT_ARMS_PREDICT : SET === 'matrix' ? CHAT_ARMS : []
491
+ for (const arm of chatArms) {
492
+ const rec = { type: 'probe', endpoint: '/v2/chat/completions', ...await probeChat(cred, arm) }
493
+ appendFileSync(OUT, JSON.stringify(rec) + '\n')
494
+ console.log(`${arm.name.padEnd(20)} status=${rec.status ?? '-'} code=${rec.code ?? '-'} msg=${rec.msg ?? rec.error ?? ''}${judge(rec)}`)
495
+ await sleep(SPACING_MS)
496
+ }
497
+ }
498
+
499
+ if (SET === 'predict4' || SET === 'predict5') {
500
+ const arms = SET === 'predict4' ? AGENTTOOL_ARMS : AGENTTOOL_ARMS_R2
501
+ for (const arm of arms) {
502
+ const rec = { type: 'probe', endpoint: '/agenttool/v1/search', ...await probeAgentTool(cred, arm) }
503
+ appendFileSync(OUT, JSON.stringify(rec) + '\n')
504
+ console.log(`${arm.name.padEnd(28)} status=${rec.status ?? '-'} code=${rec.code ?? '-'} msg=${rec.msg ?? rec.error ?? ''}${judge(rec)}`)
505
+ await sleep(SPACING_MS)
506
+ }
507
+ }
508
+ console.log('done.')