@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,196 @@
1
+ /** Production composition entry for GoTry's BFF-only Booking Copilot server. */
2
+
3
+ import { mkdirSync } from 'node:fs'
4
+ import { isAbsolute, resolve } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { ensureLedger, type StateLedger } from '../state-ledger.ts'
7
+ import {
8
+ buildDshPlannerEnvironment,
9
+ createDshEmbeddedBookingPlanner,
10
+ createDshEmbeddedBookingPlannerV2,
11
+ type DshEmbeddedBookingPlannerHandleV1,
12
+ type DshEmbeddedBookingPlannerHandleV2,
13
+ type DshEmbeddedBookingPlannerOptionsV1,
14
+ } from './dsh-planner.ts'
15
+ import { BookingCopilotTaskRuntime } from './runtime.ts'
16
+ import { BookingCopilotTaskRuntimeV2 } from './runtime-v2.ts'
17
+ import { BOOKING_COPILOT_V2_INGRESS_MODES, type BookingCopilotV2IngressMode, type BookingIngressBindingV2, type BookingIngressPrincipalV2 } from './contracts-v2.ts'
18
+ import {
19
+ startBookingCopilotServer,
20
+ type BookingCopilotServerHandleV1,
21
+ type BookingCopilotServerOptionsV1,
22
+ } from './server.ts'
23
+
24
+ export interface BookingCopilotStartupConfigV1 {
25
+ apiKey: string
26
+ stateRoot: string
27
+ host: string
28
+ port: number
29
+ artifactId?: string
30
+ /** Defaults to bound-turn-only; a complete injected seam enables ingress. */
31
+ ingressMode: BookingCopilotV2IngressMode
32
+ }
33
+
34
+ export interface BookingCopilotStartupHandleV1 {
35
+ port: number
36
+ close(): Promise<void>
37
+ }
38
+
39
+ export interface BookingCopilotStartupDependenciesV1 {
40
+ ensureLedger(stateRoot: string): StateLedger
41
+ runtimeFactory(ledger: StateLedger): BookingCopilotTaskRuntime
42
+ createPlanner(options: DshEmbeddedBookingPlannerOptionsV1): Promise<DshEmbeddedBookingPlannerHandleV1>
43
+ runtimeFactoryV2?: (ledger: StateLedger) => BookingCopilotTaskRuntimeV2
44
+ createPlannerV2?: (options: DshEmbeddedBookingPlannerOptionsV1) => Promise<DshEmbeddedBookingPlannerHandleV2>
45
+ /** Optional in-process BFF identity seam; standalone v2 uses bound-turn-only mode. */
46
+ ingressBinding?: BookingIngressBindingV2
47
+ principal?: BookingIngressPrincipalV2
48
+ startServer(options: BookingCopilotServerOptionsV1): Promise<BookingCopilotServerHandleV1>
49
+ }
50
+
51
+ const DEFAULT_DEPENDENCIES: BookingCopilotStartupDependenciesV1 = {
52
+ ensureLedger,
53
+ runtimeFactory: (ledger) => new BookingCopilotTaskRuntime(ledger),
54
+ createPlanner: createDshEmbeddedBookingPlanner,
55
+ runtimeFactoryV2: (ledger) => new BookingCopilotTaskRuntimeV2(ledger),
56
+ createPlannerV2: createDshEmbeddedBookingPlannerV2,
57
+ startServer: startBookingCopilotServer,
58
+ }
59
+
60
+ function parsePort(raw: string | undefined): number {
61
+ if (raw === undefined || raw === '') return 3_082
62
+ const value = Number(raw)
63
+ if (!Number.isSafeInteger(value) || value < 0 || value > 65_535) throw new Error('booking_copilot_invalid_port')
64
+ return value
65
+ }
66
+
67
+ export function resolveBookingCopilotStartupConfig(
68
+ env: Record<string, string | undefined> = process.env,
69
+ ): BookingCopilotStartupConfigV1 {
70
+ const apiKey = env.GOTRY_BOOKING_COPILOT_API_KEY ?? ''
71
+ if (!apiKey) throw new Error('booking_copilot_api_key_required')
72
+ const stateRoot = env.GOTRY_BOOKING_COPILOT_STATE_ROOT ?? ''
73
+ if (!stateRoot) throw new Error('booking_copilot_state_root_required')
74
+ if (!isAbsolute(stateRoot)) throw new Error('booking_copilot_state_root_must_be_absolute')
75
+ const artifactId = env.GOTRY_BOOKING_COPILOT_ARTIFACT_ID || undefined
76
+ if (artifactId !== undefined && !/^[0-9a-f]{40}$/.test(artifactId)) {
77
+ throw new Error('booking_copilot_artifact_id_invalid')
78
+ }
79
+ const rawIngressMode = env.GOTRY_BOOKING_COPILOT_V2_INGRESS_MODE
80
+ const ingressMode = rawIngressMode === undefined || rawIngressMode === '' ? 'bff-bound-turn-only' : rawIngressMode
81
+ if (!(BOOKING_COPILOT_V2_INGRESS_MODES as readonly string[]).includes(ingressMode)) {
82
+ throw new Error('booking_copilot_v2_ingress_mode_invalid')
83
+ }
84
+ return {
85
+ apiKey,
86
+ stateRoot,
87
+ host: env.GOTRY_BOOKING_COPILOT_HOST || '127.0.0.1',
88
+ port: parsePort(env.GOTRY_BOOKING_COPILOT_PORT),
89
+ artifactId,
90
+ ingressMode: ingressMode as BookingCopilotV2IngressMode,
91
+ }
92
+ }
93
+
94
+ async function closeAll(
95
+ server: BookingCopilotServerHandleV1 | undefined,
96
+ planner: DshEmbeddedBookingPlannerHandleV1 | DshEmbeddedBookingPlannerHandleV2 | undefined,
97
+ plannerV2: DshEmbeddedBookingPlannerHandleV2 | undefined,
98
+ ledger: StateLedger | undefined,
99
+ ): Promise<void> {
100
+ const failures: unknown[] = []
101
+ for (const close of [
102
+ server ? () => server.close() : undefined,
103
+ planner ? () => planner.close() : undefined,
104
+ plannerV2 ? () => plannerV2.close() : undefined,
105
+ ledger ? () => Promise.resolve(ledger.close()) : undefined,
106
+ ]) {
107
+ if (!close) continue
108
+ try { await close() } catch (error) { failures.push(error) }
109
+ }
110
+ if (failures.length === 1) throw failures[0]
111
+ if (failures.length > 1) throw new AggregateError(failures, 'booking_copilot_shutdown_failed')
112
+ }
113
+
114
+ /**
115
+ * Boot one process-scoped dsh core and task-scoped sessions behind the typed
116
+ * HTTP/SSE server. Only the HTTP layer receives the BFF deployment key.
117
+ */
118
+ export async function startBookingCopilotFromEnvironment(
119
+ env: Record<string, string | undefined> = process.env,
120
+ dependencies: BookingCopilotStartupDependenciesV1 = DEFAULT_DEPENDENCIES,
121
+ ): Promise<BookingCopilotStartupHandleV1> {
122
+ const config = resolveBookingCopilotStartupConfig(env)
123
+ const activeDependencies = dependencies
124
+ const hasIngressBinding = Boolean(activeDependencies.ingressBinding)
125
+ const hasPrincipal = Boolean(activeDependencies.principal)
126
+ if (hasIngressBinding !== hasPrincipal) throw new Error('booking_copilot_v2_ingress_binding_pair_required')
127
+ const ingressPair = hasIngressBinding && hasPrincipal
128
+ if (ingressPair && env.GOTRY_BOOKING_COPILOT_V2_INGRESS_MODE !== undefined && config.ingressMode === 'bff-bound-turn-only') {
129
+ throw new Error('booking_copilot_v2_ingress_mode_conflict')
130
+ }
131
+ if (!ingressPair && config.ingressMode === 'bff-ingress-binding') throw new Error('booking_copilot_v2_ingress_binding_required')
132
+ if (ingressPair && (typeof activeDependencies.ingressBinding?.bind !== 'function'
133
+ || typeof activeDependencies.principal?.subject !== 'string' || activeDependencies.principal.subject.length === 0 || activeDependencies.principal.subject.length > 256
134
+ || typeof activeDependencies.principal?.scope !== 'string' || activeDependencies.principal.scope.length === 0 || activeDependencies.principal.scope.length > 256)) {
135
+ throw new Error('booking_copilot_v2_ingress_binding_invalid')
136
+ }
137
+ const activeIngressMode: BookingCopilotV2IngressMode = ingressPair ? 'bff-ingress-binding' : 'bff-bound-turn-only'
138
+ mkdirSync(config.stateRoot, { recursive: true })
139
+ let ledger: StateLedger | undefined
140
+ let planner: DshEmbeddedBookingPlannerHandleV1 | undefined
141
+ let plannerV2: DshEmbeddedBookingPlannerHandleV2 | undefined
142
+ let server: BookingCopilotServerHandleV1 | undefined
143
+ try {
144
+ ledger = dependencies.ensureLedger(config.stateRoot)
145
+ const plannerOptions = { stateRoot: config.stateRoot, env: buildDshPlannerEnvironment(env) }
146
+ if (!activeDependencies.runtimeFactoryV2 || !activeDependencies.createPlannerV2) throw new Error('booking_copilot_v2_startup_dependency_missing')
147
+ const runtime = activeDependencies.runtimeFactory(ledger)
148
+ planner = await activeDependencies.createPlanner(plannerOptions)
149
+ const runtimeV2 = activeDependencies.runtimeFactoryV2(ledger)
150
+ plannerV2 = await activeDependencies.createPlannerV2(plannerOptions)
151
+ server = await activeDependencies.startServer({
152
+ apiKey: config.apiKey, runtime, plannerFactory: planner.plannerFactory,
153
+ v2: { runtime: runtimeV2, plannerFactory: plannerV2.plannerFactory, ingressBinding: activeDependencies.ingressBinding, principal: activeDependencies.principal, ingressMode: activeIngressMode },
154
+ host: config.host, port: config.port, artifactId: config.artifactId,
155
+ })
156
+ } catch (error) {
157
+ try { await closeAll(server, planner, plannerV2, ledger) } catch (cleanupError) {
158
+ throw new AggregateError([error, cleanupError], 'booking_copilot_startup_and_cleanup_failed')
159
+ }
160
+ throw error
161
+ }
162
+
163
+ let closed = false
164
+ return {
165
+ port: server.port,
166
+ async close() {
167
+ if (closed) return
168
+ closed = true
169
+ await closeAll(server, planner, plannerV2, ledger)
170
+ },
171
+ }
172
+ }
173
+
174
+ async function runCli(): Promise<void> {
175
+ const handle = await startBookingCopilotFromEnvironment()
176
+ process.stderr.write(`[gotry-booking-copilot] listening on http://127.0.0.1:${handle.port}/a2a/booking-copilot/turn\n`)
177
+ let stopping = false
178
+ const stop = () => {
179
+ if (stopping) return
180
+ stopping = true
181
+ void handle.close().then(() => process.exit(0), (error) => {
182
+ process.stderr.write(`[gotry-booking-copilot] shutdown failed: ${error instanceof Error ? error.message : String(error)}\n`)
183
+ process.exit(1)
184
+ })
185
+ }
186
+ process.once('SIGINT', stop)
187
+ process.once('SIGTERM', stop)
188
+ }
189
+
190
+ const invokedPath = process.argv[1] ? resolve(process.argv[1]) : ''
191
+ if (invokedPath === fileURLToPath(import.meta.url)) {
192
+ runCli().catch((error) => {
193
+ process.stderr.write(`[gotry-booking-copilot] startup failed: ${error instanceof Error ? error.message : String(error)}\n`)
194
+ process.exit(1)
195
+ })
196
+ }
@@ -0,0 +1,205 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { Ajv2020, type ValidateFunction } from 'ajv/dist/2020.js'
3
+ import { BOOKING_READ_ACTION_KINDS_V2, BOOKING_V2_BLOCKER_CODES, BOOKING_V2_GAP_CODES } from './contracts-v2.ts'
4
+
5
+ const schema = JSON.parse(readFileSync(new URL('../../../schemas/booking.surface.v2.schema.json', import.meta.url), 'utf8')) as Record<string, unknown>
6
+ const v1 = JSON.parse(readFileSync(new URL('../../../schemas/booking.surface.v1.schema.json', import.meta.url), 'utf8')) as Record<string, unknown>
7
+ const ajv = new Ajv2020({ allErrors: true, strict: true })
8
+ ajv.addSchema(v1)
9
+ ajv.addSchema(schema)
10
+ const validate: ValidateFunction = ajv.compile(schema)
11
+ const validateActionCanonical = ajv.compile({ $ref: `${schema.$id}#/$defs/Action` })
12
+ const validateBlockerCanonical = ajv.compile({ $ref: `${schema.$id}#/$defs/Blocker` })
13
+ const validateApprovalCanonical = ajv.compile({ $ref: `${schema.$id}#/$defs/Approval` })
14
+ const validateEventCanonical = ajv.compile({ $ref: `${schema.$id}#/$defs/Event` })
15
+ const kinds = new Set<string>(BOOKING_READ_ACTION_KINDS_V2)
16
+ const codes = new Set<string>(BOOKING_V2_BLOCKER_CODES)
17
+ const gapCodes = new Set<string>(BOOKING_V2_GAP_CODES)
18
+ const safeOpaque = (value: unknown): value is string => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(value)
19
+ const safeRequestKey = (value: unknown): value is string => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{7,127}$/.test(value) && !secret.test(value)
20
+ const secret = /(?:Bearer\s+\S|\bsk-(?!ill(?:s)?\b)[A-Za-z0-9_-]{12,}|\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/i
21
+ // `.example` is reserved for documentation, so these identifiers cannot be a
22
+ // real mailbox. Do not allowlist lookalikes on routable domains.
23
+ const technicalEmail = /^(?:schema|version)@[a-z0-9-]+\.example$/i
24
+ const currencyAmount = /\b(?:[A-Z]{3}\s*\d+(?:\.\d+)?|\d+(?:\.\d+)?\s*[A-Z]{3})\b/
25
+ const rfc3339DateTime = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|([+-])(\d{2}):(\d{2}))$/
26
+
27
+ export function isBookingDateTimeV2(value: unknown): value is string {
28
+ if (typeof value !== 'string') return false
29
+ const match = rfc3339DateTime.exec(value)
30
+ if (!match) return false
31
+ const [, yearText, monthText, dayText, hourText, minuteText, secondText] = match
32
+ const year = Number(yearText)
33
+ const month = Number(monthText)
34
+ const day = Number(dayText)
35
+ const hour = Number(hourText)
36
+ const minute = Number(minuteText)
37
+ const second = Number(secondText)
38
+ const offsetHour = match[9] === undefined ? 0 : Number(match[9])
39
+ const offsetMinute = match[10] === undefined ? 0 : Number(match[10])
40
+ if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) return false
41
+ const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate()
42
+ if (day < 1 || day > daysInMonth) return false
43
+ return !Number.isNaN(Date.parse(value))
44
+ }
45
+
46
+ function normalizedWords(value: string): string {
47
+ return value.normalize('NFKC').replace(/[^\p{L}\p{N}.]+/gu, ' ').trim()
48
+ }
49
+
50
+ function hasSupplierCost(value: string): boolean {
51
+ const normalized = normalizedWords(value)
52
+ const lower = normalized.toLowerCase()
53
+ return (
54
+ /\b(?:supplier|vendor|internal)\b/.test(lower) &&
55
+ /\b(?:cost|rate|price)\b/.test(lower) &&
56
+ currencyAmount.test(normalized.toUpperCase())
57
+ )
58
+ }
59
+
60
+ function hasInternationalPhone(value: string): boolean {
61
+ const normalized = value.normalize('NFKC')
62
+ for (const plusIndex of normalized.split('').flatMap((char, index) => char === '+' ? [index] : [])) {
63
+ let digits = ''
64
+ for (const char of normalized.slice(plusIndex + 1, plusIndex + 48)) {
65
+ if (/\d/u.test(char)) digits += char
66
+ else if (/\p{L}|@/u.test(char)) break
67
+ if (digits.length > 15) break
68
+ }
69
+ if (/^[1-9]\d{7,14}$/.test(digits)) return true
70
+ }
71
+ return false
72
+ }
73
+
74
+ function hasPaymentCard(value: string): boolean {
75
+ const normalized = value.normalize('NFKC')
76
+ const candidates = normalized.match(/(?:\d[ -]?){12,18}\d/g) ?? []
77
+ const explicitlyLabeled = /\b(?:card|payment|visa|mastercard)\b/i.test(normalized)
78
+ return candidates.some((candidate) => (explicitlyLabeled || /[ -]/.test(candidate)) && luhn(candidate))
79
+ }
80
+
81
+ function hasSensitiveText(value: string, opts: { allowOpaqueNumericId?: boolean } = {}): boolean {
82
+ const normalized = value.normalize('NFKC')
83
+ if (normalized.includes('@') && !technicalEmail.test(normalized.trim())) return true
84
+ return secret.test(normalized) || hasInternationalPhone(normalized) || hasSupplierCost(normalized) || (!opts.allowOpaqueNumericId && hasPaymentCard(normalized))
85
+ }
86
+ function luhn(value: string): boolean { const digits = value.replaceAll(/\D/g, ''); if (digits.length < 13 || digits.length > 19) return false; let sum = 0; for (let i = digits.length - 1, parity = 0; i >= 0; i--, parity++) { let n = Number(digits[i]); if (parity % 2 === 1) { n *= 2; if (n > 9) n -= 9 } sum += n } return sum % 10 === 0 }
87
+ const opaqueFieldNames = new Set([
88
+ 'eventId', 'taskId', 'contextRef', 'actionId', 'hotelRef', 'offerRef', 'offerVersionRef', 'checkedOfferVersionRef', 'currentOfferVersionRef', 'verifiedOfferRef',
89
+ 'orderRef', 'searchSessionRef', 'handoffRef', 'questionId', 'blockerId', 'approvalId',
90
+ 'sourceActionId', 'targetActionId', 'sourceTurnId', 'turnId', 'requestKey', 'taskHandle',
91
+ 'nonce', 'deliveryNonce', 'optionDigest', 'sourceReceiptDigest', 'valueDigest',
92
+ ])
93
+ function isOpaqueFieldPath(path: string): boolean {
94
+ const clean = path.replace(/\[\]$/g, '')
95
+ const field = clean.split('.').at(-1)
96
+ return field ? opaqueFieldNames.has(field) || field.endsWith('Ref') || field.endsWith('Refs') : false
97
+ }
98
+ const scan = (v: unknown, path = '$'): boolean => typeof v === 'string'
99
+ ? hasSensitiveText(v, { allowOpaqueNumericId: isOpaqueFieldPath(path) })
100
+ : Array.isArray(v)
101
+ ? v.some((child) => scan(child, `${path}[]`))
102
+ : !!v && typeof v === 'object'
103
+ ? Object.entries(v).some(([k,x]) => /cost|price/i.test(k) ? /supplier|internal|wholesale/i.test(String(x)) : scan(x, `${path}.${k}`))
104
+ : false
105
+ export function isSafeBookingRequestKeyV2(value: unknown): value is string { return safeRequestKey(value) }
106
+ export type BookingSurfaceV2ValidationResult = { ok: true } | { ok: false; errors: string[] }
107
+ function errors(v: ValidateFunction): string[] { return (v.errors ?? []).map(e => `${e.instancePath || '$'}: ${e.message ?? e.keyword}`) }
108
+ function workspaceErrors(workspace: Record<string, any> | undefined, path: string): string[] {
109
+ const e: string[] = []
110
+ if (!workspace || typeof workspace !== 'object' || Array.isArray(workspace)) return e
111
+ const loadedOffers = Array.isArray(workspace.loadedOffers) ? workspace.loadedOffers : []
112
+ const offerRefs = loadedOffers.map((offer: any) => offer?.offerRef).filter((ref: unknown): ref is string => typeof ref === 'string')
113
+ const offerVersionRefs = loadedOffers.map((offer: any) => offer?.offerVersionRef).filter((ref: unknown): ref is string => typeof ref === 'string')
114
+ if (new Set(offerRefs).size !== offerRefs.length) e.push(`${path}.loadedOffers: duplicate offerRef`)
115
+ if (new Set(offerVersionRefs).size !== offerVersionRefs.length) e.push(`${path}.loadedOffers: duplicate offerVersionRef`)
116
+ const verified = workspace.verifiedOffer
117
+ if (verified !== undefined) {
118
+ if (!loadedOffers.some((offer: any) => offer?.offerRef === verified.offerRef && offer?.offerVersionRef === verified.offerVersionRef)) e.push(`${path}.verifiedOffer: loaded offer version required`)
119
+ if (!safeOpaque(verified.verifiedOfferRef) || !isBookingDateTimeV2(verified.expiresAt)) e.push(`${path}.verifiedOffer: safe ref and parseable expiry required`)
120
+ }
121
+ return e
122
+ }
123
+ function cross(value: unknown): string[] {
124
+ const e: string[] = []
125
+ if (scan(value)) e.push('$: unsafe PII/payment/supplier-cost/credential text')
126
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return e
127
+ const x = value as Record<string, any>
128
+ const semantic = x.kind === 'action.receipt.continuation' ? x.receipt : x
129
+ if (x.workspace && x.kind !== 'user.turn.ingress') e.push(...workspaceErrors(x.workspace, '$.workspace'))
130
+ if (x.schemaVersion === 'booking.surface.v2' && Array.isArray(x.loadedOffers)) e.push(...workspaceErrors(x, '$'))
131
+ if (x.kind === 'user.turn.ingress') {
132
+ if (!safeRequestKey(x.requestKey)) e.push('$.requestKey: safe opaque request key required')
133
+ const workspace = x.workspace as Record<string, any> | undefined
134
+ e.push(...workspaceErrors(workspace, '$.workspace'))
135
+ const visibleHotels = Array.isArray(workspace?.visibleHotels) ? workspace.visibleHotels : []
136
+ const loadedOffers = Array.isArray(workspace?.loadedOffers) ? workspace.loadedOffers : []
137
+ if (workspace?.focusedHotelRef !== undefined && !visibleHotels.some((hotel: any) => hotel?.hotelRef === workspace.focusedHotelRef)) e.push('$.workspace.focusedHotelRef: visible hotel required')
138
+ const selectedOffer = workspace?.selectedOfferRef === undefined ? undefined : loadedOffers.find((offer: any) => offer?.offerRef === workspace.selectedOfferRef)
139
+ if (workspace?.selectedOfferRef !== undefined && !selectedOffer) e.push('$.workspace.selectedOfferRef: loaded offer required')
140
+ if (selectedOffer && !visibleHotels.some((hotel: any) => hotel?.hotelRef === selectedOffer.hotelRef)) e.push('$.workspace.selectedOfferRef: offer hotel must be visible')
141
+ }
142
+ if (semantic !== x && semantic?.resultContract?.hardCriteriaMet === true && semantic.resultContract.blockers?.length) e.push('$.receipt.resultContract: hardCriteriaMet cannot coexist with blockers')
143
+ if (kinds.has(x.kind)) {
144
+ const allowed: Record<string, string[]> = {
145
+ 'search.patch': ['patch'], 'search.run': [], 'results.view.patch': ['patch'],
146
+ 'hotel.focus': ['hotelRef'], 'hotel.select': ['hotelRef'],
147
+ 'offers.query': ['hotelRefs', 'criteria'], 'offers.view.patch': ['hotelRef', 'criteria'],
148
+ 'offers.compare': ['offerRefs', 'requestedCount'], 'offer.select': ['offerRef', 'offerVersionRef'],
149
+ 'offer.check': ['offerRef', 'offerVersionRef'], 'checkout.prepare': ['offerRef', 'offerVersionRef', 'verifiedOfferRef'], 'order.observe': ['orderRef'],
150
+ }
151
+ const input = x.input
152
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(k => !allowed[x.kind].includes(k))) e.push('$.input: closed kind-specific input required')
153
+ }
154
+ if (x.kind === 'action.receipt' && x.resultContract?.hardCriteriaMet === true && x.resultContract.blockers?.length) e.push('$.resultContract: hardCriteriaMet cannot coexist with blockers')
155
+ if (x.kind === 'user.turn' && Array.isArray(x.request?.approval)) e.push('$.request.approval: at most one approval')
156
+ if (x.kind === 'action.receipt' || x.kind === 'action.receipt.continuation') {
157
+ const r = x.kind === 'action.receipt.continuation' ? x.receipt : x
158
+ if (r?.resultContract?.gapCodes?.some((code: unknown) => !gapCodes.has(String(code)))) e.push('$.resultContract.gapCodes: unregistered gap code')
159
+ const observation = r?.observation
160
+ if (observation?.kind === 'offer.availability') {
161
+ if (r.status === 'applied' && (observation.available !== true || !observation.verifiedOfferRef || observation.currentOfferVersionRef !== observation.checkedOfferVersionRef || observation.changedFactRefs?.length || observation.gapCodes?.length)) e.push('$.observation: applied availability requires unchanged checked/current version, verified ref, and no gaps')
162
+ if (r.status === 'applied' && (r.resultContract?.outcome !== 'complete' || r.resultContract?.hardCriteriaMet !== true || r.resultContract?.gapCodes?.length || r.resultContract?.blockers?.length)) e.push('$.resultContract: applied availability requires complete hard-criteria result without gaps or blockers')
163
+ if (r.status === 'changed' && (observation.available !== true || !observation.currentOfferVersionRef || observation.currentOfferVersionRef === observation.checkedOfferVersionRef || !observation.changedFactRefs?.length || observation.verifiedOfferRef)) e.push('$.observation: changed availability requires available replacement current version without verified ref')
164
+ if ((r.status === 'unavailable' || r.status === 'no_match') && (observation.available !== false || observation.currentOfferVersionRef || observation.verifiedOfferRef)) e.push('$.observation: negative availability must not expose current or verified version')
165
+ if (r.status !== 'applied' && observation.verifiedOfferRef) e.push('$.observation: only applied availability may expose verified ref')
166
+ }
167
+ }
168
+ const blocker = x.blocker ?? x.question?.blocker
169
+ if (blocker && !/^(searchDraft|results|offers|checkout)(\.[A-Za-z][A-Za-z0-9_]*){0,3}$/.test(blocker.criterionPath ?? '')) e.push('$.blocker.criterionPath: closed criterion path required')
170
+ if (blocker?.evidence?.factRefs && new Set(blocker.evidence.factRefs).size !== blocker.evidence.factRefs.length) e.push('$.blocker.evidence.factRefs: unique refs required')
171
+ if (x.kind === 'question' && blocker && Array.isArray(x.question?.approvalOptions)) {
172
+ for (const [i, option] of x.question.approvalOptions.entries()) {
173
+ const result = validateApprovalAgainstBlocker(option?.approval, blocker)
174
+ if (!result.ok) e.push(...result.errors.map(error => `$.question.approvalOptions[${i}].${error.slice(2)}`))
175
+ }
176
+ }
177
+ if (kinds.has(String(x.kind)) && x.relaxationApprovalRef) {
178
+ const ref = x.relaxationApprovalRef
179
+ if (ref.contextRef !== x.contextRef) e.push('$.relaxationApprovalRef.contextRef: must match action')
180
+ if (ref.sourceRevision !== x.expectedRevision) e.push('$.relaxationApprovalRef.sourceRevision: must equal action revision')
181
+ if (ref.targetActionKind !== x.kind) e.push('$.relaxationApprovalRef.targetActionKind: must match action kind')
182
+ if (ref.targetActionId !== x.actionId) e.push('$.relaxationApprovalRef.targetActionId: must match action id')
183
+ // Expiry and one-time consumption are Phase B runtime concerns; Phase A validates only structure/binding.
184
+ if (!safeOpaque(ref.nonce) || !isBookingDateTimeV2(ref.expiresAt)) e.push('$.relaxationApprovalRef: invalid expiry or nonce')
185
+ }
186
+ if (x.kind === 'question' && (!Array.isArray(x.question?.approvalOptions) || x.question.approvalOptions.length < 1 || x.question.approvalOptions.length > 2)) e.push('$.question.approvalOptions: one or two options required')
187
+ return e
188
+ }
189
+ export function validateBookingSurfaceV2(value: unknown): BookingSurfaceV2ValidationResult { const ok = validate(value); const e = ok ? [] : errors(validate); e.push(...cross(value)); return e.length ? { ok: false, errors: e } : { ok: true } }
190
+ function fromCanonical(fn: ValidateFunction, value: unknown): BookingSurfaceV2ValidationResult { const ok = fn(value); const e = ok ? [] : errors(fn); e.push(...cross(value)); return e.length ? { ok: false, errors: e } : { ok: true } }
191
+ export function validateBookingReadActionV2(value: unknown): BookingSurfaceV2ValidationResult { return fromCanonical(validateActionCanonical, value) }
192
+ export function validateCriterionBlockerV2(value: unknown): BookingSurfaceV2ValidationResult { return fromCanonical(validateBlockerCanonical, value) }
193
+ export function validateRelaxationApprovalV2(value: unknown): BookingSurfaceV2ValidationResult { return fromCanonical(validateApprovalCanonical, value) }
194
+ export function validateBookingSurfaceEventV2(value: unknown): BookingSurfaceV2ValidationResult { return fromCanonical(validateEventCanonical, value) }
195
+ export function validateApprovalAgainstBlocker(approval: unknown, blocker: unknown): BookingSurfaceV2ValidationResult {
196
+ const a = approval as any
197
+ const b = blocker as any
198
+ const blockerResult = validateCriterionBlockerV2(b)
199
+ if (!blockerResult.ok) return blockerResult
200
+ const approvalResult = validateRelaxationApprovalV2(a)
201
+ if (!approvalResult.ok) return approvalResult
202
+ const fields = ['blockerId', 'sourceActionId', 'sourceReceiptDigest', 'scope', 'code', 'criterionPath', 'valueDigest']
203
+ const mismatches = fields.filter(field => a[field] !== b?.[field])
204
+ return mismatches.length ? { ok: false, errors: mismatches.map(field => `$.${field}: approval does not match blocker`) } : { ok: true }
205
+ }