dextunnel 0.1.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 (76) hide show
  1. package/LICENSE +211 -0
  2. package/README.md +112 -0
  3. package/SECURITY.md +27 -0
  4. package/SUPPORT.md +43 -0
  5. package/package.json +44 -0
  6. package/public/client-shared.js +1831 -0
  7. package/public/favicon.svg +11 -0
  8. package/public/host.html +29 -0
  9. package/public/host.js +2079 -0
  10. package/public/index.html +28 -0
  11. package/public/index.js +98 -0
  12. package/public/live-bridge-lifecycle.js +258 -0
  13. package/public/live-bridge-retry-state.js +61 -0
  14. package/public/live-selection-intent.js +79 -0
  15. package/public/remote-operator-state.js +316 -0
  16. package/public/remote.html +167 -0
  17. package/public/remote.js +3967 -0
  18. package/public/styles.css +2793 -0
  19. package/public/surface-view-state.js +89 -0
  20. package/public/voice-dictation.js +45 -0
  21. package/src/bin/desktop-rehydration-smoke.mjs +111 -0
  22. package/src/bin/dextunnel.mjs +41 -0
  23. package/src/bin/doctor.mjs +48 -0
  24. package/src/bin/launch-attest.mjs +39 -0
  25. package/src/bin/launch-status.mjs +49 -0
  26. package/src/bin/mobile-link-proxy.mjs +221 -0
  27. package/src/bin/mobile-proof.mjs +164 -0
  28. package/src/bin/mobile-transport-smoke.mjs +200 -0
  29. package/src/bin/probe-codex-app-server-write.mjs +36 -0
  30. package/src/bin/probe-codex-app-server.mjs +30 -0
  31. package/src/lib/agent-room-context.mjs +54 -0
  32. package/src/lib/agent-room-runtime.mjs +355 -0
  33. package/src/lib/agent-room-service.mjs +335 -0
  34. package/src/lib/agent-room-state.mjs +406 -0
  35. package/src/lib/agent-room-store.mjs +71 -0
  36. package/src/lib/agent-room-text.mjs +48 -0
  37. package/src/lib/app-server-contract.mjs +66 -0
  38. package/src/lib/app-server-runtime.mjs +60 -0
  39. package/src/lib/attachment-service.mjs +119 -0
  40. package/src/lib/bridge-api-handler.mjs +719 -0
  41. package/src/lib/bridge-runtime-lifecycle.mjs +51 -0
  42. package/src/lib/bridge-status-builder.mjs +60 -0
  43. package/src/lib/codex-app-server-client.mjs +1511 -0
  44. package/src/lib/companion-state.mjs +453 -0
  45. package/src/lib/control-lease-service.mjs +180 -0
  46. package/src/lib/debug-harness-service.mjs +173 -0
  47. package/src/lib/desktop-integration.mjs +146 -0
  48. package/src/lib/desktop-rehydration-smoke.mjs +269 -0
  49. package/src/lib/dextunnel-cli.mjs +122 -0
  50. package/src/lib/discovery-docs.mjs +1321 -0
  51. package/src/lib/fake-codex-app-server-bridge.mjs +340 -0
  52. package/src/lib/install-preflight.mjs +373 -0
  53. package/src/lib/interaction-resolution-service.mjs +185 -0
  54. package/src/lib/interaction-state.mjs +360 -0
  55. package/src/lib/launch-release-bar.mjs +158 -0
  56. package/src/lib/live-control-state.mjs +107 -0
  57. package/src/lib/live-payload-builder.mjs +298 -0
  58. package/src/lib/live-selection-transition-state.mjs +49 -0
  59. package/src/lib/live-transcript-state.mjs +549 -0
  60. package/src/lib/mobile-network-profile.mjs +39 -0
  61. package/src/lib/mock-codex-adapter.mjs +62 -0
  62. package/src/lib/operator-diagnostics.mjs +82 -0
  63. package/src/lib/repo-changes-service.mjs +527 -0
  64. package/src/lib/runtime-config.mjs +106 -0
  65. package/src/lib/selection-state-service.mjs +214 -0
  66. package/src/lib/session-store.mjs +355 -0
  67. package/src/lib/shared-room-state.mjs +473 -0
  68. package/src/lib/shared-selection-state.mjs +40 -0
  69. package/src/lib/sse-hub.mjs +35 -0
  70. package/src/lib/static-surface-service.mjs +71 -0
  71. package/src/lib/surface-access.mjs +189 -0
  72. package/src/lib/surface-presence-service.mjs +118 -0
  73. package/src/lib/surface-request-guard.mjs +52 -0
  74. package/src/lib/thread-sync-state.mjs +536 -0
  75. package/src/lib/watcher-lifecycle.mjs +287 -0
  76. package/src/server.mjs +1446 -0
@@ -0,0 +1,1321 @@
1
+ import { SURFACE_CAPABILITIES } from "./surface-access.mjs";
2
+
3
+ export const DISCOVERY_MANIFEST_PATH = "/.well-known/dextunnel.json";
4
+ export const OPENAPI_DOC_PATH = "/openapi.json";
5
+ export const ARAZZO_DOC_PATH = "/arazzo.json";
6
+ export const LLMS_TXT_PATH = "/llms.txt";
7
+
8
+ function normalizeBaseUrl(baseUrl = "") {
9
+ return String(baseUrl || "").replace(/\/+$/, "");
10
+ }
11
+
12
+ function joinUrl(baseUrl, pathname) {
13
+ const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
14
+ if (!normalizedBaseUrl) {
15
+ return pathname;
16
+ }
17
+ return `${normalizedBaseUrl}${pathname}`;
18
+ }
19
+
20
+ function bootstrapUrl(baseUrl, surface = "agent") {
21
+ const query = new URLSearchParams({ surface }).toString();
22
+ return `${joinUrl(baseUrl, "/api/codex-app-server/bootstrap")}?${query}`;
23
+ }
24
+
25
+ function buildSurfaceDescriptions(baseUrl) {
26
+ return {
27
+ agent: {
28
+ bootstrapUrl: bootstrapUrl(baseUrl, "agent"),
29
+ capabilities: [...SURFACE_CAPABILITIES.agent],
30
+ description:
31
+ "Advanced automation surface with room read/write, control, refresh, and interaction handling.",
32
+ intendedClients: ["agents", "scripts", "local automation"],
33
+ supportLevel: "advanced"
34
+ },
35
+ remote: {
36
+ bootstrapUrl: bootstrapUrl(baseUrl, "remote"),
37
+ capabilities: [...SURFACE_CAPABILITIES.remote],
38
+ description:
39
+ "Human remote-operator surface used by the web companion UI. Includes companion and agent-room features.",
40
+ intendedClients: ["browser remote", "mobile operator UI"],
41
+ supportLevel: "primary"
42
+ },
43
+ host: {
44
+ bootstrapUrl: bootstrapUrl(baseUrl, "host"),
45
+ capabilities: [...SURFACE_CAPABILITIES.host],
46
+ description:
47
+ "Desktop-adjacent host surface. Restricted to loopback unless DEXTUNNEL_EXPOSE_HOST_SURFACE is enabled.",
48
+ intendedClients: ["local host UI"],
49
+ supportLevel: "primary"
50
+ }
51
+ };
52
+ }
53
+
54
+ export function buildDiscoveryLinks({ baseUrl = "" } = {}) {
55
+ return {
56
+ arazzo: joinUrl(baseUrl, ARAZZO_DOC_PATH),
57
+ llms: joinUrl(baseUrl, LLMS_TXT_PATH),
58
+ manifest: joinUrl(baseUrl, DISCOVERY_MANIFEST_PATH),
59
+ openapi: joinUrl(baseUrl, OPENAPI_DOC_PATH)
60
+ };
61
+ }
62
+
63
+ export function buildWellKnownManifest({ baseUrl = "" } = {}) {
64
+ const links = buildDiscoveryLinks({ baseUrl });
65
+ return {
66
+ schemaVersion: "1.0",
67
+ id: "dextunnel-bridge-api",
68
+ name: "Dextunnel Bridge API",
69
+ description:
70
+ "Local-first bridge API for advanced automation and local integrations around live Codex threads over HTTP JSON and SSE.",
71
+ preferredBootstrapSurface: "agent",
72
+ supportLevel: "advanced",
73
+ apiVersion: "2026-03-23",
74
+ links,
75
+ bootstrap: {
76
+ defaultUrl: bootstrapUrl(baseUrl, "agent"),
77
+ pathTemplate: "/api/codex-app-server/bootstrap?surface={surface}",
78
+ supportedSurfaces: buildSurfaceDescriptions(baseUrl)
79
+ },
80
+ auth: {
81
+ preferred: {
82
+ scheme: "bearer",
83
+ usage: "Authorization: Bearer <accessToken>"
84
+ },
85
+ accepted: [
86
+ "Authorization: Bearer <accessToken>",
87
+ "x-dextunnel-surface-token: <accessToken>",
88
+ "surfaceToken=<accessToken> query parameter for compatibility, mainly for SSE or browser EventSource clients"
89
+ ]
90
+ },
91
+ transports: {
92
+ http: {
93
+ apiBase: joinUrl(baseUrl, "/api"),
94
+ format: "json"
95
+ },
96
+ sse: {
97
+ eventTypes: ["snapshot", "live"],
98
+ url: joinUrl(baseUrl, "/api/stream")
99
+ }
100
+ },
101
+ recommendedWorkflow: [
102
+ "Fetch this manifest.",
103
+ "Call the agent bootstrap URL to obtain a signed access token.",
104
+ "Send the token as Authorization: Bearer <accessToken> on subsequent API calls.",
105
+ "Read /api/codex-app-server/live-state for the current selected thread.",
106
+ "Claim control if you plan to send a turn into an active remote-controlled thread.",
107
+ "Subscribe to /api/stream for live updates."
108
+ ]
109
+ };
110
+ }
111
+
112
+ function securitySchemesDescription() {
113
+ return "Bootstrap a token from GET /api/codex-app-server/bootstrap?surface=agent, then send it as Authorization: Bearer <accessToken>.";
114
+ }
115
+
116
+ function createOpenApiSchemas() {
117
+ return {
118
+ BootstrapToken: {
119
+ type: "object",
120
+ additionalProperties: false,
121
+ required: ["accessToken", "capabilities", "clientId", "expiresAt", "issuedAt", "surface"],
122
+ properties: {
123
+ accessToken: { type: "string" },
124
+ capabilities: {
125
+ type: "array",
126
+ items: { type: "string" }
127
+ },
128
+ clientId: { type: "string" },
129
+ expiresAt: { type: "string", format: "date-time" },
130
+ issuedAt: { type: "string", format: "date-time" },
131
+ surface: {
132
+ type: "string",
133
+ enum: ["agent", "host", "remote"]
134
+ }
135
+ }
136
+ },
137
+ CheckEntry: {
138
+ type: "object",
139
+ additionalProperties: true,
140
+ properties: {
141
+ detail: { type: "string" },
142
+ id: { type: "string" },
143
+ label: { type: "string" },
144
+ severity: {
145
+ type: "string",
146
+ enum: ["error", "ready", "warning"]
147
+ }
148
+ }
149
+ },
150
+ DiscoveryLinks: {
151
+ type: "object",
152
+ additionalProperties: false,
153
+ properties: {
154
+ arazzo: { type: "string" },
155
+ llms: { type: "string" },
156
+ manifest: { type: "string" },
157
+ openapi: { type: "string" }
158
+ }
159
+ },
160
+ Preflight: {
161
+ type: "object",
162
+ additionalProperties: true,
163
+ properties: {
164
+ appServer: {
165
+ type: "object",
166
+ additionalProperties: true
167
+ },
168
+ checks: {
169
+ type: "array",
170
+ items: { $ref: "#/components/schemas/CheckEntry" }
171
+ },
172
+ codexBinary: {
173
+ type: "object",
174
+ additionalProperties: true
175
+ },
176
+ links: { $ref: "#/components/schemas/DiscoveryLinks" },
177
+ nextSteps: {
178
+ type: "array",
179
+ items: { type: "string" }
180
+ },
181
+ runtime: {
182
+ type: "object",
183
+ additionalProperties: true
184
+ },
185
+ status: {
186
+ type: "string",
187
+ enum: ["error", "ready", "warning"]
188
+ },
189
+ summary: { type: "string" },
190
+ workspace: {
191
+ type: "object",
192
+ additionalProperties: true
193
+ }
194
+ }
195
+ },
196
+ LiveState: {
197
+ type: "object",
198
+ additionalProperties: true,
199
+ properties: {
200
+ pendingInteraction: {
201
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
202
+ },
203
+ selectedAgentRoom: {
204
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
205
+ },
206
+ selectedAttachments: {
207
+ type: "array",
208
+ items: { type: "object", additionalProperties: true }
209
+ },
210
+ selectedChannel: {
211
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
212
+ },
213
+ selectedCompanion: {
214
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
215
+ },
216
+ selectedProjectCwd: {
217
+ anyOf: [{ type: "string" }, { type: "null" }]
218
+ },
219
+ selectedThreadId: {
220
+ anyOf: [{ type: "string" }, { type: "null" }]
221
+ },
222
+ selectedThreadSnapshot: {
223
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
224
+ },
225
+ status: {
226
+ anyOf: [{ type: "string" }, { type: "null" }]
227
+ },
228
+ threads: {
229
+ type: "array",
230
+ items: { type: "object", additionalProperties: true }
231
+ },
232
+ turnDiff: {
233
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
234
+ }
235
+ }
236
+ },
237
+ BridgeStatus: {
238
+ type: "object",
239
+ additionalProperties: true,
240
+ properties: {
241
+ diagnostics: {
242
+ type: "array",
243
+ items: {
244
+ type: "string"
245
+ }
246
+ },
247
+ lastControlEvent: {
248
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
249
+ },
250
+ lastInteraction: {
251
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
252
+ },
253
+ lastSelectionEvent: {
254
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
255
+ },
256
+ lastSurfaceEvent: {
257
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
258
+ },
259
+ lastWrite: {
260
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
261
+ },
262
+ watcherConnected: {
263
+ anyOf: [{ type: "boolean" }, { type: "null" }]
264
+ }
265
+ }
266
+ },
267
+ TranscriptHistoryPage: {
268
+ type: "object",
269
+ additionalProperties: true,
270
+ properties: {
271
+ hasMore: { type: "boolean" },
272
+ items: {
273
+ type: "array",
274
+ items: { type: "object", additionalProperties: true }
275
+ },
276
+ nextBeforeIndex: {
277
+ anyOf: [{ type: "integer" }, { type: "null" }]
278
+ },
279
+ totalCount: { type: "integer" }
280
+ }
281
+ },
282
+ ThreadsResponse: {
283
+ type: "object",
284
+ additionalProperties: true,
285
+ properties: {
286
+ cwd: {
287
+ anyOf: [{ type: "string" }, { type: "null" }]
288
+ },
289
+ data: {
290
+ type: "array",
291
+ items: { type: "object", additionalProperties: true }
292
+ }
293
+ }
294
+ },
295
+ ThreadSnapshotResponse: {
296
+ type: "object",
297
+ additionalProperties: true,
298
+ properties: {
299
+ found: { type: "boolean" },
300
+ snapshot: {
301
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
302
+ },
303
+ threadId: {
304
+ anyOf: [{ type: "string" }, { type: "null" }]
305
+ }
306
+ }
307
+ },
308
+ LatestThreadResponse: {
309
+ type: "object",
310
+ additionalProperties: true,
311
+ properties: {
312
+ cwd: { type: "string" },
313
+ found: { type: "boolean" },
314
+ snapshot: {
315
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
316
+ }
317
+ }
318
+ },
319
+ StateEnvelope: {
320
+ type: "object",
321
+ additionalProperties: true,
322
+ properties: {
323
+ ok: { type: "boolean" },
324
+ state: { $ref: "#/components/schemas/LiveState" }
325
+ }
326
+ },
327
+ SelectionRequest: {
328
+ type: "object",
329
+ additionalProperties: false,
330
+ properties: {
331
+ cwd: {
332
+ anyOf: [{ type: "string" }, { type: "null" }]
333
+ },
334
+ threadId: {
335
+ anyOf: [{ type: "string" }, { type: "null" }]
336
+ }
337
+ }
338
+ },
339
+ CreateThreadRequest: {
340
+ type: "object",
341
+ additionalProperties: false,
342
+ properties: {
343
+ cwd: {
344
+ anyOf: [{ type: "string" }, { type: "null" }]
345
+ }
346
+ }
347
+ },
348
+ ControlRequest: {
349
+ type: "object",
350
+ additionalProperties: false,
351
+ properties: {
352
+ action: {
353
+ type: "string",
354
+ enum: ["claim", "release"]
355
+ },
356
+ reason: {
357
+ anyOf: [{ type: "string" }, { type: "null" }]
358
+ },
359
+ threadId: {
360
+ anyOf: [{ type: "string" }, { type: "null" }]
361
+ }
362
+ }
363
+ },
364
+ InteractionRequest: {
365
+ type: "object",
366
+ additionalProperties: true,
367
+ description:
368
+ "Pass through the pending interaction response payload for the selected thread. Fields depend on the interaction kind."
369
+ },
370
+ OpenInCodexRequest: {
371
+ type: "object",
372
+ additionalProperties: false,
373
+ properties: {
374
+ threadId: {
375
+ anyOf: [{ type: "string" }, { type: "null" }]
376
+ }
377
+ }
378
+ },
379
+ SendTurnRequest: {
380
+ type: "object",
381
+ additionalProperties: false,
382
+ properties: {
383
+ attachments: {
384
+ type: "array",
385
+ items: { type: "object", additionalProperties: true }
386
+ },
387
+ createThreadIfMissing: { type: "boolean" },
388
+ cwd: {
389
+ anyOf: [{ type: "string" }, { type: "null" }]
390
+ },
391
+ text: { type: "string" },
392
+ threadId: {
393
+ anyOf: [{ type: "string" }, { type: "null" }]
394
+ },
395
+ timeoutMs: { type: "integer" }
396
+ }
397
+ },
398
+ SendTurnResponse: {
399
+ type: "object",
400
+ additionalProperties: true,
401
+ properties: {
402
+ mode: {
403
+ anyOf: [{ type: "string" }, { type: "null" }]
404
+ },
405
+ ok: { type: "boolean" },
406
+ snapshot: {
407
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
408
+ },
409
+ thread: {
410
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
411
+ },
412
+ turn: {
413
+ anyOf: [{ type: "object", additionalProperties: true }, { type: "null" }]
414
+ }
415
+ }
416
+ },
417
+ CompanionRequest: {
418
+ type: "object",
419
+ additionalProperties: false,
420
+ properties: {
421
+ action: { type: "string" },
422
+ advisorId: {
423
+ anyOf: [{ type: "string" }, { type: "null" }]
424
+ },
425
+ threadId: {
426
+ anyOf: [{ type: "string" }, { type: "null" }]
427
+ },
428
+ wakeKey: {
429
+ anyOf: [{ type: "string" }, { type: "null" }]
430
+ }
431
+ }
432
+ },
433
+ AgentRoomRequest: {
434
+ type: "object",
435
+ additionalProperties: false,
436
+ properties: {
437
+ action: { type: "string" },
438
+ memberIds: {
439
+ anyOf: [
440
+ {
441
+ type: "array",
442
+ items: { type: "string" }
443
+ },
444
+ { type: "null" }
445
+ ]
446
+ },
447
+ text: { type: "string" },
448
+ threadId: {
449
+ anyOf: [{ type: "string" }, { type: "null" }]
450
+ }
451
+ }
452
+ },
453
+ MessageEnvelope: {
454
+ type: "object",
455
+ additionalProperties: true,
456
+ properties: {
457
+ message: {
458
+ anyOf: [{ type: "string" }, { type: "null" }]
459
+ },
460
+ ok: { type: "boolean" },
461
+ state: { $ref: "#/components/schemas/LiveState" }
462
+ }
463
+ },
464
+ ErrorResponse: {
465
+ type: "object",
466
+ additionalProperties: true,
467
+ required: ["error"],
468
+ properties: {
469
+ error: { type: "string" },
470
+ state: {
471
+ anyOf: [{ $ref: "#/components/schemas/LiveState" }, { type: "null" }]
472
+ },
473
+ status: {
474
+ anyOf: [{ type: "string" }, { type: "null" }]
475
+ }
476
+ }
477
+ }
478
+ };
479
+ }
480
+
481
+ function securedPath(operation) {
482
+ return {
483
+ ...operation,
484
+ security: [{ BearerAuth: [] }, { SurfaceTokenHeader: [] }]
485
+ };
486
+ }
487
+
488
+ function securedStreamPath(operation) {
489
+ return {
490
+ ...operation,
491
+ security: [{ BearerAuth: [] }, { SurfaceTokenHeader: [] }, { SurfaceTokenQuery: [] }]
492
+ };
493
+ }
494
+
495
+ export function buildOpenApiDocument({ baseUrl = "" } = {}) {
496
+ return {
497
+ openapi: "3.1.1",
498
+ jsonSchemaDialect: "https://json-schema.org/draft/2020-12/schema",
499
+ info: {
500
+ title: "Dextunnel Bridge API",
501
+ version: "2026-03-23",
502
+ summary: "HTTP JSON and SSE bridge for reading and steering live Codex threads.",
503
+ description:
504
+ "Bootstrap a signed surface token, then use it to read live state, claim control, send turns, resolve interactions, and subscribe to server-sent updates."
505
+ },
506
+ servers: [
507
+ {
508
+ url: normalizeBaseUrl(baseUrl) || "/",
509
+ description: "Current Dextunnel bridge instance"
510
+ }
511
+ ],
512
+ tags: [
513
+ { name: "discovery" },
514
+ { name: "read" },
515
+ { name: "control" },
516
+ { name: "write" },
517
+ { name: "stream" }
518
+ ],
519
+ paths: {
520
+ "/api/preflight": {
521
+ get: {
522
+ tags: ["discovery"],
523
+ operationId: "getPreflight",
524
+ summary: "Inspect local Dextunnel readiness without authentication.",
525
+ parameters: [
526
+ {
527
+ in: "query",
528
+ name: "warmup",
529
+ schema: { type: "string", enum: ["0", "1"] },
530
+ description:
531
+ "Use warmup=0 to skip thread-list warmup and return a lighter local setup check."
532
+ }
533
+ ],
534
+ responses: {
535
+ 200: {
536
+ description: "Install preflight payload.",
537
+ content: {
538
+ "application/json": {
539
+ schema: { $ref: "#/components/schemas/Preflight" }
540
+ }
541
+ }
542
+ }
543
+ }
544
+ }
545
+ },
546
+ "/api/codex-app-server/bootstrap": {
547
+ get: {
548
+ tags: ["discovery"],
549
+ operationId: "bootstrapSurface",
550
+ summary: "Issue a signed surface token.",
551
+ description:
552
+ "Use surface=agent for automation clients. The returned accessToken can be sent as Authorization: Bearer <accessToken>.",
553
+ parameters: [
554
+ {
555
+ in: "query",
556
+ name: "surface",
557
+ schema: {
558
+ type: "string",
559
+ enum: ["agent", "host", "remote"],
560
+ default: "remote"
561
+ }
562
+ }
563
+ ],
564
+ responses: {
565
+ 200: {
566
+ description: "Signed access token for the requested surface.",
567
+ content: {
568
+ "application/json": {
569
+ schema: { $ref: "#/components/schemas/BootstrapToken" }
570
+ }
571
+ }
572
+ },
573
+ 400: {
574
+ description: "Unsupported surface.",
575
+ content: {
576
+ "application/json": {
577
+ schema: { $ref: "#/components/schemas/ErrorResponse" }
578
+ }
579
+ }
580
+ },
581
+ 403: {
582
+ description: "Host bootstrap requested from a non-loopback address without host exposure enabled.",
583
+ content: {
584
+ "application/json": {
585
+ schema: { $ref: "#/components/schemas/ErrorResponse" }
586
+ }
587
+ }
588
+ }
589
+ }
590
+ }
591
+ },
592
+ "/api/codex-app-server/live-state": {
593
+ get: securedPath({
594
+ tags: ["read"],
595
+ operationId: "getLiveState",
596
+ summary: "Read the current selected live state.",
597
+ responses: {
598
+ 200: {
599
+ description: "Live bridge payload.",
600
+ content: {
601
+ "application/json": {
602
+ schema: { $ref: "#/components/schemas/LiveState" }
603
+ }
604
+ }
605
+ },
606
+ 403: {
607
+ description: "Missing or expired surface token.",
608
+ content: {
609
+ "application/json": {
610
+ schema: { $ref: "#/components/schemas/ErrorResponse" }
611
+ }
612
+ }
613
+ }
614
+ }
615
+ })
616
+ },
617
+ "/api/codex-app-server/transcript-history": {
618
+ get: securedPath({
619
+ tags: ["read"],
620
+ operationId: "getTranscriptHistory",
621
+ summary: "Load older transcript pages for a thread.",
622
+ parameters: [
623
+ {
624
+ in: "query",
625
+ name: "threadId",
626
+ schema: { type: "string" },
627
+ description: "Defaults to the selected thread when omitted."
628
+ },
629
+ {
630
+ in: "query",
631
+ name: "beforeIndex",
632
+ schema: { type: "string" }
633
+ },
634
+ {
635
+ in: "query",
636
+ name: "limit",
637
+ schema: { type: "string" }
638
+ },
639
+ {
640
+ in: "query",
641
+ name: "visibleCount",
642
+ schema: { type: "string" }
643
+ }
644
+ ],
645
+ responses: {
646
+ 200: {
647
+ description: "Transcript history page.",
648
+ content: {
649
+ "application/json": {
650
+ schema: { $ref: "#/components/schemas/TranscriptHistoryPage" }
651
+ }
652
+ }
653
+ },
654
+ 400: {
655
+ description: "Bad history request.",
656
+ content: {
657
+ "application/json": {
658
+ schema: { $ref: "#/components/schemas/ErrorResponse" }
659
+ }
660
+ }
661
+ }
662
+ }
663
+ })
664
+ },
665
+ "/api/codex-app-server/status": {
666
+ get: securedPath({
667
+ tags: ["read"],
668
+ operationId: "getBridgeStatus",
669
+ summary: "Read operator-facing bridge status and diagnostics.",
670
+ responses: {
671
+ 200: {
672
+ description: "Bridge status.",
673
+ content: {
674
+ "application/json": {
675
+ schema: { $ref: "#/components/schemas/BridgeStatus" }
676
+ }
677
+ }
678
+ }
679
+ }
680
+ })
681
+ },
682
+ "/api/codex-app-server/threads": {
683
+ get: securedPath({
684
+ tags: ["read"],
685
+ operationId: "listThreads",
686
+ summary: "Refresh and return the visible thread list.",
687
+ responses: {
688
+ 200: {
689
+ description: "Thread list payload.",
690
+ content: {
691
+ "application/json": {
692
+ schema: { $ref: "#/components/schemas/ThreadsResponse" }
693
+ }
694
+ }
695
+ }
696
+ }
697
+ })
698
+ },
699
+ "/api/codex-app-server/thread": {
700
+ get: securedPath({
701
+ tags: ["read"],
702
+ operationId: "getThreadSnapshot",
703
+ summary: "Read a specific thread snapshot.",
704
+ parameters: [
705
+ {
706
+ in: "query",
707
+ name: "threadId",
708
+ required: true,
709
+ schema: { type: "string" }
710
+ },
711
+ {
712
+ in: "query",
713
+ name: "limit",
714
+ schema: { type: "integer", minimum: 1 }
715
+ }
716
+ ],
717
+ responses: {
718
+ 200: {
719
+ description: "Thread snapshot response.",
720
+ content: {
721
+ "application/json": {
722
+ schema: { $ref: "#/components/schemas/ThreadSnapshotResponse" }
723
+ }
724
+ }
725
+ }
726
+ }
727
+ }),
728
+ post: securedPath({
729
+ tags: ["control"],
730
+ operationId: "createThreadSelection",
731
+ summary: "Create a new Codex thread selection for the provided cwd.",
732
+ requestBody: {
733
+ required: true,
734
+ content: {
735
+ "application/json": {
736
+ schema: { $ref: "#/components/schemas/CreateThreadRequest" }
737
+ }
738
+ }
739
+ },
740
+ responses: {
741
+ 200: {
742
+ description: "Created thread selection payload.",
743
+ content: {
744
+ "application/json": {
745
+ schema: { $ref: "#/components/schemas/StateEnvelope" }
746
+ }
747
+ }
748
+ },
749
+ 409: {
750
+ description: "Selection conflict.",
751
+ content: {
752
+ "application/json": {
753
+ schema: { $ref: "#/components/schemas/ErrorResponse" }
754
+ }
755
+ }
756
+ }
757
+ }
758
+ })
759
+ },
760
+ "/api/codex-app-server/latest-thread": {
761
+ get: securedPath({
762
+ tags: ["read"],
763
+ operationId: "getLatestThreadForWorkspace",
764
+ summary: "Return the latest thread for a workspace cwd.",
765
+ parameters: [
766
+ {
767
+ in: "query",
768
+ name: "cwd",
769
+ schema: { type: "string" }
770
+ },
771
+ {
772
+ in: "query",
773
+ name: "limit",
774
+ schema: { type: "integer", minimum: 1 }
775
+ }
776
+ ],
777
+ responses: {
778
+ 200: {
779
+ description: "Latest thread snapshot response.",
780
+ content: {
781
+ "application/json": {
782
+ schema: { $ref: "#/components/schemas/LatestThreadResponse" }
783
+ }
784
+ }
785
+ }
786
+ }
787
+ })
788
+ },
789
+ "/api/codex-app-server/refresh": {
790
+ post: securedPath({
791
+ tags: ["control"],
792
+ operationId: "refreshLiveState",
793
+ summary: "Refresh live state and optionally the thread list.",
794
+ parameters: [
795
+ {
796
+ in: "query",
797
+ name: "threads",
798
+ schema: { type: "string", enum: ["0", "1"] },
799
+ description:
800
+ "Use threads=0 when you only need fresh live state and can reuse the current room list."
801
+ }
802
+ ],
803
+ responses: {
804
+ 200: {
805
+ description: "Refreshed live state.",
806
+ content: {
807
+ "application/json": {
808
+ schema: { $ref: "#/components/schemas/StateEnvelope" }
809
+ }
810
+ }
811
+ }
812
+ }
813
+ })
814
+ },
815
+ "/api/codex-app-server/reconnect": {
816
+ post: securedPath({
817
+ tags: ["control"],
818
+ operationId: "reconnectWatcher",
819
+ summary: "Restart the app-server watcher and refresh live state.",
820
+ parameters: [
821
+ {
822
+ in: "query",
823
+ name: "threads",
824
+ schema: { type: "string", enum: ["0", "1"] }
825
+ }
826
+ ],
827
+ responses: {
828
+ 200: {
829
+ description: "Refreshed live state after reconnect.",
830
+ content: {
831
+ "application/json": {
832
+ schema: { $ref: "#/components/schemas/StateEnvelope" }
833
+ }
834
+ }
835
+ }
836
+ }
837
+ })
838
+ },
839
+ "/api/codex-app-server/selection": {
840
+ post: securedPath({
841
+ tags: ["control"],
842
+ operationId: "setSelection",
843
+ summary: "Switch the selected room and thread.",
844
+ requestBody: {
845
+ required: true,
846
+ content: {
847
+ "application/json": {
848
+ schema: { $ref: "#/components/schemas/SelectionRequest" }
849
+ }
850
+ }
851
+ },
852
+ responses: {
853
+ 200: {
854
+ description: "Selection update payload.",
855
+ content: {
856
+ "application/json": {
857
+ schema: { $ref: "#/components/schemas/StateEnvelope" }
858
+ }
859
+ }
860
+ },
861
+ 409: {
862
+ description: "Selection conflict.",
863
+ content: {
864
+ "application/json": {
865
+ schema: { $ref: "#/components/schemas/ErrorResponse" }
866
+ }
867
+ }
868
+ }
869
+ }
870
+ })
871
+ },
872
+ "/api/codex-app-server/control": {
873
+ post: securedPath({
874
+ tags: ["control"],
875
+ operationId: "controlRemoteLease",
876
+ summary: "Claim or release remote control for a thread.",
877
+ requestBody: {
878
+ required: true,
879
+ content: {
880
+ "application/json": {
881
+ schema: { $ref: "#/components/schemas/ControlRequest" }
882
+ }
883
+ }
884
+ },
885
+ responses: {
886
+ 200: {
887
+ description: "Updated control state.",
888
+ content: {
889
+ "application/json": {
890
+ schema: { $ref: "#/components/schemas/StateEnvelope" }
891
+ }
892
+ }
893
+ },
894
+ 400: {
895
+ description: "Bad control request.",
896
+ content: {
897
+ "application/json": {
898
+ schema: { $ref: "#/components/schemas/ErrorResponse" }
899
+ }
900
+ }
901
+ }
902
+ }
903
+ })
904
+ },
905
+ "/api/codex-app-server/interaction": {
906
+ post: securedPath({
907
+ tags: ["control"],
908
+ operationId: "resolveInteraction",
909
+ summary: "Resolve the current pending interaction.",
910
+ requestBody: {
911
+ required: true,
912
+ content: {
913
+ "application/json": {
914
+ schema: { $ref: "#/components/schemas/InteractionRequest" }
915
+ }
916
+ }
917
+ },
918
+ responses: {
919
+ 200: {
920
+ description: "Updated state after interaction resolution.",
921
+ content: {
922
+ "application/json": {
923
+ schema: { $ref: "#/components/schemas/StateEnvelope" }
924
+ }
925
+ }
926
+ }
927
+ }
928
+ })
929
+ },
930
+ "/api/codex-app-server/interrupt": {
931
+ post: securedPath({
932
+ tags: ["control"],
933
+ operationId: "interruptTurn",
934
+ summary: "Interrupt the selected active turn.",
935
+ responses: {
936
+ 200: {
937
+ description: "Interrupt response payload.",
938
+ content: {
939
+ "application/json": {
940
+ schema: {
941
+ type: "object",
942
+ additionalProperties: true
943
+ }
944
+ }
945
+ }
946
+ }
947
+ }
948
+ })
949
+ },
950
+ "/api/codex-app-server/open-in-codex": {
951
+ post: securedPath({
952
+ tags: ["control"],
953
+ operationId: "openInCodex",
954
+ summary: "Reveal the selected thread in the desktop Codex app.",
955
+ requestBody: {
956
+ required: false,
957
+ content: {
958
+ "application/json": {
959
+ schema: { $ref: "#/components/schemas/OpenInCodexRequest" }
960
+ }
961
+ }
962
+ },
963
+ responses: {
964
+ 200: {
965
+ description: "Desktop reveal response.",
966
+ content: {
967
+ "application/json": {
968
+ schema: {
969
+ type: "object",
970
+ additionalProperties: true
971
+ }
972
+ }
973
+ }
974
+ }
975
+ }
976
+ })
977
+ },
978
+ "/api/codex-app-server/turn": {
979
+ post: securedPath({
980
+ tags: ["write"],
981
+ operationId: "sendTurn",
982
+ summary: "Send a user turn into the selected or specified thread.",
983
+ requestBody: {
984
+ required: true,
985
+ content: {
986
+ "application/json": {
987
+ schema: { $ref: "#/components/schemas/SendTurnRequest" }
988
+ }
989
+ }
990
+ },
991
+ responses: {
992
+ 200: {
993
+ description: "Accepted turn with the immediate thread snapshot.",
994
+ content: {
995
+ "application/json": {
996
+ schema: { $ref: "#/components/schemas/SendTurnResponse" }
997
+ }
998
+ }
999
+ },
1000
+ 409: {
1001
+ description: "Turn could not be sent because the thread is busy, control is held elsewhere, or an interaction is pending.",
1002
+ content: {
1003
+ "application/json": {
1004
+ schema: { $ref: "#/components/schemas/ErrorResponse" }
1005
+ }
1006
+ }
1007
+ }
1008
+ }
1009
+ })
1010
+ },
1011
+ "/api/codex-app-server/companion": {
1012
+ post: securedPath({
1013
+ tags: ["write"],
1014
+ operationId: "applyCompanionAction",
1015
+ summary: "Summon or resolve a companion wakeup.",
1016
+ requestBody: {
1017
+ required: true,
1018
+ content: {
1019
+ "application/json": {
1020
+ schema: { $ref: "#/components/schemas/CompanionRequest" }
1021
+ }
1022
+ }
1023
+ },
1024
+ responses: {
1025
+ 200: {
1026
+ description: "Companion action result.",
1027
+ content: {
1028
+ "application/json": {
1029
+ schema: { $ref: "#/components/schemas/MessageEnvelope" }
1030
+ }
1031
+ }
1032
+ }
1033
+ }
1034
+ })
1035
+ },
1036
+ "/api/codex-app-server/agent-room": {
1037
+ post: securedPath({
1038
+ tags: ["write"],
1039
+ operationId: "updateAgentRoom",
1040
+ summary: "Operate on the advisory agent room for the selected thread.",
1041
+ requestBody: {
1042
+ required: true,
1043
+ content: {
1044
+ "application/json": {
1045
+ schema: { $ref: "#/components/schemas/AgentRoomRequest" }
1046
+ }
1047
+ }
1048
+ },
1049
+ responses: {
1050
+ 200: {
1051
+ description: "Agent-room update result.",
1052
+ content: {
1053
+ "application/json": {
1054
+ schema: { $ref: "#/components/schemas/MessageEnvelope" }
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+ })
1060
+ },
1061
+ "/api/stream": {
1062
+ get: securedStreamPath({
1063
+ tags: ["stream"],
1064
+ operationId: "streamEvents",
1065
+ summary: "Open a server-sent event stream with snapshot and live updates.",
1066
+ responses: {
1067
+ 200: {
1068
+ description: "SSE stream containing snapshot and live events.",
1069
+ content: {
1070
+ "text/event-stream": {
1071
+ schema: {
1072
+ type: "string"
1073
+ },
1074
+ example:
1075
+ "event: snapshot\ndata: {\"selectedThreadId\":\"thr_example\"}\n\n" +
1076
+ "event: live\ndata: {\"selectedThreadId\":\"thr_example\",\"status\":\"idle\"}\n\n"
1077
+ }
1078
+ }
1079
+ }
1080
+ }
1081
+ })
1082
+ }
1083
+ },
1084
+ components: {
1085
+ securitySchemes: {
1086
+ BearerAuth: {
1087
+ type: "http",
1088
+ scheme: "bearer",
1089
+ bearerFormat: "DextunnelSurfaceToken",
1090
+ description: securitySchemesDescription()
1091
+ },
1092
+ SurfaceTokenHeader: {
1093
+ type: "apiKey",
1094
+ in: "header",
1095
+ name: "x-dextunnel-surface-token",
1096
+ description:
1097
+ "Compatibility header for older clients. Prefer BearerAuth for new agent integrations."
1098
+ },
1099
+ SurfaceTokenQuery: {
1100
+ type: "apiKey",
1101
+ in: "query",
1102
+ name: "surfaceToken",
1103
+ description:
1104
+ "Compatibility query parameter, mainly for SSE or browser EventSource clients that cannot set headers."
1105
+ }
1106
+ },
1107
+ schemas: createOpenApiSchemas()
1108
+ }
1109
+ };
1110
+ }
1111
+
1112
+ export function buildArazzoDocument({ baseUrl = "" } = {}) {
1113
+ return {
1114
+ arazzo: "1.0.1",
1115
+ info: {
1116
+ title: "Dextunnel Bridge Workflows",
1117
+ summary: "Common workflows for bootstrapping, reading, and steering Dextunnel threads.",
1118
+ version: "2026-03-23"
1119
+ },
1120
+ sourceDescriptions: [
1121
+ {
1122
+ name: "dextunnelOpenapi",
1123
+ type: "openapi",
1124
+ url: joinUrl(baseUrl, OPENAPI_DOC_PATH)
1125
+ }
1126
+ ],
1127
+ workflows: [
1128
+ {
1129
+ workflowId: "bootstrapAndReadLiveState",
1130
+ summary: "Bootstrap an automation token and read the current live state.",
1131
+ inputs: {
1132
+ type: "object",
1133
+ properties: {
1134
+ surface: {
1135
+ type: "string",
1136
+ default: "agent"
1137
+ }
1138
+ }
1139
+ },
1140
+ steps: [
1141
+ {
1142
+ stepId: "bootstrap",
1143
+ description: "Issue a signed token for the requested surface.",
1144
+ operationId: "bootstrapSurface",
1145
+ parameters: [
1146
+ {
1147
+ name: "surface",
1148
+ in: "query",
1149
+ value: "$inputs.surface"
1150
+ }
1151
+ ],
1152
+ successCriteria: [{ condition: "$statusCode == 200" }],
1153
+ outputs: {
1154
+ accessToken: "$response.body.accessToken"
1155
+ }
1156
+ },
1157
+ {
1158
+ stepId: "readLiveState",
1159
+ description: "Read the current selected live thread and room state.",
1160
+ operationId: "getLiveState",
1161
+ parameters: [
1162
+ {
1163
+ name: "x-dextunnel-surface-token",
1164
+ in: "header",
1165
+ value: "$steps.bootstrap.outputs.accessToken"
1166
+ }
1167
+ ],
1168
+ successCriteria: [{ condition: "$statusCode == 200" }],
1169
+ outputs: {
1170
+ selectedThreadId: "$response.body.selectedThreadId"
1171
+ }
1172
+ }
1173
+ ],
1174
+ outputs: {
1175
+ accessToken: "$steps.bootstrap.outputs.accessToken",
1176
+ selectedThreadId: "$steps.readLiveState.outputs.selectedThreadId"
1177
+ }
1178
+ },
1179
+ {
1180
+ workflowId: "claimControlAndSendTurn",
1181
+ summary: "Claim control for the selected thread and send a user turn.",
1182
+ inputs: {
1183
+ type: "object",
1184
+ properties: {
1185
+ accessToken: { type: "string" },
1186
+ text: { type: "string" },
1187
+ threadId: { type: "string" }
1188
+ },
1189
+ required: ["accessToken", "text"]
1190
+ },
1191
+ steps: [
1192
+ {
1193
+ stepId: "claimControl",
1194
+ description: "Claim remote control before sending into a live thread.",
1195
+ operationId: "controlRemoteLease",
1196
+ parameters: [
1197
+ {
1198
+ name: "x-dextunnel-surface-token",
1199
+ in: "header",
1200
+ value: "$inputs.accessToken"
1201
+ }
1202
+ ],
1203
+ requestBody: {
1204
+ contentType: "application/json",
1205
+ payload: {
1206
+ action: "claim",
1207
+ reason: "agent automation",
1208
+ threadId: "$inputs.threadId"
1209
+ }
1210
+ },
1211
+ successCriteria: [{ condition: "$statusCode == 200" }]
1212
+ },
1213
+ {
1214
+ stepId: "sendTurn",
1215
+ description: "Send the requested user text into the selected thread.",
1216
+ operationId: "sendTurn",
1217
+ parameters: [
1218
+ {
1219
+ name: "x-dextunnel-surface-token",
1220
+ in: "header",
1221
+ value: "$inputs.accessToken"
1222
+ }
1223
+ ],
1224
+ requestBody: {
1225
+ contentType: "application/json",
1226
+ payload: {
1227
+ text: "$inputs.text",
1228
+ threadId: "$inputs.threadId"
1229
+ }
1230
+ },
1231
+ successCriteria: [{ condition: "$statusCode == 200" }],
1232
+ outputs: {
1233
+ turnId: "$response.body.turn.id"
1234
+ }
1235
+ }
1236
+ ],
1237
+ outputs: {
1238
+ turnId: "$steps.sendTurn.outputs.turnId"
1239
+ }
1240
+ },
1241
+ {
1242
+ workflowId: "watchLiveEvents",
1243
+ summary: "Subscribe to the server-sent event stream after bootstrap.",
1244
+ inputs: {
1245
+ type: "object",
1246
+ properties: {
1247
+ accessToken: { type: "string" }
1248
+ },
1249
+ required: ["accessToken"]
1250
+ },
1251
+ steps: [
1252
+ {
1253
+ stepId: "openStream",
1254
+ description:
1255
+ "Open the SSE stream using the compatibility query parameter when a header cannot be set by the client.",
1256
+ operationId: "streamEvents",
1257
+ parameters: [
1258
+ {
1259
+ name: "surfaceToken",
1260
+ in: "query",
1261
+ value: "$inputs.accessToken"
1262
+ }
1263
+ ],
1264
+ successCriteria: [{ condition: "$statusCode == 200" }]
1265
+ }
1266
+ ]
1267
+ }
1268
+ ]
1269
+ };
1270
+ }
1271
+
1272
+ export function buildLlmsText({ baseUrl = "" } = {}) {
1273
+ const links = buildDiscoveryLinks({ baseUrl });
1274
+ return [
1275
+ "# Dextunnel",
1276
+ "",
1277
+ "> Local-first bridge API for reading and steering live Codex threads over HTTP JSON and SSE.",
1278
+ "",
1279
+ "## Start Here",
1280
+ "",
1281
+ `- Discovery manifest: ${links.manifest}`,
1282
+ `- OpenAPI description: ${links.openapi}`,
1283
+ `- Arazzo workflows: ${links.arazzo}`,
1284
+ `- Agent bootstrap URL: ${bootstrapUrl(baseUrl, "agent")}`,
1285
+ "",
1286
+ "## Auth",
1287
+ "",
1288
+ "- Preferred: Authorization: Bearer <accessToken>",
1289
+ "- Compatibility: x-dextunnel-surface-token: <accessToken>",
1290
+ "- Compatibility for SSE/browser clients: surfaceToken=<accessToken> query parameter",
1291
+ "- Bootstrap by calling GET /api/codex-app-server/bootstrap?surface=agent",
1292
+ "",
1293
+ "## Core Workflow",
1294
+ "",
1295
+ "1. Fetch the discovery manifest.",
1296
+ "2. Bootstrap an agent token.",
1297
+ "3. Read /api/codex-app-server/live-state.",
1298
+ "4. Claim control with POST /api/codex-app-server/control when needed.",
1299
+ "5. Send a turn with POST /api/codex-app-server/turn.",
1300
+ "6. Subscribe to GET /api/stream for snapshot and live events.",
1301
+ "",
1302
+ "## Key Routes",
1303
+ "",
1304
+ "- GET /api/preflight",
1305
+ "- GET /api/codex-app-server/live-state",
1306
+ "- GET /api/codex-app-server/transcript-history",
1307
+ "- GET /api/codex-app-server/status",
1308
+ "- GET /api/codex-app-server/threads",
1309
+ "- POST /api/codex-app-server/selection",
1310
+ "- POST /api/codex-app-server/control",
1311
+ "- POST /api/codex-app-server/interaction",
1312
+ "- POST /api/codex-app-server/turn",
1313
+ "- GET /api/stream",
1314
+ "",
1315
+ "## Notes",
1316
+ "",
1317
+ "- Use surface=agent for automation rather than surface=remote.",
1318
+ "- Dextunnel is local-first and optimized for trusted local or tailnet access.",
1319
+ "- The desktop Codex app may still require a manual restart to rehydrate externally written turns."
1320
+ ].join("\n");
1321
+ }