@danceiny/gotry 0.0.1-rc.16 → 0.0.1-rc.17

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 (229) hide show
  1. package/README.md +47 -13
  2. package/README.zh-CN.md +20 -10
  3. package/bin/gotry-booking-copilot.js +53 -0
  4. package/bin/gotry-bootstrap.js +111 -51
  5. package/bin/gotry-inner.js +442 -60
  6. package/bin/gotry-runtime-resolution.d.ts +27 -0
  7. package/bin/gotry-runtime-resolution.js +50 -0
  8. package/bin/gotry.js +1 -1
  9. package/cordis.gotry-patch.yml +5 -1
  10. package/dist/capabilities/agent-reach-deep.js +1 -1
  11. package/dist/capabilities/agent-reach.js +1 -1
  12. package/dist/capabilities/anything.js +1 -1
  13. package/dist/capabilities/artifacts.js +1 -1
  14. package/dist/capabilities/effect.js +1 -1
  15. package/dist/capabilities/fact-log.js +1 -1
  16. package/dist/capabilities/flyai.js +20 -6
  17. package/dist/capabilities/hbcli.js +1 -1
  18. package/dist/capabilities/incident-log.js +1 -1
  19. package/dist/capabilities/model-override.js +18 -0
  20. package/dist/capabilities/opensky.js +1 -1
  21. package/dist/capabilities/resilience.js +1 -1
  22. package/dist/capabilities/session/action-cache.js +1 -1
  23. package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
  24. package/dist/capabilities/session/adapters/meituan-local.js +1 -1
  25. package/dist/capabilities/session/benchmark.js +1 -1
  26. package/dist/capabilities/session/extension-bridge.js +19 -6
  27. package/dist/capabilities/session/extension-channel.js +3 -3
  28. package/dist/capabilities/session/extension-distribution.js +234 -0
  29. package/dist/capabilities/session/extract.js +1 -1
  30. package/dist/capabilities/session/golden-score.js +92 -0
  31. package/dist/capabilities/session/health-watch.js +1 -1
  32. package/dist/capabilities/session/read-guard.js +1 -1
  33. package/dist/capabilities/session/static-flight-golden.js +137 -0
  34. package/dist/capabilities/session/transport.js +1 -1
  35. package/dist/capabilities/session/wizard.js +21 -251
  36. package/dist/capabilities/session-consent.js +1 -1
  37. package/dist/capabilities/session-login.js +11 -5
  38. package/dist/capabilities/session-search.js +46 -4
  39. package/dist/capabilities/weather.js +168 -46
  40. package/dist/data/session-golden-20.json +25 -0
  41. package/dist/data/sf-golden-manifest.json +102 -0
  42. package/dist/data/sf-static-routes.json +91 -0
  43. package/dist/scripts/action-cache-tests.js +1 -1
  44. package/dist/scripts/agent-planning-budget-e2e.js +227 -0
  45. package/dist/scripts/agent-planning-budget-tests.js +173 -0
  46. package/dist/scripts/agent-reach-deep-tests.js +1 -1
  47. package/dist/scripts/agent-reach-tests.js +1 -1
  48. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  49. package/dist/scripts/anything-tests.js +1 -1
  50. package/dist/scripts/async-collect.js +1 -1
  51. package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
  52. package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
  53. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
  54. package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
  55. package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
  56. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
  57. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
  58. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
  59. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
  60. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
  61. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
  62. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
  63. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
  64. package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
  65. package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
  66. package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
  67. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
  68. package/dist/scripts/booking-saga-tests.js +1 -1
  69. package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
  70. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
  71. package/dist/scripts/bootstrap-tests.js +30 -10
  72. package/dist/scripts/build-changelog.js +1 -1
  73. package/dist/scripts/changelog-tests.js +1 -1
  74. package/dist/scripts/companion-tests.js +1 -1
  75. package/dist/scripts/diff-test.js +1 -1
  76. package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
  77. package/dist/scripts/dsh-runtime-closure.js +150 -0
  78. package/dist/scripts/effect-tests.js +1 -1
  79. package/dist/scripts/engine-run.js +1 -1
  80. package/dist/scripts/engine-tests.js +1 -1
  81. package/dist/scripts/evaluation-cadence-tests.js +347 -0
  82. package/dist/scripts/evaluation-contract-tests.js +574 -0
  83. package/dist/scripts/extension-distribution-cli.js +35 -0
  84. package/dist/scripts/extension-distribution-tests.js +341 -0
  85. package/dist/scripts/extension-tests.js +128 -23
  86. package/dist/scripts/fact-gate-tests.js +1 -1
  87. package/dist/scripts/flyai-tests.js +1 -1
  88. package/dist/scripts/hbcli-e2e-tests.js +1 -1
  89. package/dist/scripts/hbcli-tests.js +1 -1
  90. package/dist/scripts/health-watch-cli.js +1 -1
  91. package/dist/scripts/i18n-tests.js +1 -1
  92. package/dist/scripts/incident-tests.js +1 -1
  93. package/dist/scripts/journey-tests.js +1 -1
  94. package/dist/scripts/ledger-tests.js +1 -1
  95. package/dist/scripts/ledger-workflow-crash.js +1 -1
  96. package/dist/scripts/memory-capture-tests.js +1 -1
  97. package/dist/scripts/memory-decay-tests.js +1 -1
  98. package/dist/scripts/memory-metrics.js +1 -1
  99. package/dist/scripts/memory-value-report.js +1 -1
  100. package/dist/scripts/model-override-e2e.js +176 -0
  101. package/dist/scripts/nightly-evidence-tests.js +1 -1
  102. package/dist/scripts/nightly-evidence.js +1 -1
  103. package/dist/scripts/nudge-digest.js +1 -1
  104. package/dist/scripts/onboarding-tests.js +21 -53
  105. package/dist/scripts/opensky-check.js +1 -1
  106. package/dist/scripts/opensky-tests.js +1 -1
  107. package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
  108. package/dist/scripts/price-drift-tests.js +1 -1
  109. package/dist/scripts/price-drift-watch.js +1 -1
  110. package/dist/scripts/probe-poi-tests.js +1 -1
  111. package/dist/scripts/product-metrics.js +1 -1
  112. package/dist/scripts/publish-preverify.js +40 -4
  113. package/dist/scripts/realtime-pricing-tests.js +1 -1
  114. package/dist/scripts/replay-async.js +1 -1
  115. package/dist/scripts/replay-real.js +1 -1
  116. package/dist/scripts/replay.js +1 -1
  117. package/dist/scripts/session-attach-diagnose.js +1 -1
  118. package/dist/scripts/session-attach-poc.js +1 -1
  119. package/dist/scripts/session-benchmark.js +1 -1
  120. package/dist/scripts/session-extract-tests.js +1 -1
  121. package/dist/scripts/session-login.js +1 -1
  122. package/dist/scripts/session-tests.js +70 -20
  123. package/dist/scripts/sf-live-benchmark.js +338 -0
  124. package/dist/scripts/sf-live-cli-tests.js +21 -0
  125. package/dist/scripts/sf-soft-score-tests.js +108 -0
  126. package/dist/scripts/sf-summary.js +93 -0
  127. package/dist/scripts/skeleton-check.js +1 -1
  128. package/dist/scripts/skeleton-integration-test.js +1 -1
  129. package/dist/scripts/skills-contract-tests.js +1 -1
  130. package/dist/scripts/smoke-session-gate-tests.js +29 -0
  131. package/dist/scripts/smoke.js +79 -33
  132. package/dist/scripts/state-cli-tests.js +1 -1
  133. package/dist/scripts/state-cli.js +1 -1
  134. package/dist/scripts/static-golden-tests.js +299 -0
  135. package/dist/scripts/time-eval-tests.js +1 -1
  136. package/dist/scripts/travel-timeline-tests.js +1 -1
  137. package/dist/scripts/unified-tests.js +1 -1
  138. package/dist/scripts/weather-tests.js +694 -44
  139. package/dist/scripts/z3-race-tests.js +1 -1
  140. package/dist/src/artifact-gate.js +1 -1
  141. package/dist/src/benchmark-agent-conformance.js +370 -0
  142. package/dist/src/benchmark-environment-bridge.js +384 -0
  143. package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
  144. package/dist/src/benchmark-tool-isolation.js +124 -0
  145. package/dist/src/bookable-facts.js +1 -1
  146. package/dist/src/booking-saga.js +1 -1
  147. package/dist/src/booking-surface/availability-policy-v2.js +830 -0
  148. package/dist/src/booking-surface/canonical-schema.js +113 -0
  149. package/dist/src/booking-surface/contracts-v2.js +89 -0
  150. package/dist/src/booking-surface/contracts.js +46 -0
  151. package/dist/src/booking-surface/dsh-planner.js +453 -0
  152. package/dist/src/booking-surface/dsh-plugin.js +93 -0
  153. package/dist/src/booking-surface/error-codes.js +94 -0
  154. package/dist/src/booking-surface/index.js +15 -0
  155. package/dist/src/booking-surface/profile.js +68 -0
  156. package/dist/src/booking-surface/runtime-v2.js +1771 -0
  157. package/dist/src/booking-surface/runtime.js +351 -0
  158. package/dist/src/booking-surface/server-v2.js +334 -0
  159. package/dist/src/booking-surface/server.js +302 -0
  160. package/dist/src/booking-surface/startup.js +159 -0
  161. package/dist/src/booking-surface/validation-v2.js +319 -0
  162. package/dist/src/booking-surface/validation.js +809 -0
  163. package/dist/src/bridge.js +1 -1
  164. package/dist/src/companions.js +1 -1
  165. package/dist/src/contracts.js +1 -1
  166. package/dist/src/dsh-llm.js +1 -1
  167. package/dist/src/engine.js +1 -1
  168. package/dist/src/evaluation-cadence.js +234 -0
  169. package/dist/src/evaluation-contracts.js +906 -0
  170. package/dist/src/i18n.js +1 -1
  171. package/dist/src/index.js +50 -19
  172. package/dist/src/journey.js +1 -1
  173. package/dist/src/loop.js +1 -1
  174. package/dist/src/memory-capture.js +1 -1
  175. package/dist/src/memory-decay.js +1 -1
  176. package/dist/src/memory-utility.js +1 -1
  177. package/dist/src/mock-llm.js +1 -1
  178. package/dist/src/model.js +1 -1
  179. package/dist/src/realtime-pricing.js +1 -1
  180. package/dist/src/slot-spec.js +1 -1
  181. package/dist/src/state-ledger.js +2 -1
  182. package/dist/src/time-anchor.js +1 -1
  183. package/dist/src/tool-budget.js +136 -0
  184. package/dist/src/tool-packet.js +1 -1
  185. package/dist/src/travel-slots.js +1 -1
  186. package/dist/src/travel-timeline.js +1 -1
  187. package/dist/src/unified.js +1 -1
  188. package/dist/src/wish-pool.js +1 -1
  189. package/dist/src/z3-shared.js +1 -1
  190. package/extension/README.md +31 -7
  191. package/package.json +286 -11
  192. package/schemas/booking.surface.v1.schema.json +927 -0
  193. package/schemas/booking.surface.v2.schema.json +61 -0
  194. package/ts/capabilities/flyai.ts +16 -3
  195. package/ts/capabilities/session/extension-bridge.ts +37 -14
  196. package/ts/capabilities/session/extension-channel.ts +6 -3
  197. package/ts/capabilities/session/extension-distribution.ts +264 -0
  198. package/ts/capabilities/session/golden-score.ts +139 -0
  199. package/ts/capabilities/session/health-watch.ts +1 -1
  200. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  201. package/ts/capabilities/session/wizard.ts +34 -176
  202. package/ts/capabilities/session-login.ts +15 -5
  203. package/ts/capabilities/session-search.ts +40 -3
  204. package/ts/capabilities/weather.ts +141 -52
  205. package/ts/package.json +3 -3
  206. package/ts/src/benchmark-agent-conformance.ts +448 -0
  207. package/ts/src/benchmark-environment-bridge.ts +348 -0
  208. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  209. package/ts/src/benchmark-tool-isolation.ts +166 -0
  210. package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
  211. package/ts/src/booking-surface/canonical-schema.js +113 -0
  212. package/ts/src/booking-surface/contracts-v2.ts +118 -0
  213. package/ts/src/booking-surface/contracts.ts +380 -0
  214. package/ts/src/booking-surface/dsh-planner.ts +452 -0
  215. package/ts/src/booking-surface/dsh-plugin.js +93 -0
  216. package/ts/src/booking-surface/error-codes.ts +101 -0
  217. package/ts/src/booking-surface/index.ts +12 -0
  218. package/ts/src/booking-surface/profile.ts +42 -0
  219. package/ts/src/booking-surface/runtime-v2.ts +1466 -0
  220. package/ts/src/booking-surface/runtime.ts +483 -0
  221. package/ts/src/booking-surface/server-v2.ts +247 -0
  222. package/ts/src/booking-surface/server.ts +324 -0
  223. package/ts/src/booking-surface/startup.ts +196 -0
  224. package/ts/src/booking-surface/validation-v2.ts +205 -0
  225. package/ts/src/booking-surface/validation.ts +453 -0
  226. package/ts/src/index.ts +64 -11
  227. package/ts/src/state-ledger.ts +1 -0
  228. package/ts/src/tool-budget.ts +165 -0
  229. package/dist/scripts/wizard-bootstrap.js +0 -32
@@ -0,0 +1,348 @@
1
+ import { accessSync, constants, lstatSync, readFileSync, statSync } from 'node:fs'
2
+ import { getuid } from 'node:process'
3
+ import { isAbsolute } from 'node:path'
4
+ import { defineTool } from '@deepseek-ai/dsh-tools'
5
+ import {
6
+ validateTerminalOutputConfig,
7
+ type BenchmarkBridgeProjection,
8
+ type TerminalOutputConfig,
9
+ } from './benchmark-agent-conformance.ts'
10
+
11
+ const SCHEMA_VERSION = 'gotry_benchmark_environment_bridge_v2'
12
+ const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_.-]*$/
13
+ const CONFIG_KEYS = ['allowed_tools', 'argv_prefix', 'cwd', 'enabled', 'executable', 'isolation', 'max_output_bytes', 'schema_version', 'terminal_output', 'timeout_ms']
14
+ const OPTIONAL_CONFIG_KEYS = ['allowed_output_keys']
15
+
16
+ export interface BenchmarkEnvironmentBridgeConfig {
17
+ schema_version: typeof SCHEMA_VERSION
18
+ enabled: true
19
+ executable: string
20
+ cwd: string
21
+ argv_prefix: string[]
22
+ allowed_tools: string[]
23
+ allowed_output_keys?: Record<string, string[]>
24
+ timeout_ms: number
25
+ max_output_bytes: number
26
+ terminal_output: TerminalOutputConfig
27
+ isolation: {
28
+ mode: 'host-enforced'
29
+ writes: 'forbidden'
30
+ network: 'denied'
31
+ }
32
+ }
33
+
34
+ function plainObject(value: unknown): value is Record<string, unknown> {
35
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
36
+ }
37
+
38
+ function existingExecutable(value: unknown): value is string {
39
+ if (typeof value !== 'string' || !isAbsolute(value)) return false
40
+ try {
41
+ const stat = statSync(value)
42
+ accessSync(value, constants.X_OK)
43
+ return stat.isFile()
44
+ } catch {
45
+ return false
46
+ }
47
+ }
48
+
49
+ function existingDirectory(value: unknown): value is string {
50
+ if (typeof value !== 'string' || !isAbsolute(value)) return false
51
+ try {
52
+ return statSync(value).isDirectory()
53
+ } catch {
54
+ return false
55
+ }
56
+ }
57
+
58
+ function identifiers(value: unknown): value is string[] {
59
+ return Array.isArray(value) && value.length > 0 && value.length <= 64 && new Set(value).size === value.length && value.every(
60
+ item => typeof item === 'string' && item.length > 0 && IDENTIFIER.test(item),
61
+ )
62
+ }
63
+
64
+ function allowedOutputKeys(value: unknown, allowedTools: string[]): value is Record<string, string[]> {
65
+ if (!plainObject(value)) return false
66
+ const keys = Object.keys(value)
67
+ return keys.length > 0 && keys.length <= 64
68
+ && keys.every(tool => allowedTools.includes(tool) && identifiers(value[tool]))
69
+ }
70
+
71
+ // These are passed as direct argv entries (never through a shell), so options
72
+ // such as "-m" and dotted module names are valid. Keep the prefix bounded and
73
+ // reject line/control separators that could make diagnostics ambiguous.
74
+ function argvPrefix(value: unknown): value is string[] {
75
+ return Array.isArray(value) && value.length > 0 && value.length <= 32 && value.every(
76
+ item => typeof item === 'string'
77
+ && item.length > 0
78
+ && item.length <= 256
79
+ && !item.includes('\0')
80
+ && !item.includes('\r')
81
+ && !item.includes('\n'),
82
+ )
83
+ }
84
+
85
+ function exactIsolation(value: unknown): value is BenchmarkEnvironmentBridgeConfig['isolation'] {
86
+ if (!plainObject(value)) return false
87
+ const keys = Object.keys(value).sort()
88
+ return JSON.stringify(keys) === JSON.stringify(['mode', 'network', 'writes'])
89
+ && value.mode === 'host-enforced'
90
+ && value.writes === 'forbidden'
91
+ && value.network === 'denied'
92
+ }
93
+
94
+ function validBound(value: unknown, maximum: number): value is number {
95
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= maximum
96
+ }
97
+
98
+ function validate(value: unknown): value is BenchmarkEnvironmentBridgeConfig {
99
+ const configKeys = plainObject(value) ? Object.keys(value).sort() : []
100
+ const baseKeys = [...CONFIG_KEYS].sort()
101
+ const extendedKeys = [...CONFIG_KEYS, ...OPTIONAL_CONFIG_KEYS].sort()
102
+ if (!plainObject(value)
103
+ || ![baseKeys, extendedKeys].some(keys => JSON.stringify(configKeys) === JSON.stringify(keys))
104
+ || value.schema_version !== SCHEMA_VERSION
105
+ || value.enabled !== true
106
+ || !existingExecutable(value.executable)
107
+ || !existingDirectory(value.cwd)
108
+ || !argvPrefix(value.argv_prefix)
109
+ || !identifiers(value.allowed_tools)
110
+ || !validBound(value.timeout_ms, 120_000)
111
+ || !validBound(value.max_output_bytes, 10 * 1024 * 1024)
112
+ || !validateTerminalOutputConfig(value.terminal_output)
113
+ || !exactIsolation(value.isolation)
114
+ || (Object.hasOwn(value, 'allowed_output_keys') && !allowedOutputKeys(value.allowed_output_keys, value.allowed_tools))) return false
115
+ return true
116
+ }
117
+
118
+ /** Read and validate an owner-local bridge config without exposing its contents. */
119
+ export function loadBenchmarkEnvironmentConfig(path: string): BenchmarkEnvironmentBridgeConfig | null {
120
+ try {
121
+ if (!isAbsolute(path)) return null
122
+ const metadata = lstatSync(path)
123
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > 64 * 1024) return null
124
+ if (typeof getuid !== 'function' || metadata.uid !== getuid()) return null
125
+ if ((metadata.mode & 0o022) !== 0) return null
126
+ const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
127
+ return validate(parsed) ? parsed : null
128
+ } catch {
129
+ return null
130
+ }
131
+ }
132
+
133
+ type Register = (tool: ReturnType<typeof defineTool>) => void
134
+ interface CollectedText {
135
+ readFrom(offset: number): { text: string; nextOffset: number; lossy?: boolean }
136
+ }
137
+
138
+ interface SubprocessHandle {
139
+ /** DSH reports -1 when process creation itself failed. */
140
+ readonly pid: number
141
+ collected: { stdout: CollectedText; stderr: CollectedText }
142
+ done: Promise<{ exitCode: number | null; signal?: string | null }>
143
+ terminate?: () => void
144
+ }
145
+
146
+ export interface BenchmarkSubprocessService {
147
+ spawn(spec: {
148
+ argv: readonly string[]
149
+ cwd: string
150
+ stdio: {
151
+ stdin: 'ignore'
152
+ stdout: { maxBytes: number }
153
+ stderr: { maxBytes: number }
154
+ }
155
+ graceMs: number
156
+ env?: NodeJS.ProcessEnv
157
+ signal?: AbortSignal
158
+ }): SubprocessHandle
159
+ }
160
+
161
+ function structuredObject(text: string): Record<string, unknown> | null {
162
+ try {
163
+ const parsed: unknown = JSON.parse(text.trim())
164
+ return plainObject(parsed) ? parsed : null
165
+ } catch {
166
+ return null
167
+ }
168
+ }
169
+
170
+ function jsonObject(value: unknown): Record<string, never> {
171
+ return JSON.parse(JSON.stringify(value)) as Record<string, never>
172
+ }
173
+
174
+ const FORBIDDEN_OUTPUT_KEY_FRAGMENTS = [
175
+ 'answer', 'expected', 'gold', 'groundtruth', 'hiddenquery', 'label',
176
+ 'loadermetadata', 'oracle', 'reference', 'reward', 'score',
177
+ ]
178
+
179
+ function inspectOutput(value: unknown): 'ok' | 'forbidden_key' | 'structure_limit' {
180
+ const pending: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }]
181
+ let nodes = 0
182
+ while (pending.length > 0) {
183
+ const current = pending.pop()!
184
+ if (++nodes > 10_000 || current.depth > 24) return 'structure_limit'
185
+ if (Array.isArray(current.value)) {
186
+ for (const child of current.value) pending.push({ value: child, depth: current.depth + 1 })
187
+ continue
188
+ }
189
+ if (!plainObject(current.value)) continue
190
+ for (const [key, child] of Object.entries(current.value)) {
191
+ if (!/^[\x20-\x7E]+$/.test(key)) return 'forbidden_key'
192
+ const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, '')
193
+ if (FORBIDDEN_OUTPUT_KEY_FRAGMENTS.some(fragment => normalized.includes(fragment))) return 'forbidden_key'
194
+ pending.push({ value: child, depth: current.depth + 1 })
195
+ }
196
+ }
197
+ return 'ok'
198
+ }
199
+
200
+ function inspectAllowedOutput(value: unknown, allowedKeys: string[]): boolean {
201
+ const allowed = new Set(allowedKeys)
202
+ const pending: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }]
203
+ let nodes = 0
204
+ while (pending.length > 0) {
205
+ const current = pending.pop()!
206
+ if (++nodes > 10_000 || current.depth > 24) return false
207
+ if (Array.isArray(current.value)) {
208
+ for (const child of current.value) pending.push({ value: child, depth: current.depth + 1 })
209
+ continue
210
+ }
211
+ if (!plainObject(current.value)) continue
212
+ for (const [key, child] of Object.entries(current.value)) {
213
+ if (!allowed.has(key)) return false
214
+ pending.push({ value: child, depth: current.depth + 1 })
215
+ }
216
+ }
217
+ return true
218
+ }
219
+
220
+ function serializedArguments(value: Record<string, unknown>): { json: string; reason?: never } | { json?: never; reason: string } {
221
+ const seen = new Set<object>()
222
+ let nodes = 0
223
+ let keys = 0
224
+ const inspect = (current: unknown, depth: number): boolean => {
225
+ if (++nodes > 10_000 || depth > 12) return false
226
+ if (current && typeof current === 'object') {
227
+ if (seen.has(current)) return false
228
+ seen.add(current)
229
+ if (Array.isArray(current)) return current.every(item => inspect(item, depth + 1))
230
+ return Object.entries(current).every(([key, child]) => {
231
+ if (++keys > 10_000) return false
232
+ return inspect(key, depth + 1) && inspect(child, depth + 1)
233
+ })
234
+ }
235
+ return true
236
+ }
237
+ if (!inspect(value, 0)) return { reason: 'serialization_limit' }
238
+ try {
239
+ const json = JSON.stringify(value)
240
+ if (typeof json !== 'string' || Buffer.byteLength(json, 'utf8') > 65_536) return { reason: 'serialization_limit' }
241
+ return { json }
242
+ } catch {
243
+ return { reason: 'serialization_limit' }
244
+ }
245
+ }
246
+
247
+ /** Register the opt-in model-facing bridge. */
248
+ export function registerBenchmarkEnvironmentBridge(
249
+ path: string,
250
+ register: Register,
251
+ subprocess?: BenchmarkSubprocessService,
252
+ ): BenchmarkBridgeProjection {
253
+ const bridge = loadBenchmarkEnvironmentConfig(path)
254
+ if (!bridge) throw new Error('benchmark environment bridge configuration unavailable')
255
+ if (!subprocess) throw new Error('benchmark environment bridge subprocess unavailable')
256
+
257
+ register(defineTool({
258
+ name: 'gotry_benchmark_environment',
259
+ description: 'When a prompt asks to run agent_env.cli, use action=call here with the mapped tool; arbitrary shell is not exposed.',
260
+ parameters: {
261
+ query: {
262
+ type: 'json',
263
+ required: true,
264
+ description: '{ action: "tools" | "call", tool?: string, arguments?: object }',
265
+ },
266
+ },
267
+ output: {
268
+ schema: { type: 'json' },
269
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
270
+ },
271
+ async execute(args: { query?: unknown }): Promise<Record<string, never>> {
272
+ const query = plainObject(args?.query) ? args.query : {}
273
+ if (query.action === 'tools') return jsonObject({ ok: true, tools: bridge.allowed_tools })
274
+ if (query.action !== 'call') return jsonObject({ ok: false, error: 'invalid_action' })
275
+ if (typeof query.tool !== 'string' || !bridge.allowed_tools.includes(query.tool)) {
276
+ return jsonObject({ ok: false, error: 'disallowed_tool' })
277
+ }
278
+ if (query.arguments !== undefined && !plainObject(query.arguments)) {
279
+ return jsonObject({ ok: false, error: 'invalid_arguments' })
280
+ }
281
+ const callArguments = query.arguments === undefined ? {} : query.arguments
282
+ const serialized = serializedArguments(callArguments)
283
+ if (serialized.reason) return jsonObject({ ok: false, error: 'invalid_arguments', reason: serialized.reason })
284
+ const controller = new AbortController()
285
+ const timer = setTimeout(() => {
286
+ controller.abort()
287
+ }, bridge.timeout_ms)
288
+ let handle: SubprocessHandle | undefined
289
+ try {
290
+ const env: NodeJS.ProcessEnv = Object.fromEntries(
291
+ Object.keys(process.env).map(key => [key, undefined]),
292
+ )
293
+ for (const key of ['PATH', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TZ']) {
294
+ if (process.env[key] !== undefined) env[key] = process.env[key]!
295
+ }
296
+ env.PYTHONDONTWRITEBYTECODE = '1'
297
+ env.PYTHONNOUSERSITE = '1'
298
+ handle = subprocess.spawn({
299
+ argv: [bridge.executable, ...bridge.argv_prefix, 'call', query.tool, serialized.json!],
300
+ cwd: bridge.cwd,
301
+ stdio: {
302
+ stdin: 'ignore',
303
+ stdout: { maxBytes: bridge.max_output_bytes },
304
+ stderr: { maxBytes: bridge.max_output_bytes },
305
+ },
306
+ graceMs: Math.min(1_000, bridge.timeout_ms),
307
+ env,
308
+ signal: controller.signal,
309
+ })
310
+ const status = await handle.done
311
+ if (controller.signal.aborted) return jsonObject({ ok: false, error: 'timed_out' })
312
+ const stdout = handle.collected.stdout.readFrom(0)
313
+ const stderr = handle.collected.stderr.readFrom(0)
314
+ if (stdout.lossy || stderr.lossy) return jsonObject({ ok: false, error: 'output_truncated' })
315
+ if (status.exitCode !== 0) {
316
+ return jsonObject({
317
+ ok: false,
318
+ error: 'runner_failed',
319
+ exit_code: status.exitCode,
320
+ signal: status.signal ?? null,
321
+ })
322
+ }
323
+ const parsed = structuredObject(stdout.text)
324
+ if (!parsed) return jsonObject({ ok: false, error: 'invalid_json' })
325
+ const outputInspection = inspectOutput(parsed)
326
+ if (outputInspection === 'forbidden_key') return jsonObject({ ok: false, error: 'forbidden_output' })
327
+ if (outputInspection === 'structure_limit') return jsonObject({ ok: false, error: 'invalid_output' })
328
+ const visibleResult = parsed.result ?? parsed
329
+ if (!plainObject(visibleResult) && !Array.isArray(visibleResult)) return jsonObject({ ok: false, error: 'invalid_output' })
330
+ const outputKeys = bridge.allowed_output_keys && Object.hasOwn(bridge.allowed_output_keys, query.tool)
331
+ ? bridge.allowed_output_keys[query.tool]
332
+ : undefined
333
+ if (outputKeys && !inspectAllowedOutput(visibleResult, outputKeys)) return jsonObject({ ok: false, error: 'forbidden_output' })
334
+ return jsonObject({ ok: true, result: visibleResult })
335
+ } catch {
336
+ if (controller.signal.aborted) return jsonObject({ ok: false, error: 'timed_out' })
337
+ return jsonObject({ ok: false, error: !handle || handle.pid === -1 ? 'spawn_failed' : 'runner_failed' })
338
+ } finally {
339
+ clearTimeout(timer)
340
+ }
341
+ },
342
+ }))
343
+ return Object.freeze({
344
+ toolName: 'gotry_benchmark_environment',
345
+ allowedTools: Object.freeze([...bridge.allowed_tools]),
346
+ terminal: Object.freeze({ ...bridge.terminal_output }),
347
+ })
348
+ }
@@ -0,0 +1,184 @@
1
+ /** Stable, redacted diagnostics carried on the benchmark-only control pipe. */
2
+
3
+ import { writeSync } from 'node:fs'
4
+
5
+ export const BENCHMARK_CHILD_DIAGNOSTIC_SCHEMA = 'gotry_benchmark_child_diagnostic_v1' as const
6
+ export const BENCHMARK_CHILD_DIAGNOSTIC_FD_ENV = 'GOTRY_BENCHMARK_DIAGNOSTIC_FD' as const
7
+ export const BENCHMARK_CHILD_DIAGNOSTIC_MAX_BYTES = 4 * 1024
8
+
9
+ export type BenchmarkChildFailureCode =
10
+ | 'child_output_truncated'
11
+ | 'child_signaled'
12
+ | 'child_lifecycle_failure'
13
+ | 'child_conformance_failure'
14
+ | 'child_bridge_timed_out'
15
+ | 'child_bridge_runner_failed'
16
+ | 'child_bridge_spawn_failed'
17
+ | 'child_bridge_output_truncated'
18
+ | 'child_bridge_failure'
19
+ | 'child_terminal_invalid'
20
+ | 'child_nonzero_exit'
21
+ | 'child_spawn_failure'
22
+ | 'child_model_auth'
23
+ | 'child_model_capacity'
24
+ | 'child_model_server'
25
+ | 'child_model_transport'
26
+ | 'child_model_stream'
27
+ | 'child_model_request'
28
+ | 'child_runtime_error'
29
+ | 'child_blocked'
30
+ | 'child_max_tokens'
31
+ | 'child_aborted'
32
+ | 'child_interrupted'
33
+
34
+ const FAILURE_CODES = new Set<BenchmarkChildFailureCode>([
35
+ 'child_output_truncated',
36
+ 'child_signaled',
37
+ 'child_lifecycle_failure',
38
+ 'child_conformance_failure',
39
+ 'child_bridge_timed_out',
40
+ 'child_bridge_runner_failed',
41
+ 'child_bridge_spawn_failed',
42
+ 'child_bridge_output_truncated',
43
+ 'child_bridge_failure',
44
+ 'child_terminal_invalid',
45
+ 'child_nonzero_exit',
46
+ 'child_spawn_failure',
47
+ 'child_model_auth',
48
+ 'child_model_capacity',
49
+ 'child_model_server',
50
+ 'child_model_transport',
51
+ 'child_model_stream',
52
+ 'child_model_request',
53
+ 'child_runtime_error',
54
+ 'child_blocked',
55
+ 'child_max_tokens',
56
+ 'child_aborted',
57
+ 'child_interrupted',
58
+ ])
59
+
60
+ export interface BenchmarkChildFailureInput {
61
+ readonly code?: number | null
62
+ readonly signal?: string | null
63
+ readonly diagnostic?: string
64
+ readonly outputTruncated?: boolean
65
+ }
66
+
67
+ type DiagnosticCode = BenchmarkChildFailureCode | undefined
68
+
69
+ const MODEL_AUTH = new Set(['AUTH', 'INVALID_CREDENTIAL', 'MISSING_CREDENTIAL'])
70
+ const MODEL_CAPACITY = new Set(['QUOTA', 'RATE_LIMIT'])
71
+ const MODEL_SERVER = new Set(['SERVER'])
72
+ const MODEL_TRANSPORT = new Set(['TRANSPORT', 'TIMEOUT'])
73
+ const MODEL_STREAM = new Set(['EMPTY_RESPONSE', 'STREAM_CLOSED', 'MALFORMED_RESPONSE', 'INVALID_RESPONSE'])
74
+ const MODEL_REQUEST = new Set(['INVALID_REQUEST', 'CONTEXT_WINDOW_EXCEEDED', 'NO_ADAPTER', 'UNKNOWN_MODEL', 'UNSUPPORTED_OPTION'])
75
+
76
+ /** Classify only the structured final turn/end envelope; free-form fields are ignored. */
77
+ export function classifyBenchmarkTurnEnd(reason: unknown): DiagnosticCode {
78
+ if (!plainObject(reason) || typeof reason.kind !== 'string') return 'child_runtime_error'
79
+ switch (reason.kind) {
80
+ case 'completed': return undefined
81
+ case 'blocked': return 'child_blocked'
82
+ case 'max-tokens': return 'child_max_tokens'
83
+ case 'aborted': return 'child_aborted'
84
+ case 'interrupted': return 'child_interrupted'
85
+ case 'error': {
86
+ const error = plainObject(reason.error) ? reason.error : {}
87
+ const code = typeof error.code === 'string' ? error.code : ''
88
+ const status = typeof error.status === 'number' && Number.isInteger(error.status) && error.status >= 100 && error.status <= 599
89
+ ? error.status
90
+ : undefined
91
+ if (MODEL_AUTH.has(code)) return 'child_model_auth'
92
+ if (MODEL_CAPACITY.has(code)) return 'child_model_capacity'
93
+ if (MODEL_SERVER.has(code)) return 'child_model_server'
94
+ if (MODEL_TRANSPORT.has(code)) return 'child_model_transport'
95
+ if (MODEL_STREAM.has(code)) return 'child_model_stream'
96
+ if (MODEL_REQUEST.has(code)) return 'child_model_request'
97
+ if (code === 'ABORTED') return 'child_aborted'
98
+ if (status === 401 || status === 403) return 'child_model_auth'
99
+ if (status === 429) return 'child_model_capacity'
100
+ if (status !== undefined && status >= 500 && status <= 599) return 'child_model_server'
101
+ return 'child_runtime_error'
102
+ }
103
+ default: return 'child_runtime_error'
104
+ }
105
+ }
106
+
107
+ /** Per-session final arbiter. It emits one best closed enum, never its payload. */
108
+ export function createBenchmarkDiagnosticArbiter(write: (code: BenchmarkChildFailureCode) => void) {
109
+ const pending = new Map<string, BenchmarkChildFailureCode>()
110
+ const emitted = new Set<string>()
111
+ const rank = (code: BenchmarkChildFailureCode): number => code.startsWith('child_bridge_') ? 4 : code === 'child_conformance_failure' ? 3 : code === 'child_runtime_error' ? 1 : 2
112
+ return {
113
+ offer(session: string, code: BenchmarkChildFailureCode | undefined): void {
114
+ if (code === undefined || emitted.has(session)) return
115
+ const previous = pending.get(session)
116
+ if (previous === undefined || rank(code) > rank(previous)) pending.set(session, code)
117
+ },
118
+ flush(session: string): void {
119
+ if (emitted.has(session)) return
120
+ const code = pending.get(session)
121
+ if (code === undefined) return
122
+ emitted.add(session)
123
+ pending.delete(session)
124
+ write(code)
125
+ },
126
+ }
127
+ }
128
+
129
+ function plainObject(value: unknown): value is Record<string, unknown> {
130
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
131
+ }
132
+
133
+ /** Keep the newest bounded bytes so a final control record cannot be displaced by noise. */
134
+ export function appendBoundedChildDiagnostic(current: Buffer, chunk: Buffer | string): Buffer {
135
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
136
+ if (bytes.length >= BENCHMARK_CHILD_DIAGNOSTIC_MAX_BYTES) {
137
+ return bytes.subarray(bytes.length - BENCHMARK_CHILD_DIAGNOSTIC_MAX_BYTES)
138
+ }
139
+ if (current.length + bytes.length <= BENCHMARK_CHILD_DIAGNOSTIC_MAX_BYTES) {
140
+ return Buffer.concat([current, bytes])
141
+ }
142
+ const keep = BENCHMARK_CHILD_DIAGNOSTIC_MAX_BYTES - bytes.length
143
+ return Buffer.concat([current.subarray(current.length - keep), bytes])
144
+ }
145
+
146
+ export function parseBenchmarkChildDiagnostic(raw: string): BenchmarkChildFailureCode | undefined {
147
+ const lines = raw.split('\n').map(line => line.trim()).filter(Boolean)
148
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
149
+ try {
150
+ const parsed: unknown = JSON.parse(lines[index])
151
+ if (!plainObject(parsed)) continue
152
+ const keys = Object.keys(parsed).sort()
153
+ if (JSON.stringify(keys) !== JSON.stringify(['code', 'schema_version'])) continue
154
+ if (parsed.schema_version !== BENCHMARK_CHILD_DIAGNOSTIC_SCHEMA) continue
155
+ if (typeof parsed.code !== 'string' || !FAILURE_CODES.has(parsed.code as BenchmarkChildFailureCode)) continue
156
+ return parsed.code as BenchmarkChildFailureCode
157
+ } catch {
158
+ // The control pipe is fail-closed: malformed or unrelated records are ignored.
159
+ }
160
+ }
161
+ return undefined
162
+ }
163
+
164
+ /** Emit one namespaced enum record without reflecting prompts, paths, or stderr. */
165
+ export function emitBenchmarkChildDiagnostic(code: BenchmarkChildFailureCode): void {
166
+ const fd = Number(process.env[BENCHMARK_CHILD_DIAGNOSTIC_FD_ENV])
167
+ if (!Number.isInteger(fd) || fd < 3) return
168
+ const record = `\n${JSON.stringify({ schema_version: BENCHMARK_CHILD_DIAGNOSTIC_SCHEMA, code })}\n`
169
+ if (Buffer.byteLength(record, 'utf8') > BENCHMARK_CHILD_DIAGNOSTIC_MAX_BYTES) return
170
+ try {
171
+ writeSync(fd, record)
172
+ } catch {
173
+ // Diagnostics never change the conformance decision itself.
174
+ }
175
+ }
176
+
177
+ export function classifyBenchmarkChildFailure(input: BenchmarkChildFailureInput): BenchmarkChildFailureCode {
178
+ if (input.outputTruncated) return 'child_output_truncated'
179
+ if (input.signal) return 'child_signaled'
180
+ const structured = parseBenchmarkChildDiagnostic(input.diagnostic ?? '')
181
+ if (structured) return structured
182
+ if (input.code !== 0) return 'child_nonzero_exit'
183
+ return 'child_lifecycle_failure'
184
+ }