clankerbend 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.
@@ -0,0 +1,1935 @@
1
+ # OneWhack Protocol
2
+
3
+ Status: draft
4
+
5
+ Disclaimer: OneWhack is an independent OneWill project compatible with OpenAI
6
+ Codex Desktop. It is not affiliated with or endorsed by OpenAI.
7
+
8
+ OneWhack Protocol, short for the OneWill Extension Hacks protocol, is a
9
+ standalone local protocol for building companion apps around Codex Desktop
10
+ without requiring changes to Codex Desktop itself. OneWhack separates a shared
11
+ local host from individual apps: the host owns the risky integration work, and
12
+ apps consume stable transcript, annotation, navigation, and state primitives.
13
+
14
+ OneWhack codifies the hack paths available today:
15
+
16
+ - a local host process bound to loopback
17
+ - a browser side panel loaded from that local host
18
+ - CDP attachment to the Codex Desktop Electron renderer
19
+ - renderer injection for transcript discovery, annotation, scroll, and highlight
20
+ - `codex app-server` awareness for thread, turn, item, approval, and rollback
21
+ data
22
+
23
+ OneWhack is not a proposal for a privileged built-in Codex plugin API. If Codex later
24
+ adds such an API, it should be able to replace the adapters underneath OneWhack, but
25
+ the protocol here is designed to work now with external tools.
26
+
27
+ ## Normative Language
28
+
29
+ The words **must**, **must not**, **should**, **should not**, and **may** are
30
+ normative when used in protocol sections. Examples and appendix material are
31
+ non-normative unless explicitly marked otherwise.
32
+
33
+ ## Goals
34
+
35
+ OneWhack should give any local app a small set of reusable primitives:
36
+
37
+ 1. **View**: start a local browser UI and place it in Codex Desktop's existing
38
+ side panel.
39
+ 2. **Attach**: connect to a live Codex Desktop renderer through CDP.
40
+ 3. **Observe**: read coarse Desktop context such as current URL, transcript
41
+ anchors, visibility, scroll state, and selected app state.
42
+ 4. **Annotate**: add lightweight clickable markers or labels near transcript
43
+ anchors without taking over the transcript DOM.
44
+ 5. **Navigate**: scroll to and highlight transcript anchors from the app panel.
45
+ 6. **Select**: keep app-side and transcript-side selection in sync without
46
+ stale state overwriting newer user intent.
47
+ 7. **Correlate**: optionally map Desktop anchors to app-server threads, turns,
48
+ and items when those identities are available.
49
+ 8. **Act**: route app-specific commands through the host without granting apps
50
+ direct CDP or app-server control.
51
+ 9. **Compose**: run multiple apps against one shared Desktop adapter and
52
+ transcript bridge.
53
+ 10. **Extend**: optionally attach app-defined state to anchors, such as
54
+ reviews, traces, bookmarks, diagnostics, provenance, or timeline events.
55
+
56
+ ## Scope
57
+
58
+ OneWhack `0.1` is transcript-adjacent. It covers side panels that observe, annotate,
59
+ navigate, and correlate Codex Desktop transcript content. Other Desktop
60
+ surfaces such as terminal panes, file trees, browser previews, command palette
61
+ commands, notifications, and settings are out of scope for this version.
62
+
63
+ ## Non-Goals
64
+
65
+ - OneWhack does not require Codex Desktop changes.
66
+ - OneWhack does not replace `codex app-server`.
67
+ - OneWhack does not define a security boundary.
68
+ - OneWhack does not grant host filesystem, shell, network, or approval authority.
69
+ - OneWhack does not assume exact item-to-DOM mapping is always possible.
70
+
71
+ ## Architecture
72
+
73
+ OneWhack describes a shared local host that can mount multiple apps.
74
+
75
+ ```text
76
+ OneWhack app panel(s) <-> OneWhack host <-> Codex Desktop adapter <-> renderer bridge
77
+ \
78
+ -> app-server adapter
79
+ ```
80
+
81
+ ### OneWhack Host
82
+
83
+ The OneWhack host is the trusted local process for a session. It owns the
84
+ loopback HTTP/SSE server, app registry, app state fanout, Desktop CDP adapter,
85
+ renderer bridge injection, app-server adapter, lifecycle cleanup, and policy
86
+ checks. Apps do not talk directly to CDP or app-server unless the host grants a
87
+ specific capability.
88
+
89
+ One host should be enough to serve multiple apps, such as a state-diff viewer,
90
+ Vim-style transcript navigation, diagnostics, bookmarks, or review tools. Apps
91
+ share transcript anchors, selection, app-server correlation, and side-panel
92
+ placement through the host.
93
+
94
+ ### OneWhack App
95
+
96
+ A OneWhack app is a UI and state contributor mounted by the host. It may provide
97
+ a side-panel route, annotations, commands, app state entries, and action
98
+ handlers. An app is identified by a stable `appId` and described by a manifest.
99
+
100
+ Apps should be replaceable. A panel should be able to render from host state and
101
+ app state without knowing whether Desktop integration is implemented through CDP,
102
+ a future built-in plugin API, or a mock adapter.
103
+
104
+ ### Codex Desktop Adapter
105
+
106
+ The Codex Desktop adapter is the implementation-specific component that launches
107
+ or attaches to Codex Desktop with a loopback CDP port and evaluates the injected
108
+ renderer bridge. This adapter is not the protocol. It is the current way the
109
+ OneWhack host realizes the protocol without Codex Desktop changes.
110
+
111
+ ### Injected Renderer Bridge
112
+
113
+ The JavaScript object installed in the Codex Desktop renderer. It discovers DOM
114
+ anchors, adds annotations, opens the native side panel through existing UI, and
115
+ executes scroll/highlight requests. The bridge is owned by the host, not by any
116
+ single app.
117
+
118
+ ### App-Server Adapter
119
+
120
+ Connection to `codex app-server --stdio` or another app-server transport when
121
+ available. It is used for task identity and lifecycle data, not for Desktop UI
122
+ control. A OneWhack host may still run when app-server is unavailable, but it
123
+ must report that state explicitly.
124
+
125
+ ## Transport
126
+
127
+ OneWhack uses ordinary local web primitives:
128
+
129
+ - HTTP JSON endpoints for commands
130
+ - Server-Sent Events for state updates
131
+ - CDP `Runtime.evaluate` for renderer bridge calls
132
+ - App-server JSON-RPC or JSONL streams for Codex task data when available
133
+
134
+ All host HTTP ports must bind to loopback.
135
+
136
+ ```text
137
+ 127.0.0.1:<ephemeral>
138
+ ```
139
+
140
+ External network exposure is out of scope.
141
+
142
+ ## Wire Format
143
+
144
+ All HTTP request and response bodies must be UTF-8 JSON unless the endpoint is
145
+ explicitly defined as SSE. JSON endpoints must use `application/json`.
146
+
147
+ Successful JSON responses use this envelope:
148
+
149
+ ```ts
150
+ type OneWhackResponse<T> = {
151
+ ok: true;
152
+ data: T;
153
+ };
154
+ ```
155
+
156
+ Failed JSON responses use this envelope:
157
+
158
+ ```ts
159
+ type OneWhackError = {
160
+ ok: false;
161
+ error: {
162
+ code:
163
+ | "bad_request"
164
+ | "unauthorized"
165
+ | "forbidden"
166
+ | "not_found"
167
+ | "conflict"
168
+ | "stale_selection"
169
+ | "capability_unavailable"
170
+ | "adapter_unavailable"
171
+ | "app_unavailable"
172
+ | "action_failed"
173
+ | "internal_error";
174
+ message: string;
175
+ detail?: unknown;
176
+ };
177
+ };
178
+ ```
179
+
180
+ HTTP status codes should match the envelope:
181
+
182
+ - `200`: successful read or completed command.
183
+ - `202`: accepted asynchronous action.
184
+ - `400`: invalid JSON or invalid request shape.
185
+ - `401`: missing or invalid session token.
186
+ - `403`: app lacks the requested permission.
187
+ - `404`: unknown endpoint, app, anchor, marker, or entry.
188
+ - `409`: stale selection, duplicate app id, or state conflict.
189
+ - `503`: required adapter or app-server capability is unavailable.
190
+ - `500`: unexpected host failure.
191
+
192
+ Hosts must not encode successful results as bare objects. A client can treat any
193
+ response without `ok: true` as failed.
194
+
195
+ ### Request Validation
196
+
197
+ JSON command endpoints must reject malformed requests before dispatching to an
198
+ app or adapter:
199
+
200
+ 1. Request bodies must parse as JSON objects. Arrays, strings, numbers, and
201
+ invalid JSON return `400` with code `bad_request`.
202
+ 2. Hosts should reject bodies larger than 1 MiB for OneWhack `0.1` JSON
203
+ endpoints.
204
+ 3. `POST /onewhack/apps/:appId/actions` requires an `action` object with
205
+ non-empty string `appId`, `actionId`, and `type` fields.
206
+ 4. `action.appId` must match the route `:appId`. A mismatch returns `400` with
207
+ code `bad_request`.
208
+ 5. `requestedAt` and `selectedAt`, when provided, must be parseable timestamps.
209
+ 6. Transcript scroll and highlight requests require a non-empty `anchorId`.
210
+ 7. Enum-like option fields such as scroll `behavior`, scroll `block`, and
211
+ selection `source` must reject unknown values with `400`.
212
+
213
+ These checks are host responsibilities. App action handlers may perform
214
+ additional validation for app-specific payloads.
215
+
216
+ ### Session Token
217
+
218
+ A production OneWhack host must protect JSON and SSE endpoints with an
219
+ unguessable session token. The token is passed as:
220
+
221
+ ```text
222
+ Authorization: Bearer <token>
223
+ ```
224
+
225
+ Local development hosts may omit authentication only when the host manifest sets:
226
+
227
+ ```json
228
+ {
229
+ "security": {
230
+ "localDevInsecure": true
231
+ }
232
+ }
233
+ ```
234
+
235
+ An app must not bypass the host token by calling CDP or app-server directly.
236
+
237
+ ### Bootstrap
238
+
239
+ OneWhack `0.1` defines two token bootstrap paths:
240
+
241
+ 1. **Hosted panel apps**: the host serves the app entry URL and may append the
242
+ token in the URL fragment as `#onewhack_token=<token>`. App JavaScript reads
243
+ the fragment, stores the token in memory, immediately removes the fragment
244
+ with `history.replaceState`, and sends the token in the `Authorization`
245
+ header for JSON and SSE requests.
246
+ 2. **External clients**: the token is provided out of band by the launcher, such
247
+ as an environment variable, config file, local IPC channel, or command output.
248
+
249
+ Hosts must not require a token in the query string. Query strings are more
250
+ likely to be logged or copied than URL fragments. Hosts may additionally set a
251
+ same-origin secure session cookie for panels they serve, but clients must still
252
+ support the bearer-token header.
253
+
254
+ ## State Model
255
+
256
+ The host maintains one public state object and streams snapshots to the panel.
257
+
258
+ ```ts
259
+ type OneWhackPublicState = {
260
+ protocolName: "onewhack";
261
+ protocolVersion: "0.1";
262
+ sequence: number;
263
+ generatedAt: string;
264
+ capabilities: OneWhackCapabilities;
265
+ host: HostStatus;
266
+ desktop: DesktopStatus;
267
+ panel: PanelStatus;
268
+ transcript: TranscriptState;
269
+ selection: OneWhackSelection | null;
270
+ selectionActions?: OneWhackSelectionAction[];
271
+ overlay?: OneWhackOverlay | null;
272
+ composer?: OneWhackComposerState;
273
+ apps: OneWhackAppState[];
274
+ appServer: AppServerStatus;
275
+ lastAction?: OneWhackActionResult;
276
+ };
277
+ ```
278
+
279
+ ```ts
280
+ type HostStatus = {
281
+ status: "starting" | "running" | "degraded" | "stopping" | "exited";
282
+ hostId: string;
283
+ url: string;
284
+ launchedAt?: string;
285
+ error?: string;
286
+ };
287
+
288
+ type PanelStatus = {
289
+ status: "closed" | "opening" | "waiting" | "open" | "focused" | "unavailable" | "error";
290
+ activeAppId?: string;
291
+ url?: string;
292
+ preferredWidth?: number;
293
+ lastOpenedAt?: string;
294
+ error?: string;
295
+ };
296
+
297
+ type TranscriptState = {
298
+ anchors: TranscriptAnchor[];
299
+ visibleCount: number;
300
+ annotationCount: number;
301
+ scroll?: TranscriptScrollState;
302
+ updatedAt?: string;
303
+ };
304
+
305
+ type TranscriptScrollState = {
306
+ top: number;
307
+ height: number;
308
+ clientHeight: number;
309
+ };
310
+ ```
311
+
312
+ The state object is intentionally denormalized so the panel can render from a
313
+ single event payload.
314
+
315
+ ### `GET /onewhack/state`
316
+
317
+ Returns the latest public state.
318
+
319
+ ```ts
320
+ type GetStateResponse = OneWhackResponse<OneWhackPublicState>;
321
+ ```
322
+
323
+ ### `GET /onewhack/events`
324
+
325
+ Streams host events as SSE. The host must send a full state snapshot
326
+ immediately after a client connects.
327
+
328
+ ```text
329
+ event: state
330
+ id: 42
331
+ data: {"protocolName":"onewhack","protocolVersion":"0.1","sequence":42,...}
332
+ ```
333
+
334
+ SSE rules:
335
+
336
+ 1. `id` is the decimal string form of `OneWhackPublicState.sequence`.
337
+ 2. `state` events contain a complete `OneWhackPublicState`.
338
+ 3. `app-state` events may contain one `OneWhackAppState`; clients must still be
339
+ able to recover from `state`.
340
+ 4. `action` events contain `OneWhackActionResult`.
341
+ 5. `heartbeat` events have empty JSON object data and should be sent at least
342
+ every 15 seconds.
343
+ 6. `error` events contain `OneWhackError["error"]`.
344
+ 7. Hosts should avoid emitting `state` when nothing semantically changed.
345
+ 8. Clients may reconnect with `Last-Event-ID`; hosts may replay missed events
346
+ but must send a fresh `state` event even if replay is unavailable.
347
+ 9. Clients must preserve active text selection and expanded rows while applying
348
+ updates.
349
+
350
+ ### Capabilities
351
+
352
+ The host exposes negotiated capabilities in every state snapshot so apps can
353
+ degrade cleanly.
354
+
355
+ ```ts
356
+ type OneWhackCapabilities = {
357
+ protocolVersion: "0.1";
358
+ host: {
359
+ apps: boolean;
360
+ actions: boolean;
361
+ appState: boolean;
362
+ sidePanel: boolean;
363
+ };
364
+ transcript: {
365
+ read: boolean;
366
+ annotate: boolean;
367
+ navigate: boolean;
368
+ select: boolean;
369
+ rangeSelect?: boolean;
370
+ rangeHighlight?: boolean;
371
+ };
372
+ overlay?: {
373
+ anchored: boolean;
374
+ forms: boolean;
375
+ };
376
+ composer?: {
377
+ contextItems: boolean;
378
+ draft: boolean;
379
+ submit?: boolean;
380
+ };
381
+ adapter: {
382
+ name: "codex-desktop-cdp" | string;
383
+ cdp: boolean;
384
+ rendererInjection: boolean;
385
+ rendererFetchToLoopback?: boolean;
386
+ providers?: {
387
+ transcriptSnapshot: string;
388
+ transcriptOrder: string;
389
+ transcriptNavigation: string;
390
+ transcriptHighlight: string;
391
+ transcriptTextSelection?: string;
392
+ composerDraft?: string;
393
+ composerContext?: string;
394
+ };
395
+ };
396
+ appServer: {
397
+ available: boolean;
398
+ correlateItems: boolean;
399
+ approvals: boolean;
400
+ rollback: boolean;
401
+ };
402
+ };
403
+ ```
404
+
405
+ `adapter.providers` is launch-time configuration. It names which registered app
406
+ bridge supplies shared transcript behavior. Multiple apps may still contribute
407
+ annotations and app state, but only the configured provider handles each shared
408
+ transcript operation until a future protocol version adds dynamic arbitration.
409
+
410
+ ### Forward Compatibility
411
+
412
+ OneWhack clients must treat minor-version protocol changes as additive.
413
+ Unknown object fields, unknown capability keys, unknown app state entry fields,
414
+ and unknown action result data must be ignored by default. Apps must gate
415
+ optional behavior on the capabilities they read from `GET /onewhack/manifest`
416
+ or `GET /onewhack/state`; they must not infer availability from the host name,
417
+ adapter name, or Codex Desktop version alone.
418
+
419
+ Hosts must keep the `ok` envelope, error envelope, endpoint names, and required
420
+ fields stable within a major protocol version. A host may add endpoints,
421
+ capabilities, app manifest fields, app state fields, action types, SSE event
422
+ types, or error `detail` content without breaking compatible clients. A client
423
+ that receives an unknown SSE event type should ignore it and recover from the
424
+ next `state` event.
425
+
426
+ Breaking changes require a major version bump. A host that implements a newer
427
+ major protocol may continue to expose a compatible older route set, but if it
428
+ does not, it must report the newer `protocolVersion` before app-specific
429
+ actions are required.
430
+
431
+ ## Host API
432
+
433
+ The host API is the stable surface apps use. Codex Desktop adapter details are
434
+ behind the host.
435
+
436
+ Path parameters such as `:appId` and `:actionId` must be percent-encoded when
437
+ they contain characters outside unreserved URI characters. App ids should use
438
+ lowercase reverse-DNS-style names such as `onewill.vim-nav`.
439
+
440
+ ### Version Negotiation
441
+
442
+ Apps must read host capabilities before assuming any optional feature exists.
443
+
444
+ ```text
445
+ GET /onewhack/manifest
446
+ GET /onewhack/state
447
+ GET /onewhack/events
448
+ ```
449
+
450
+ `GET /onewhack/manifest` returns host identity, protocol version, and mounted
451
+ app manifests.
452
+
453
+ ```ts
454
+ type GetManifestResponse = OneWhackResponse<OneWhackHostManifest>;
455
+
456
+ type OneWhackHostManifest = {
457
+ oneWhackVersion: "0.1";
458
+ hostId: string;
459
+ hostName: string;
460
+ capabilities: OneWhackCapabilities;
461
+ security?: {
462
+ auth: "bearer" | "none";
463
+ localDevInsecure?: boolean;
464
+ };
465
+ apps: OneWhackAppManifest[];
466
+ };
467
+ ```
468
+
469
+ If an app requires a capability that the host does not expose, the app must
470
+ render a degraded state rather than attempting direct CDP or app-server access.
471
+
472
+ ### App Registry
473
+
474
+ The host exposes mounted apps through a registry.
475
+
476
+ ```text
477
+ GET /onewhack/apps
478
+ GET /onewhack/apps/:appId/manifest
479
+ GET /onewhack/apps/:appId/state
480
+ GET /onewhack/apps/:appId/actions/:actionId
481
+ POST /onewhack/apps/:appId/actions
482
+ ```
483
+
484
+ The registry lets one host serve multiple apps without each app owning a
485
+ separate CDP adapter or injected bridge.
486
+
487
+ ```ts
488
+ type GetAppsResponse = OneWhackResponse<{
489
+ apps: OneWhackAppSummary[];
490
+ }>;
491
+
492
+ type OneWhackAppSummary = {
493
+ appId: string;
494
+ name: string;
495
+ status: OneWhackAppStatus;
496
+ entry?: string;
497
+ };
498
+
499
+ type GetAppManifestResponse = OneWhackResponse<OneWhackAppManifest>;
500
+ type GetAppStateResponse = OneWhackResponse<OneWhackAppState>;
501
+
502
+ type OneWhackAppStatus =
503
+ | "mounted"
504
+ | "loading"
505
+ | "ready"
506
+ | "degraded"
507
+ | "disabled"
508
+ | "error";
509
+
510
+ type OneWhackAppManifest = {
511
+ oneWhackVersion: "0.1";
512
+ appId: string;
513
+ version: string;
514
+ name: string;
515
+ description?: string;
516
+ entry: string;
517
+ distribution?: OneWhackAppDistribution;
518
+ entrypoint?: OneWhackAppEntrypoint;
519
+ capabilities?: {
520
+ panel?: boolean;
521
+ annotations?: boolean;
522
+ commands?: boolean;
523
+ actions?: boolean;
524
+ appState?: boolean;
525
+ selectionActions?: boolean;
526
+ overlays?: boolean;
527
+ composerContext?: boolean;
528
+ composerDraft?: boolean;
529
+ rendererBridge?: boolean;
530
+ };
531
+ contributes?: {
532
+ panel?: boolean;
533
+ annotations?: boolean;
534
+ commands?: boolean;
535
+ actions?: boolean;
536
+ appState?: boolean;
537
+ selectionActions?: boolean;
538
+ overlays?: boolean;
539
+ composerContext?: boolean;
540
+ composerDraft?: boolean;
541
+ };
542
+ permissions?: {
543
+ transcriptRead?: boolean;
544
+ transcriptAnnotate?: boolean;
545
+ transcriptNavigate?: boolean;
546
+ overlayWrite?: boolean;
547
+ composerWrite?: boolean;
548
+ appServerRead?: boolean;
549
+ appServerApprove?: boolean;
550
+ appServerRollback?: boolean;
551
+ };
552
+ panel?: {
553
+ title: string;
554
+ reloadPolicy: "preserve" | "reload";
555
+ preferredWidth?: number;
556
+ };
557
+ rendererBridge?: OneWhackRendererBridgeManifest;
558
+ lifecycle?: OneWhackAppLifecycleManifest;
559
+ };
560
+
561
+ type OneWhackAppDistribution = {
562
+ kind: "local" | "npm" | "tarball" | "binary";
563
+ source?: string;
564
+ integrity?: string;
565
+ update?: {
566
+ channel?: string;
567
+ url?: string;
568
+ packageName?: string;
569
+ };
570
+ };
571
+
572
+ type OneWhackAppEntrypoint = {
573
+ kind: "module" | "binary" | "static";
574
+ module?: string;
575
+ factory?: string;
576
+ command?: string;
577
+ args?: string[];
578
+ publicDir?: string;
579
+ };
580
+
581
+ type OneWhackRendererBridgeManifest = {
582
+ script: string;
583
+ primary?: boolean;
584
+ provides?: (
585
+ | "transcriptSnapshot"
586
+ | "transcriptOrder"
587
+ | "transcriptNavigation"
588
+ | "transcriptHighlight"
589
+ | "transcriptTextSelection"
590
+ | "composerDraft"
591
+ | "composerContext"
592
+ )[];
593
+ methods?: {
594
+ openPanel?: string;
595
+ scroll?: string;
596
+ highlight?: string;
597
+ };
598
+ };
599
+
600
+ type OneWhackAppLifecycleManifest = {
601
+ install?: { kind: string };
602
+ start?: { kind: string };
603
+ stop?: { kind: string };
604
+ update?: { kind: string };
605
+ remove?: { kind: string };
606
+ };
607
+ ```
608
+
609
+ ### Actions
610
+
611
+ Apps send commands through the host so the host can enforce capability and
612
+ approval rules.
613
+
614
+ ```ts
615
+ type PostAppActionRequest = {
616
+ action: OneWhackAction;
617
+ };
618
+
619
+ type PostAppActionResponse = OneWhackResponse<
620
+ OneWhackActionResult | OneWhackActionReceipt
621
+ >;
622
+
623
+ type OneWhackAction = {
624
+ actionId: string;
625
+ appId: string;
626
+ type: string;
627
+ anchorId?: string;
628
+ entryId?: string;
629
+ markerId?: string;
630
+ payload?: unknown;
631
+ requestedAt: string;
632
+ };
633
+
634
+ type OneWhackActionReceipt = {
635
+ actionId: string;
636
+ appId: string;
637
+ accepted: true;
638
+ status: "accepted";
639
+ resultUrl?: string;
640
+ };
641
+
642
+ type OneWhackActionResult = {
643
+ actionId: string;
644
+ appId: string;
645
+ ok: boolean;
646
+ status?: "accepted" | "rejected" | "applied" | "failed";
647
+ error?: string;
648
+ data?: unknown;
649
+ completedAt?: string;
650
+ };
651
+ ```
652
+
653
+ Action rules:
654
+
655
+ 1. `actionId` must be unique per `appId`.
656
+ 2. If the same `(appId, actionId)` is submitted again, the host must return the
657
+ original receipt or result without executing the action twice.
658
+ 3. Hosts should complete fast actions synchronously with `200`.
659
+ 4. Hosts may accept long-running actions with `202`; the final result must be
660
+ emitted as an `action` SSE event.
661
+ 5. Unknown action `type` values must return `404` with code `not_found`.
662
+ 6. Missing permissions must return `403` with code `forbidden`.
663
+ 7. Unavailable adapters must return `503` with code `adapter_unavailable` or
664
+ `capability_unavailable`.
665
+ 8. When `resultUrl` is present, it must point to
666
+ `/onewhack/apps/:appId/actions/:actionId`.
667
+ 9. App handlers must return an action result with boolean `ok`. Invalid handler
668
+ returns must fail with `500` and code `action_failed`.
669
+
670
+ Public examples include `vim.jump`, `vim.search`, `bookmark.add`, and
671
+ `diagnostics.open`. App-specific action types do not need to be published in
672
+ this document, but they must follow the same action contract.
673
+
674
+ ### Action Result Lookup
675
+
676
+ Hosts must retain completed action results for at least 60 seconds.
677
+
678
+ ```text
679
+ GET /onewhack/apps/:appId/actions/:actionId
680
+ ```
681
+
682
+ ```ts
683
+ type GetAppActionResponse = OneWhackResponse<
684
+ OneWhackActionResult | OneWhackActionReceipt
685
+ >;
686
+ ```
687
+
688
+ This endpoint lets clients recover when they miss an SSE `action` event.
689
+
690
+ ### App Lifecycle
691
+
692
+ OneWhack `0.1` defines manifest-based app registration. A host may install apps
693
+ from local manifests, npm packages, tarballs, or binary-style bundles. Download
694
+ and update transports are implementation details, but every installed app must
695
+ resolve to a validated `OneWhackAppManifest`.
696
+
697
+ The public launcher command is intentionally small:
698
+
699
+ ```text
700
+ clankerbend codex
701
+ ```
702
+
703
+ App installation, enablement, update, and removal are product configuration
704
+ concerns owned by the running OneWhack experience. The registry model remains
705
+ part of the host protocol, but apps should not depend on a public app-management
706
+ CLI being present.
707
+
708
+ Regardless of loading mechanism, a mounted app must appear in
709
+ `GET /onewhack/apps`.
710
+
711
+ Lifecycle rules:
712
+
713
+ 1. App ids must be unique within a host.
714
+ 2. A manifest parse failure must produce an app summary with `status: "error"`
715
+ when the host can identify the intended `appId`.
716
+ 3. Disabled apps should remain visible with `status: "disabled"` and must not
717
+ contribute annotations or actions.
718
+ 4. A degraded app may contribute read-only state but must not perform actions
719
+ that require unavailable capabilities.
720
+ 5. Hosts must remove an app's annotations when that app is disabled, unmounted,
721
+ or enters unrecoverable error.
722
+ 6. Hosts should keep app state available after recoverable errors so panels can
723
+ render the failure and any last-known entries.
724
+ 7. Hosts must reject manifests with unknown capabilities or permissions unless
725
+ the host advertises a newer compatible protocol version.
726
+ 8. An app must not receive hidden APIs that are unavailable to another app
727
+ declaring the same manifest.
728
+
729
+ ### Panel Model
730
+
731
+ OneWhack `0.1` assumes one Codex Desktop side panel controlled by the host. The
732
+ host may implement that side panel as:
733
+
734
+ - a shared shell that routes between mounted apps, or
735
+ - direct app entry pages loaded one at a time.
736
+
737
+ In either model:
738
+
739
+ 1. `PanelStatus.activeAppId` identifies the app currently shown when known.
740
+ 2. `POST /onewhack/panel/open` opens or focuses the host panel and may switch
741
+ the active panel app by `appId`.
742
+ 3. `POST /onewhack/apps/:appId/actions` may switch the active app when the
743
+ action requires app UI.
744
+ 4. Opening an already-loaded panel must not reload the page unless the app
745
+ manifest uses `reloadPolicy: "reload"`.
746
+ 5. Hosted app routes should live under `/apps/:appId/` on the host origin.
747
+
748
+ ## Codex Desktop Adapter
749
+
750
+ ### Launch
751
+
752
+ OneWhack can launch Codex Desktop with CDP:
753
+
754
+ ```sh
755
+ /Applications/Codex.app/Contents/MacOS/Codex \
756
+ --remote-debugging-address=127.0.0.1 \
757
+ --remote-debugging-port=<free> \
758
+ --user-data-dir=<test-or-onewhack-profile>
759
+ ```
760
+
761
+ The host must not patch `/Applications/Codex.app`.
762
+
763
+ ### Attach
764
+
765
+ The CDP adapter attaches to the `app://-/index.html` renderer target. If
766
+ `/json/list` is unavailable or unstable, the adapter may attach via the browser
767
+ WebSocket and target discovery.
768
+
769
+ ### `DesktopStatus`
770
+
771
+ ```ts
772
+ type DesktopStatus = {
773
+ cdpStatus: "starting" | "waiting-for-renderer" | "connected" | "disconnected" | "exited";
774
+ cdpPort?: number;
775
+ desktopPid?: number;
776
+ target?: {
777
+ type?: string;
778
+ title?: string;
779
+ url?: string;
780
+ };
781
+ error?: string;
782
+ };
783
+ ```
784
+
785
+ ## Renderer Runtime API
786
+
787
+ The CDP adapter initializes a single renderer runtime at:
788
+
789
+ ```ts
790
+ window.__oneWhackRuntime
791
+ ```
792
+
793
+ Apps register into runtime-owned app slots. The runtime is the only privileged
794
+ renderer global; individual apps must not require their own global bridge names.
795
+
796
+ ```ts
797
+ type OneWhackRendererRuntime = {
798
+ protocolVersion: string;
799
+ hostUrl: string;
800
+ apps: Record<string, RendererAppSlot>;
801
+ registerApp(app: RendererAppRegistration): RendererAppSlot;
802
+ getApp(appId: string): RendererAppSlot | null;
803
+ getBridge(appId: string): RendererBridge | null;
804
+ getEntryUrl(appId: string): string | null;
805
+ placeAnnotation(anchor: HTMLElement, annotation: RendererPlacedAnnotation): HTMLElement;
806
+ removeAnnotations(appId: string, liveAnchorIds?: Iterable<string>): void;
807
+ };
808
+
809
+ type RendererAppSlot = {
810
+ appId: string;
811
+ entryUrl: string | null;
812
+ capabilities: Record<string, unknown>;
813
+ bridge: RendererBridge | null;
814
+ injectedAt: string | null;
815
+ };
816
+
817
+ type RendererAppRegistration = {
818
+ appId: string;
819
+ entryUrl?: string;
820
+ capabilities?: Record<string, unknown>;
821
+ bridge: RendererBridge;
822
+ injectedAt?: string;
823
+ };
824
+
825
+ type RendererPlacedAnnotation = {
826
+ appId: string;
827
+ anchorId: string;
828
+ markerId?: string;
829
+ priority?: number;
830
+ placement?: "leading-rail" | "above";
831
+ element: HTMLElement;
832
+ };
833
+ ```
834
+
835
+ Each app bridge must be idempotent. Re-evaluating an app injection should update
836
+ styles and handlers without duplicating UI.
837
+
838
+ ```ts
839
+ type RendererBridge = {
840
+ version: number;
841
+ snapshot(): RendererSnapshot;
842
+ openPanel(): Promise<PanelOpenResult>;
843
+ scrollToAnchor(anchorId: string, options?: ScrollOptions): ScrollResult;
844
+ highlightAnchor(anchorId: string, options?: HighlightOptions): HighlightResult;
845
+ setAnnotations(annotations: AnchorAnnotation[]): AnnotationResult;
846
+ };
847
+ ```
848
+
849
+ `placeAnnotation()` is the renderer-side primitive for injected bridge code that
850
+ has already created a marker element. Apps ask the runtime to place an
851
+ annotation on a transcript anchor; the runtime, not the app, owns the host
852
+ container, slot ordering, offsets, stacking, cleanup, and collision avoidance.
853
+ Injected app bridges must not absolutely position transcript-adjacent markers
854
+ against Codex Desktop transcript rows themselves.
855
+
856
+ `removeAnnotations()` removes stale host-owned slots for one app without
857
+ touching markers owned by other apps. When `liveAnchorIds` is provided, the
858
+ runtime preserves that app's slots only for those anchors.
859
+
860
+ ### `snapshot()`
861
+
862
+ Discovers the transcript state from the live DOM.
863
+
864
+ ```ts
865
+ type RendererSnapshot = {
866
+ href: string;
867
+ title: string;
868
+ version: number;
869
+ scroll: {
870
+ top: number;
871
+ height: number;
872
+ clientHeight: number;
873
+ };
874
+ anchors: TranscriptAnchor[];
875
+ visibleCount: number;
876
+ annotationCount: number;
877
+ selection: OneWhackSelection | null;
878
+ };
879
+ ```
880
+
881
+ The current DOM anchor selectors are implementation details of the adapter, but
882
+ the current Codex Desktop adapter probes these selectors:
883
+
884
+ ```text
885
+ [data-content-search-unit-key]
886
+ [data-turn-key]
887
+ [data-content-search-turn-key]
888
+ [data-thread-user-message-navigation-item-id]
889
+ ```
890
+
891
+ ### Transcript Anchor
892
+
893
+ ```ts
894
+ type TranscriptAnchor = {
895
+ anchorId: string;
896
+ kind:
897
+ | "content-search-unit"
898
+ | "turn"
899
+ | "content-search-turn"
900
+ | "navigation-item"
901
+ | "unknown";
902
+ visible: boolean;
903
+ top?: number;
904
+ height?: number;
905
+ textPreview: string;
906
+ order: number;
907
+ inferredRole?: "user" | "assistant" | "system" | "tool";
908
+ appServer?: {
909
+ threadId?: string;
910
+ turnId?: string;
911
+ itemId?: string;
912
+ };
913
+ };
914
+ ```
915
+
916
+ `anchorId` is the DOM-derived navigation key. It is only guaranteed stable for
917
+ the current Desktop renderer session unless the adapter can map it to
918
+ app-server identity.
919
+
920
+ ### Identity Tiers
921
+
922
+ OneWhack uses separate identity tiers because exact app-server-to-DOM mapping is
923
+ not always available.
924
+
925
+ ```ts
926
+ type OneWhackIdentity = {
927
+ anchorId?: string;
928
+ entryId?: string;
929
+ markerId?: string;
930
+ appId?: string;
931
+ appServer?: {
932
+ threadId?: string;
933
+ turnId?: string;
934
+ itemId?: string;
935
+ };
936
+ correlationId?: string;
937
+ };
938
+ ```
939
+
940
+ - `anchorId` identifies a live transcript DOM anchor for the current renderer
941
+ session.
942
+ - `entryId` identifies app-owned state, such as a timeline event, bookmark, or
943
+ navigation target.
944
+ - `markerId` identifies a clickable annotation marker contributed by an app.
945
+ - `appServer.threadId`, `turnId`, and `itemId` identify Codex task objects when
946
+ app-server correlation is available.
947
+ - `correlationId` is a best-effort host-generated link between two or more
948
+ identity tiers.
949
+
950
+ Apps must not assume `anchorId` is durable across restarts. Durable state should
951
+ prefer app-owned `entryId` plus app-server identity when available.
952
+
953
+ ### `openPanel()`
954
+
955
+ Uses existing Codex Desktop UI to open the Browser side panel and load the host
956
+ URL.
957
+
958
+ ```ts
959
+ type PanelOpenResult = {
960
+ ok: boolean;
961
+ mode?:
962
+ | "opened-url-input"
963
+ | "opened-local-server-list"
964
+ | "already-open"
965
+ | "focused";
966
+ error?: string;
967
+ };
968
+ ```
969
+
970
+ Important rule: if the webview or iframe is already on the host URL,
971
+ `openPanel()` must return `already-open` and must not reload it. Reloading the
972
+ panel destroys selection and expansion state.
973
+
974
+ ### `scrollToAnchor()`
975
+
976
+ Scrolls a transcript anchor into view.
977
+
978
+ ```ts
979
+ type ScrollOptions = {
980
+ behavior?: "auto" | "smooth";
981
+ block?: "start" | "center" | "end" | "nearest";
982
+ };
983
+
984
+ type ScrollResult = {
985
+ ok: boolean;
986
+ anchorId?: string;
987
+ error?: string;
988
+ };
989
+ ```
990
+
991
+ ### `highlightAnchor()`
992
+
993
+ Temporarily highlights a transcript anchor.
994
+
995
+ ```ts
996
+ type HighlightOptions = {
997
+ durationMs?: number;
998
+ };
999
+
1000
+ type HighlightResult = {
1001
+ ok: boolean;
1002
+ anchorId?: string;
1003
+ error?: string;
1004
+ };
1005
+ ```
1006
+
1007
+ ### `setAnnotations()`
1008
+
1009
+ Adds or replaces transcript-adjacent annotations.
1010
+
1011
+ ```ts
1012
+ type AnchorAnnotation = {
1013
+ appId: string;
1014
+ anchorId: string;
1015
+ placement: "leading-rail" | "above";
1016
+ markers: AnchorAnnotationMarker[];
1017
+ priority?: number;
1018
+ };
1019
+
1020
+ type AnchorAnnotationMarker = {
1021
+ markerId: string;
1022
+ glyph: string;
1023
+ label: string;
1024
+ tone?: "default" | "info" | "success" | "warning" | "danger" | "accent" | "muted";
1025
+ shape?: "circle" | "square" | "squircle" | "diamond" | "custom";
1026
+ category?: string;
1027
+ badge?: {
1028
+ glyph: string;
1029
+ label: string;
1030
+ tone?: "default" | "info" | "success" | "warning" | "danger" | "accent" | "muted";
1031
+ };
1032
+ entryId?: string;
1033
+ action?: {
1034
+ type: string;
1035
+ payload?: unknown;
1036
+ };
1037
+ data?: unknown;
1038
+ };
1039
+
1040
+ type AnnotationResult = {
1041
+ ok: boolean;
1042
+ applied: number;
1043
+ error?: string;
1044
+ };
1045
+ ```
1046
+
1047
+ The renderer bridge may infer annotations when no external data source is
1048
+ available. Inferred markers must be marked as inferred in host state.
1049
+
1050
+ Annotation ownership rules:
1051
+
1052
+ 1. Every app-owned annotation must include `appId`.
1053
+ 2. The host merges annotations from all apps for the same `anchorId`.
1054
+ 3. Lower `priority` values render earlier; omitted priority defaults to `100`.
1055
+ Ties sort by `appId`, then `markerId`, both lexicographically.
1056
+ 4. Marker clicks route to the contributing app through
1057
+ `POST /onewhack/apps/:appId/actions`.
1058
+ 5. The renderer bridge must remove stale markers for an app without removing
1059
+ markers owned by other apps.
1060
+ 6. Marker identity is scoped by `(appId, markerId)`. Two apps may use the same
1061
+ `markerId` without collision.
1062
+ 7. If a placement cannot fit all markers, the host should preserve sorted order
1063
+ and may collapse overflow into a host-owned overflow marker.
1064
+ 8. Apps request placement; the host decides actual layout. Apps may style the
1065
+ marker contents, but must not rely on fixed offsets, negative margins, or
1066
+ transcript-row absolute positioning for placement.
1067
+
1068
+ ## Panel API
1069
+
1070
+ The panel talks only to the host server. It does not need direct CDP access.
1071
+
1072
+ ### `POST /onewhack/panel/open`
1073
+
1074
+ Asks the host to open/focus the panel in Codex Desktop. The host calls
1075
+ `window.__oneWhackRuntime.getBridge(activeAppId).openPanel()` or an equivalent
1076
+ app-id-scoped bridge call via CDP.
1077
+
1078
+ ```ts
1079
+ type OpenPanelRequest = {
1080
+ appId?: string;
1081
+ };
1082
+ ```
1083
+
1084
+ When `appId` is provided, the host must set `PanelStatus.activeAppId` to that
1085
+ registered app before loading the panel URL. Unknown app ids return `404`.
1086
+
1087
+ ```ts
1088
+ type OpenPanelResponse = OneWhackResponse<PanelOpenResult>;
1089
+ ```
1090
+
1091
+ ### `POST /onewhack/transcript/scroll`
1092
+
1093
+ ```json
1094
+ {
1095
+ "anchorId": "opaque-anchor",
1096
+ "behavior": "smooth",
1097
+ "block": "center"
1098
+ }
1099
+ ```
1100
+
1101
+ ```ts
1102
+ type ScrollToAnchorRequest = {
1103
+ anchorId: string;
1104
+ behavior?: "auto" | "smooth";
1105
+ block?: "start" | "center" | "end" | "nearest";
1106
+ };
1107
+
1108
+ type ScrollToAnchorResponse = OneWhackResponse<ScrollResult>;
1109
+ ```
1110
+
1111
+ ### `POST /onewhack/transcript/highlight`
1112
+
1113
+ ```json
1114
+ {
1115
+ "anchorId": "opaque-anchor",
1116
+ "durationMs": 2200
1117
+ }
1118
+ ```
1119
+
1120
+ ```ts
1121
+ type HighlightAnchorRequest = {
1122
+ anchorId: string;
1123
+ durationMs?: number;
1124
+ };
1125
+
1126
+ type HighlightAnchorResponse = OneWhackResponse<HighlightResult>;
1127
+ ```
1128
+
1129
+ ### `POST /onewhack/selection`
1130
+
1131
+ Updates global host selection.
1132
+
1133
+ ```json
1134
+ {
1135
+ "selection": {
1136
+ "selectionId": "sel_123",
1137
+ "source": "panel",
1138
+ "appId": "example.app",
1139
+ "anchorId": "opaque-anchor",
1140
+ "entryId": "app-entry-1",
1141
+ "selectedAt": "2026-06-17T19:21:00.000Z"
1142
+ }
1143
+ }
1144
+ ```
1145
+
1146
+ ```ts
1147
+ type PostSelectionRequest = {
1148
+ selection: OneWhackSelection;
1149
+ };
1150
+
1151
+ type PostSelectionResponse = OneWhackResponse<{
1152
+ selection: OneWhackSelection;
1153
+ stale?: boolean;
1154
+ }>;
1155
+ ```
1156
+
1157
+ The host must ignore stale selections. A selection is stale when its `selectedAt`
1158
+ is older than the current accepted selection. If two selections have the same
1159
+ `selectedAt`, host receive order wins. On every accepted selection, the host must
1160
+ assign a monotonically increasing `sequence` and `acceptedAt` timestamp.
1161
+
1162
+ ```ts
1163
+ type OneWhackSelection = {
1164
+ selectionId: string;
1165
+ sequence?: number;
1166
+ source: "panel" | "transcript" | "adapter";
1167
+ appId?: string;
1168
+ anchorId?: string;
1169
+ quote?: string;
1170
+ range?: OneWhackTextRange;
1171
+ markerId?: string;
1172
+ entryId?: string;
1173
+ appServer?: {
1174
+ threadId?: string;
1175
+ turnId?: string;
1176
+ itemId?: string;
1177
+ };
1178
+ correlationId?: string;
1179
+ selectedAt: string;
1180
+ acceptedAt?: string;
1181
+ };
1182
+ ```
1183
+
1184
+ This monotonic rule prevents stale renderer polling from restoring an
1185
+ older transcript click after the user selected a newer side-panel entry.
1186
+
1187
+ ### Text Ranges
1188
+
1189
+ Apps must not inspect Codex Desktop DOM to interpret selected text. When the
1190
+ adapter observes a transcript text selection, it normalizes that selection into
1191
+ a `OneWhackTextRange` and updates host `selection`.
1192
+
1193
+ ```ts
1194
+ type OneWhackTextRange = {
1195
+ anchorId: string;
1196
+ text: string;
1197
+ quote?: string;
1198
+ prefix?: string;
1199
+ suffix?: string;
1200
+ startOffset?: number;
1201
+ endOffset?: number;
1202
+ fingerprint?: string;
1203
+ };
1204
+ ```
1205
+
1206
+ `prefix`, `suffix`, offsets, and `fingerprint` are best-effort adapter hints.
1207
+ Apps may store them for later source matching, but they must treat `anchorId`
1208
+ plus selected `text` as the only required fields.
1209
+
1210
+ ```text
1211
+ POST /onewhack/transcript/highlight-range
1212
+ ```
1213
+
1214
+ ```ts
1215
+ type PostHighlightRangeRequest = {
1216
+ range: OneWhackTextRange;
1217
+ durationMs?: number;
1218
+ behavior?: "auto" | "smooth";
1219
+ block?: "start" | "center" | "end" | "nearest";
1220
+ };
1221
+ ```
1222
+
1223
+ The host delegates range mapping to the adapter. If precise text-range
1224
+ highlighting is unavailable, the adapter may highlight the containing anchor.
1225
+
1226
+ ### Selection Actions
1227
+
1228
+ Apps can contribute actions for host-rendered transcript selection menus through
1229
+ app state. The host or renderer runtime owns menu placement and click routing.
1230
+
1231
+ ```ts
1232
+ type OneWhackSelectionAction = {
1233
+ actionId: string;
1234
+ appId: string;
1235
+ type: string;
1236
+ label: string;
1237
+ icon?: string;
1238
+ appliesTo: "text-selection" | "anchor-selection";
1239
+ enabled?: boolean;
1240
+ payload?: unknown;
1241
+ };
1242
+ ```
1243
+
1244
+ When the user chooses a selection action, the host sends a normal
1245
+ `POST /onewhack/apps/:appId/actions` request with the current selection in the
1246
+ action payload or app context.
1247
+
1248
+ ### Anchored Overlays
1249
+
1250
+ Apps can request a host-rendered overlay anchored to an anchor or text range.
1251
+ Apps provide fields and actions; the host/adapter owns DOM placement.
1252
+
1253
+ ```text
1254
+ POST /onewhack/overlay/open
1255
+ POST /onewhack/overlay/close
1256
+ ```
1257
+
1258
+ ```ts
1259
+ type OneWhackOverlay = {
1260
+ overlayId: string;
1261
+ appId: string;
1262
+ kind: "form" | "menu" | "notice";
1263
+ title?: string;
1264
+ anchorId?: string;
1265
+ range?: OneWhackTextRange;
1266
+ fields?: OneWhackOverlayField[];
1267
+ actions?: OneWhackOverlayAction[];
1268
+ openedAt?: string;
1269
+ };
1270
+
1271
+ type OneWhackOverlayField = {
1272
+ fieldId: string;
1273
+ kind: "text" | "textarea" | "hidden";
1274
+ label?: string;
1275
+ value?: string;
1276
+ };
1277
+
1278
+ type OneWhackOverlayAction = {
1279
+ label: string;
1280
+ type: string;
1281
+ payload?: unknown;
1282
+ };
1283
+ ```
1284
+
1285
+ ### Composer Context and Drafts
1286
+
1287
+ Composer context items are app-contributed chips or context blocks owned by the
1288
+ host. Apps add/remove items and ask the host to serialize them into a draft; the
1289
+ adapter owns Codex composer DOM details.
1290
+
1291
+ ```ts
1292
+ type OneWhackComposerState = {
1293
+ contextItems: OneWhackComposerContextItem[];
1294
+ draft: {
1295
+ text: string;
1296
+ mode: "replace" | "append" | "prepend";
1297
+ contextItemIds: string[];
1298
+ updatedAt?: string;
1299
+ };
1300
+ lastSubmittedAt?: string;
1301
+ };
1302
+
1303
+ type OneWhackComposerContextItem = {
1304
+ itemId: string;
1305
+ appId: string;
1306
+ label: string;
1307
+ body: string;
1308
+ anchorId?: string;
1309
+ range?: OneWhackTextRange;
1310
+ status: "queued" | "sent" | "resolved";
1311
+ createdAt?: string;
1312
+ updatedAt?: string;
1313
+ };
1314
+ ```
1315
+
1316
+ ```text
1317
+ POST /onewhack/composer/context
1318
+ POST /onewhack/composer/context/remove
1319
+ POST /onewhack/composer/draft
1320
+ POST /onewhack/composer/submit
1321
+ ```
1322
+
1323
+ `/composer/draft` accepts `{ text, mode, contextItemIds }`. `/composer/submit`
1324
+ is optional and must fail gracefully when the adapter cannot safely submit.
1325
+
1326
+ ## Renderer Events
1327
+
1328
+ The injected bridge cannot rely on `fetch()` from `app://` to loopback because
1329
+ renderer-to-localhost fetch may fail in this integration path. Therefore the
1330
+ bridge must be able to communicate through CDP-polled renderer state.
1331
+
1332
+ The bridge records events in renderer globals:
1333
+
1334
+ ```ts
1335
+ type RendererEventBuffer = {
1336
+ selection?: OneWhackSelection;
1337
+ panelOpen?: PanelOpenResult & { at: string };
1338
+ hostEvents?: OneWhackRendererHostEvent[];
1339
+ };
1340
+ ```
1341
+
1342
+ The CDP adapter polls `snapshot()` and accepts only newer renderer selections.
1343
+ For host-rendered UI such as selection menus, overlays, and composer context
1344
+ chips, the renderer queues typed host events. The CDP adapter drains those
1345
+ events and invokes the same host APIs that app panels use.
1346
+
1347
+ ```ts
1348
+ type OneWhackRendererHostEvent =
1349
+ | {
1350
+ kind: "selection";
1351
+ selection: OneWhackSelection;
1352
+ }
1353
+ | {
1354
+ kind: "appAction";
1355
+ appId: string;
1356
+ type: string;
1357
+ payload?: unknown;
1358
+ requestedAt?: string;
1359
+ }
1360
+ | {
1361
+ kind: "overlayClose";
1362
+ overlayId?: string;
1363
+ }
1364
+ | {
1365
+ kind: "composerContextRemove";
1366
+ itemId: string;
1367
+ }
1368
+ | {
1369
+ kind: "highlightRange";
1370
+ range: OneWhackTextRange;
1371
+ }
1372
+ | {
1373
+ kind: "highlightAnchor";
1374
+ anchorId: string;
1375
+ };
1376
+ ```
1377
+
1378
+ If renderer-to-loopback networking is available, the bridge may also call the
1379
+ host HTTP endpoints directly as an optimization.
1380
+
1381
+ ## App State
1382
+
1383
+ OneWhack does not prescribe an app state provider. App state can come from
1384
+ app-server, a local audit system, an MCP server, a test runner, a diagnostics
1385
+ collector, a bookmark store, an editor-navigation model, or simple DOM
1386
+ inference.
1387
+
1388
+ ```ts
1389
+ type OneWhackAppState = {
1390
+ appId: string;
1391
+ status: OneWhackAppStatus;
1392
+ source: string;
1393
+ connected: boolean;
1394
+ entries: OneWhackAppEntry[];
1395
+ annotations?: AnchorAnnotation[];
1396
+ commands?: OneWhackCommand[];
1397
+ selectionActions?: OneWhackSelectionAction[];
1398
+ updatedAt?: string;
1399
+ };
1400
+
1401
+ type OneWhackAppEntry = {
1402
+ entryId: string;
1403
+ appId: string;
1404
+ anchorId?: string;
1405
+ markerId?: string;
1406
+ correlationId?: string;
1407
+ title: string;
1408
+ summary?: string;
1409
+ category?: string;
1410
+ tone?: AnchorAnnotationMarker["tone"];
1411
+ status?: string;
1412
+ target?: {
1413
+ path?: string;
1414
+ url?: string;
1415
+ label?: string;
1416
+ };
1417
+ detail?: unknown;
1418
+ occurredAt?: string;
1419
+ };
1420
+
1421
+ type OneWhackCommand = {
1422
+ commandId: string;
1423
+ appId: string;
1424
+ label: string;
1425
+ type: string;
1426
+ shortcut?: string;
1427
+ enabled?: boolean;
1428
+ payload?: unknown;
1429
+ };
1430
+ ```
1431
+
1432
+ Panel UIs should render `OneWhackAppEntry[]` without knowing the provider. A
1433
+ timeline, review list, trace viewer, bookmark list, or diagnostics stream can
1434
+ all be represented as entries bound optionally to transcript anchors.
1435
+
1436
+ ## App-Server Adapter
1437
+
1438
+ The app-server adapter is separate from Desktop control. OneWhack hosts are
1439
+ app-server-aware and report app-server status even when they cannot connect.
1440
+
1441
+ Useful app-server methods include:
1442
+
1443
+ - `thread/start`
1444
+ - `thread/resume`
1445
+ - `turn/start`
1446
+ - `turn/steer`
1447
+ - `turn/interrupt`
1448
+ - `thread/read`
1449
+ - `thread/turns/items/list`
1450
+ - approval request/response methods
1451
+ - `thread/rollback`
1452
+
1453
+ OneWhack should expose app-server data as correlation metadata, not as a replacement
1454
+ transport.
1455
+
1456
+ ```ts
1457
+ type AppServerStatus = {
1458
+ status: "disabled" | "starting" | "connected" | "error" | "exited";
1459
+ pid?: number;
1460
+ version?: string;
1461
+ error?: string;
1462
+ };
1463
+ ```
1464
+
1465
+ If an app sends approval responses through app-server, that path must be
1466
+ explicit and user-driven. OneWhack must not auto-approve or auto-deny actions.
1467
+
1468
+ ## Minimal Endpoint Set
1469
+
1470
+ A OneWhack host must implement these endpoints:
1471
+
1472
+ ```text
1473
+ GET /onewhack/manifest
1474
+ GET /onewhack/state
1475
+ GET /onewhack/events
1476
+ GET /onewhack/apps
1477
+ GET /onewhack/apps/:appId/manifest
1478
+ GET /onewhack/apps/:appId/state
1479
+ GET /onewhack/apps/:appId/actions/:actionId
1480
+ POST /onewhack/apps/:appId/actions
1481
+ POST /onewhack/panel/open
1482
+ POST /onewhack/transcript/scroll
1483
+ POST /onewhack/transcript/highlight
1484
+ POST /onewhack/selection
1485
+ ```
1486
+
1487
+ ## Manifest
1488
+
1489
+ OneWhack hosts and apps describe themselves with manifests. Manifests are
1490
+ normative for the host launcher, app registry, and panel UI; Codex Desktop does
1491
+ not need to understand them. A packaged app should keep its manifest on disk,
1492
+ and a running host must expose resolved manifests through the host API.
1493
+
1494
+ ```json
1495
+ {
1496
+ "oneWhackVersion": "0.1",
1497
+ "hostId": "onewill.onewhack.local",
1498
+ "hostName": "OneWhack Host",
1499
+ "security": {
1500
+ "auth": "bearer",
1501
+ "localDevInsecure": false
1502
+ },
1503
+ "capabilities": {
1504
+ "host": {
1505
+ "apps": true,
1506
+ "actions": true,
1507
+ "appState": true,
1508
+ "sidePanel": true
1509
+ },
1510
+ "transcript": {
1511
+ "read": true,
1512
+ "annotate": true,
1513
+ "navigate": true,
1514
+ "select": true
1515
+ },
1516
+ "adapter": {
1517
+ "name": "codex-desktop-cdp",
1518
+ "cdp": true,
1519
+ "rendererInjection": true,
1520
+ "providers": {
1521
+ "transcriptSnapshot": "onewill.vim-nav",
1522
+ "transcriptOrder": "onewill.vim-nav",
1523
+ "transcriptNavigation": "onewill.vim-nav",
1524
+ "transcriptHighlight": "onewill.vim-nav"
1525
+ }
1526
+ },
1527
+ "appServer": {
1528
+ "available": true,
1529
+ "correlateItems": true,
1530
+ "approvals": false,
1531
+ "rollback": false
1532
+ }
1533
+ },
1534
+ "apps": [
1535
+ {
1536
+ "oneWhackVersion": "0.1",
1537
+ "appId": "example.app",
1538
+ "version": "0.1.0",
1539
+ "name": "Example App",
1540
+ "entry": "http://127.0.0.1:0/apps/example/",
1541
+ "distribution": {
1542
+ "kind": "local",
1543
+ "source": ".",
1544
+ "integrity": "dev-local"
1545
+ },
1546
+ "entrypoint": {
1547
+ "kind": "module",
1548
+ "module": "./src/example-app.js",
1549
+ "factory": "createExampleApp",
1550
+ "publicDir": "./public"
1551
+ },
1552
+ "capabilities": {
1553
+ "panel": true,
1554
+ "annotations": true,
1555
+ "commands": true,
1556
+ "actions": true,
1557
+ "appState": true,
1558
+ "rendererBridge": false
1559
+ },
1560
+ "contributes": {
1561
+ "panel": true,
1562
+ "annotations": true,
1563
+ "commands": true,
1564
+ "actions": true,
1565
+ "appState": true
1566
+ },
1567
+ "permissions": {
1568
+ "transcriptRead": true,
1569
+ "transcriptAnnotate": true,
1570
+ "transcriptNavigate": true,
1571
+ "appServerRead": true,
1572
+ "appServerApprove": false,
1573
+ "appServerRollback": false
1574
+ },
1575
+ "panel": {
1576
+ "title": "Example",
1577
+ "reloadPolicy": "preserve",
1578
+ "preferredWidth": 420
1579
+ }
1580
+ }
1581
+ ]
1582
+ }
1583
+ ```
1584
+
1585
+ ## Public Example: Vim Navigator App
1586
+
1587
+ The public reference app for OneWhack `0.1` is a Vim-style transcript navigator.
1588
+ Other apps should use the same host/app contract and document any app-specific
1589
+ state or action vocabulary they expose.
1590
+
1591
+ ### App Manifest
1592
+
1593
+ ```json
1594
+ {
1595
+ "oneWhackVersion": "0.1",
1596
+ "appId": "onewill.vim-nav",
1597
+ "version": "0.1.0",
1598
+ "name": "OneWill VimNav",
1599
+ "entry": "http://127.0.0.1:49152/apps/onewill.vim-nav/",
1600
+ "distribution": {
1601
+ "kind": "local",
1602
+ "source": ".",
1603
+ "integrity": "dev-local"
1604
+ },
1605
+ "entrypoint": {
1606
+ "kind": "module",
1607
+ "module": "./src/vim-nav-app.js",
1608
+ "factory": "createVimNavigatorApp",
1609
+ "publicDir": "./public"
1610
+ },
1611
+ "capabilities": {
1612
+ "panel": true,
1613
+ "annotations": true,
1614
+ "commands": true,
1615
+ "actions": true,
1616
+ "appState": true,
1617
+ "rendererBridge": false
1618
+ },
1619
+ "contributes": {
1620
+ "panel": true,
1621
+ "annotations": true,
1622
+ "commands": true,
1623
+ "actions": true,
1624
+ "appState": true
1625
+ },
1626
+ "permissions": {
1627
+ "transcriptRead": true,
1628
+ "transcriptAnnotate": true,
1629
+ "transcriptNavigate": true,
1630
+ "appServerRead": false,
1631
+ "appServerApprove": false,
1632
+ "appServerRollback": false
1633
+ },
1634
+ "panel": {
1635
+ "title": "Vim Nav",
1636
+ "reloadPolicy": "preserve",
1637
+ "preferredWidth": 360
1638
+ }
1639
+ }
1640
+ ```
1641
+
1642
+ ### App State
1643
+
1644
+ ```json
1645
+ {
1646
+ "ok": true,
1647
+ "data": {
1648
+ "appId": "onewill.vim-nav",
1649
+ "status": "ready",
1650
+ "source": "dom",
1651
+ "connected": true,
1652
+ "entries": [
1653
+ {
1654
+ "entryId": "nav:anchor-2",
1655
+ "appId": "onewill.vim-nav",
1656
+ "anchorId": "anchor-2",
1657
+ "title": "02 assistant",
1658
+ "summary": "Latest assistant response",
1659
+ "category": "navigation",
1660
+ "status": "visible"
1661
+ }
1662
+ ],
1663
+ "annotations": [
1664
+ {
1665
+ "appId": "onewill.vim-nav",
1666
+ "anchorId": "anchor-2",
1667
+ "placement": "leading-rail",
1668
+ "priority": 50,
1669
+ "markers": [
1670
+ {
1671
+ "markerId": "ruler-2",
1672
+ "glyph": "02",
1673
+ "label": "Jump to transcript item 2",
1674
+ "tone": "accent",
1675
+ "shape": "circle",
1676
+ "entryId": "nav:anchor-2",
1677
+ "action": {
1678
+ "type": "vim.jump",
1679
+ "payload": {
1680
+ "anchorId": "anchor-2"
1681
+ }
1682
+ }
1683
+ }
1684
+ ]
1685
+ }
1686
+ ],
1687
+ "commands": [
1688
+ {
1689
+ "commandId": "vim.next",
1690
+ "appId": "onewill.vim-nav",
1691
+ "label": "Next transcript item",
1692
+ "type": "vim.jumpRelative",
1693
+ "shortcut": "j",
1694
+ "enabled": true,
1695
+ "payload": {
1696
+ "delta": 1
1697
+ }
1698
+ }
1699
+ ],
1700
+ "updatedAt": "2026-06-17T19:21:00.000Z"
1701
+ }
1702
+ }
1703
+ ```
1704
+
1705
+ ### Action Request
1706
+
1707
+ ```json
1708
+ {
1709
+ "action": {
1710
+ "actionId": "act_001",
1711
+ "appId": "onewill.vim-nav",
1712
+ "type": "vim.jump",
1713
+ "anchorId": "anchor-2",
1714
+ "entryId": "nav:anchor-2",
1715
+ "payload": {
1716
+ "behavior": "smooth",
1717
+ "block": "center"
1718
+ },
1719
+ "requestedAt": "2026-06-17T19:21:01.000Z"
1720
+ }
1721
+ }
1722
+ ```
1723
+
1724
+ ### Action Result
1725
+
1726
+ ```json
1727
+ {
1728
+ "ok": true,
1729
+ "data": {
1730
+ "actionId": "act_001",
1731
+ "appId": "onewill.vim-nav",
1732
+ "ok": true,
1733
+ "status": "applied",
1734
+ "completedAt": "2026-06-17T19:21:01.050Z"
1735
+ }
1736
+ }
1737
+ ```
1738
+
1739
+ ## Security And Lifecycle
1740
+
1741
+ OneWhack is local integration glue, not a sandbox or security boundary. The
1742
+ host still needs explicit safety rules because it can reach CDP, app-server,
1743
+ and user-visible UI.
1744
+
1745
+ ### Loopback And Session Access
1746
+
1747
+ - Bind host HTTP servers to loopback only.
1748
+ - Prefer ephemeral ports.
1749
+ - Use an unguessable session token for panel requests once multiple apps or
1750
+ long-lived hosts are supported.
1751
+ - Do not expose the host server on external interfaces.
1752
+
1753
+ ### App Permissions
1754
+
1755
+ App manifests declare requested permissions. The host must enforce them.
1756
+
1757
+ ```ts
1758
+ type OneWhackAppPermissions = {
1759
+ transcriptRead?: boolean;
1760
+ transcriptAnnotate?: boolean;
1761
+ transcriptNavigate?: boolean;
1762
+ appServerRead?: boolean;
1763
+ appServerApprove?: boolean;
1764
+ appServerRollback?: boolean;
1765
+ };
1766
+ ```
1767
+
1768
+ For OneWhack `0.1`, an explicit `false` means the host must deny that
1769
+ capability. Omitted permissions are interpreted by host policy, but reference
1770
+ hosts should treat omitted transcript permissions as allowed for local
1771
+ development apps. A host that denies `transcriptRead` must provide an empty
1772
+ transcript view to that app. A host that denies `transcriptAnnotate` must ignore
1773
+ or remove that app's annotations. A host that denies `transcriptNavigate` must
1774
+ return `403` for action paths that try to scroll or highlight transcript
1775
+ anchors through host context helpers.
1776
+
1777
+ No app receives approval, rollback, filesystem, shell, or network authority just
1778
+ because it is mounted. Approval and rollback actions must be explicit,
1779
+ user-driven, and visible in app state.
1780
+
1781
+ ### Desktop Lifecycle
1782
+
1783
+ - Do not patch the Codex app bundle.
1784
+ - Prefer launching through the host so CDP is enabled at process startup.
1785
+ - Attach to an already-running Desktop session only when CDP is already
1786
+ available.
1787
+ - Use a test or OneWhack profile when launching Desktop unless the user
1788
+ explicitly requests their normal profile.
1789
+ - Clean up host-launched Codex and Node processes on exit.
1790
+
1791
+ ### UI Lifecycle
1792
+
1793
+ - Do not reload an already-open side panel as part of open/focus.
1794
+ - Do not replace panel DOM while the user has active text selection.
1795
+ - Do not remove annotations owned by another app.
1796
+ - Treat DOM selectors as adapter configuration, not protocol fields.
1797
+
1798
+ ## Appendix: Non-Normative Reference Mapping
1799
+
1800
+ The public Vim Navigator example maps implementation files to OneWhack terms as
1801
+ follows:
1802
+
1803
+ | Vim Navigator implementation | OneWhack term |
1804
+ | --- | --- |
1805
+ | `onewhack/host/src/index.js` | Reusable OneWhack Host |
1806
+ | `onewhack/apps/vim-nav/server.mjs` | Host launcher registering one app |
1807
+ | `onewhack/apps/vim-nav/src/vim-nav-app.js` | Vim Navigator app manifest, state, and actions |
1808
+ | `onewhack/apps/vim-nav/public/index.html` | Hosted app panel |
1809
+ | `onewhack/apps/vim-nav/public/app.js` | App client |
1810
+ | `onewhack/apps/vim-nav/public/styles.css` | App panel styling |
1811
+ | `onewhack/host/src/codex-desktop-renderer-bridge.js` | Host-owned injected renderer bridge |
1812
+ | CDP `Runtime.evaluate` | Desktop CDP Adapter call, not used by mock tests |
1813
+ | `window.__oneWhackRuntime.getBridge("onewill.vim-nav").snapshot()` | `RendererBridge.snapshot()` |
1814
+ | `window.__oneWhackRuntime.getBridge("onewill.vim-nav").openPanel()` | `RendererBridge.openPanel()` |
1815
+ | `window.__oneWhackRuntime.getBridge("onewill.vim-nav").scrollToAnchor(anchorId)` | `scrollToAnchor(anchorId)` |
1816
+ | `window.__oneWhackRuntime.getBridge("onewill.vim-nav").highlightAnchor(anchorId)` | `highlightAnchor(anchorId)` |
1817
+ | numbered transcript ruler pills | `AnchorAnnotation.markers` |
1818
+ | `/onewhack/apps/onewill.vim-nav/state` | App state endpoint |
1819
+ | `/onewhack/apps/onewill.vim-nav/actions` | App action endpoint |
1820
+ | `onewhack/apps/vim-nav/test-onewhack.mjs` | Fast host/app behavior e2e |
1821
+
1822
+ ## Operational Rules
1823
+
1824
+ 1. A host may mount multiple apps, but only one renderer bridge should own
1825
+ transcript discovery and annotation placement.
1826
+ 2. Apps must go through the host for transcript navigation, annotation updates,
1827
+ action execution, and app-server correlation.
1828
+ 3. Treat exact app-server item mapping as optional unless proven.
1829
+ 4. Preserve user selection and expanded panel state during host event updates.
1830
+ 5. Prefer degraded, read-only behavior when CDP, app-server, or requested app
1831
+ permissions are unavailable.
1832
+
1833
+ ## Behavior Profiles
1834
+
1835
+ OneWhack `0.1` describes behavior in four profiles. Implementations should state
1836
+ which profiles they support and cover those behaviors with e2e tests.
1837
+
1838
+ ### Core Host
1839
+
1840
+ A Core Host must implement:
1841
+
1842
+ - JSON envelope and error model.
1843
+ - Session token behavior or explicit `localDevInsecure`.
1844
+ - `GET /onewhack/manifest`.
1845
+ - `GET /onewhack/state`.
1846
+ - `GET /onewhack/events`.
1847
+ - `GET /onewhack/apps`.
1848
+ - `GET /onewhack/apps/:appId/manifest`.
1849
+ - `GET /onewhack/apps/:appId/state`.
1850
+ - `GET /onewhack/apps/:appId/actions/:actionId`.
1851
+ - `POST /onewhack/apps/:appId/actions`.
1852
+ - App lifecycle states and permission enforcement.
1853
+
1854
+ ### Transcript Host
1855
+
1856
+ A Transcript Host is a Core Host that also implements:
1857
+
1858
+ - Transcript anchors in `OneWhackPublicState`.
1859
+ - `POST /onewhack/panel/open`.
1860
+ - `POST /onewhack/transcript/scroll`.
1861
+ - `POST /onewhack/transcript/highlight`.
1862
+ - `POST /onewhack/selection`.
1863
+ - Annotation ownership and merge rules.
1864
+
1865
+ ### Codex Desktop Adapter
1866
+
1867
+ A Codex Desktop Adapter is a Transcript Host implementation that realizes the
1868
+ transcript surface through Codex Desktop CDP and renderer injection. It must
1869
+ follow the lifecycle rules for CDP launch/attach, idempotent injection, and
1870
+ process cleanup.
1871
+
1872
+ ### App Client
1873
+
1874
+ An App Client must:
1875
+
1876
+ - Read `GET /onewhack/manifest` and verify required capabilities.
1877
+ - Use `GET /onewhack/apps/:appId/state` or `GET /onewhack/state` as its source
1878
+ of truth.
1879
+ - Use `POST /onewhack/apps/:appId/actions` for app commands.
1880
+ - Use `GET /onewhack/apps/:appId/actions/:actionId` to recover action results
1881
+ when needed.
1882
+ - Treat `OneWhackError` as the only failure contract.
1883
+ - Degrade cleanly when capabilities or permissions are unavailable.
1884
+
1885
+ ## Required E2E Coverage
1886
+
1887
+ A OneWhack `0.1` implementation should include fast e2e coverage for these
1888
+ observable behaviors:
1889
+
1890
+ 1. `GET /onewhack/manifest` returns `ok: true`, `oneWhackVersion: "0.1"`,
1891
+ host capabilities, security posture, and app manifests.
1892
+ 2. `GET /onewhack/state` returns `ok: true` with `protocolName: "onewhack"`,
1893
+ `protocolVersion: "0.1"`, numeric `sequence`, ISO `generatedAt`, host
1894
+ status, transcript state, app states, and app-server status.
1895
+ 3. `GET /onewhack/events` emits an initial `state` event, monotonically
1896
+ increasing event ids, and `heartbeat` events.
1897
+ 4. Unknown apps return `404` with `OneWhackError.error.code: "not_found"`.
1898
+ 5. Missing or invalid bearer tokens return `401` unless `localDevInsecure` is
1899
+ explicitly enabled.
1900
+ 6. Duplicate `(appId, actionId)` submissions return the same receipt or result
1901
+ without executing twice.
1902
+ 7. Async action completion is observable through both SSE `action` events and
1903
+ `GET /onewhack/apps/:appId/actions/:actionId`.
1904
+ 8. Stale selections return the current accepted selection with `stale: true`
1905
+ and do not change host `selection`.
1906
+ 9. Annotation merge order is stable by `priority`, then `appId`, then
1907
+ `markerId`.
1908
+ 10. Disabling or unmounting an app removes only that app's annotations.
1909
+ 11. `POST /onewhack/transcript/scroll` and
1910
+ `POST /onewhack/transcript/highlight` return `404` for unknown anchors and do
1911
+ not mutate selection.
1912
+ 12. Opening an already-loaded panel does not reload it when
1913
+ `reloadPolicy: "preserve"`.
1914
+
1915
+ ## Versioning
1916
+
1917
+ OneWhack version `0.1` is the minimal hack-path protocol:
1918
+
1919
+ - local host state/event endpoints
1920
+ - host/app manifest negotiation
1921
+ - app registry and app-scoped state
1922
+ - app action routing
1923
+ - CDP renderer bridge
1924
+ - transcript anchor discovery
1925
+ - transcript annotation
1926
+ - panel open/focus
1927
+ - selection synchronization
1928
+ - app-server correlation
1929
+
1930
+ Future versions may add alternate adapters, but they should preserve the same
1931
+ host-facing concepts. Minor versions should add optional capabilities or fields
1932
+ behind capability checks. Major versions may change required fields or endpoint
1933
+ semantics, but a host must make that visible through `protocolVersion`,
1934
+ `oneWhackVersion`, and `capabilities.protocolVersion` before app actions are
1935
+ required.