@gajae-code/coding-agent 0.3.0 → 0.3.1

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 (175) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/types/async/job-manager.d.ts +7 -0
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/config/keybindings.d.ts +5 -0
  6. package/dist/types/config/settings-schema.d.ts +4 -4
  7. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  8. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  9. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  10. package/dist/types/eval/py/executor.d.ts +2 -0
  11. package/dist/types/eval/py/kernel.d.ts +2 -0
  12. package/dist/types/exec/bash-executor.d.ts +10 -0
  13. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  14. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  16. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  17. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  18. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  20. package/dist/types/hooks/skill-state.d.ts +21 -0
  21. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  22. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  23. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  24. package/dist/types/internal-urls/types.d.ts +4 -0
  25. package/dist/types/lsp/index.d.ts +10 -10
  26. package/dist/types/modes/bridge/auth.d.ts +12 -0
  27. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  28. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  29. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  30. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  32. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  33. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  34. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  35. package/dist/types/modes/components/status-line.d.ts +2 -0
  36. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  37. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  38. package/dist/types/modes/index.d.ts +1 -0
  39. package/dist/types/modes/interactive-mode.d.ts +1 -0
  40. package/dist/types/modes/jobs-observer.d.ts +57 -0
  41. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  42. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  43. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  44. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  45. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  46. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  47. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  48. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  49. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  50. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  51. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  52. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  53. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  54. package/dist/types/modes/types.d.ts +1 -0
  55. package/dist/types/sdk.d.ts +2 -0
  56. package/dist/types/session/agent-session.d.ts +11 -1
  57. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  58. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  59. package/dist/types/task/id.d.ts +7 -0
  60. package/dist/types/task/index.d.ts +5 -0
  61. package/dist/types/task/receipt.d.ts +85 -0
  62. package/dist/types/task/spawn-gate.d.ts +38 -0
  63. package/dist/types/task/types.d.ts +143 -11
  64. package/dist/types/tools/cron.d.ts +6 -0
  65. package/dist/types/tools/index.d.ts +2 -0
  66. package/dist/types/tools/path-utils.d.ts +1 -0
  67. package/dist/types/tools/subagent.d.ts +15 -0
  68. package/package.json +7 -7
  69. package/scripts/build-binary.ts +7 -0
  70. package/src/async/job-manager.ts +36 -0
  71. package/src/cli/args.ts +9 -2
  72. package/src/commands/deep-interview.ts +1 -0
  73. package/src/commands/harness.ts +289 -19
  74. package/src/commands/launch.ts +2 -2
  75. package/src/commands/state.ts +2 -1
  76. package/src/commands/team.ts +22 -4
  77. package/src/config/keybindings.ts +6 -0
  78. package/src/config/settings-schema.ts +6 -3
  79. package/src/dap/client.ts +17 -3
  80. package/src/debug/crash-diagnostics.ts +223 -0
  81. package/src/debug/runtime-gauges.ts +20 -0
  82. package/src/deep-interview/render-middleware.ts +6 -0
  83. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  84. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  85. package/src/defaults/gjc/skills/ultragoal/SKILL.md +28 -2
  86. package/src/eval/py/executor.ts +21 -1
  87. package/src/eval/py/kernel.ts +15 -0
  88. package/src/exec/bash-executor.ts +41 -0
  89. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  90. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  91. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  92. package/src/gjc-runtime/state-migrations.ts +54 -7
  93. package/src/gjc-runtime/state-runtime.ts +461 -64
  94. package/src/gjc-runtime/state-schema.ts +192 -0
  95. package/src/gjc-runtime/state-writer.ts +32 -1
  96. package/src/gjc-runtime/team-runtime.ts +177 -105
  97. package/src/gjc-runtime/ultragoal-runtime.ts +114 -26
  98. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  99. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  100. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  101. package/src/harness-control-plane/control-endpoint.ts +19 -8
  102. package/src/harness-control-plane/owner.ts +57 -10
  103. package/src/harness-control-plane/state-machine.ts +2 -1
  104. package/src/hooks/skill-state.ts +176 -26
  105. package/src/internal-urls/agent-protocol.ts +68 -21
  106. package/src/internal-urls/artifact-protocol.ts +12 -17
  107. package/src/internal-urls/docs-index.generated.ts +3 -2
  108. package/src/internal-urls/registry-helpers.ts +19 -16
  109. package/src/internal-urls/types.ts +4 -0
  110. package/src/lsp/client.ts +18 -2
  111. package/src/main.ts +21 -5
  112. package/src/modes/bridge/auth.ts +41 -0
  113. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  114. package/src/modes/bridge/bridge-mode.ts +520 -0
  115. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  116. package/src/modes/bridge/event-stream.ts +70 -0
  117. package/src/modes/components/custom-editor.ts +101 -0
  118. package/src/modes/components/hook-selector.ts +61 -18
  119. package/src/modes/components/jobs-overlay-model.ts +109 -0
  120. package/src/modes/components/jobs-overlay.ts +172 -0
  121. package/src/modes/components/status-line/presets.ts +7 -5
  122. package/src/modes/components/status-line/segments.ts +25 -0
  123. package/src/modes/components/status-line/types.ts +2 -0
  124. package/src/modes/components/status-line.ts +9 -1
  125. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  126. package/src/modes/controllers/input-controller.ts +97 -9
  127. package/src/modes/controllers/selector-controller.ts +29 -0
  128. package/src/modes/index.ts +1 -0
  129. package/src/modes/interactive-mode.ts +27 -0
  130. package/src/modes/jobs-observer.ts +204 -0
  131. package/src/modes/rpc/host-tools.ts +1 -186
  132. package/src/modes/rpc/host-uris.ts +1 -235
  133. package/src/modes/rpc/rpc-client.ts +25 -10
  134. package/src/modes/rpc/rpc-mode.ts +12 -381
  135. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  136. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  137. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  138. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  139. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  140. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  141. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  142. package/src/modes/shared/agent-wire/responses.ts +17 -0
  143. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  144. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  145. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  146. package/src/modes/types.ts +1 -0
  147. package/src/prompts/tools/subagent.md +12 -7
  148. package/src/prompts/tools/task-summary.md +3 -9
  149. package/src/prompts/tools/task.md +5 -1
  150. package/src/sdk.ts +4 -0
  151. package/src/session/agent-session.ts +214 -38
  152. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  153. package/src/skill-state/workflow-state-contract.ts +7 -4
  154. package/src/skill-state/workflow-state-version.ts +3 -0
  155. package/src/slash-commands/builtin-registry.ts +8 -0
  156. package/src/task/executor.ts +29 -5
  157. package/src/task/id.ts +33 -0
  158. package/src/task/index.ts +257 -67
  159. package/src/task/output-manager.ts +5 -4
  160. package/src/task/receipt.ts +297 -0
  161. package/src/task/render.ts +48 -131
  162. package/src/task/spawn-gate.ts +132 -0
  163. package/src/task/types.ts +48 -7
  164. package/src/tools/ask.ts +73 -33
  165. package/src/tools/ast-edit.ts +1 -0
  166. package/src/tools/ast-grep.ts +1 -0
  167. package/src/tools/bash.ts +1 -1
  168. package/src/tools/cron.ts +48 -0
  169. package/src/tools/find.ts +4 -1
  170. package/src/tools/index.ts +2 -0
  171. package/src/tools/path-utils.ts +3 -2
  172. package/src/tools/read.ts +1 -0
  173. package/src/tools/search.ts +1 -0
  174. package/src/tools/skill.ts +6 -1
  175. package/src/tools/subagent.ts +237 -84
@@ -0,0 +1,520 @@
1
+ import type { ExtensionUIContext } from "../../extensibility/extensions";
2
+ import type { AgentSession } from "../../session/agent-session";
3
+ import type { ClientBridgePermissionOutcome } from "../../session/client-bridge";
4
+ import type { RpcCommand, RpcResponse } from "../rpc/rpc-types";
5
+ import { dispatchRpcCommand } from "../shared/agent-wire/command-dispatch";
6
+ import { isRpcCommand } from "../shared/agent-wire/command-validation";
7
+ import { BridgeFrameSequencer, toBridgeEventFrame } from "../shared/agent-wire/event-envelope";
8
+ import type { BridgeCapability } from "../shared/agent-wire/handshake";
9
+ import {
10
+ type BridgeHandshakeRequest,
11
+ isBridgeHandshakeRequest,
12
+ negotiateBridgeHandshake,
13
+ } from "../shared/agent-wire/handshake";
14
+ import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "../shared/agent-wire/host-tool-bridge";
15
+ import { isRpcHostUriResult, RpcHostUriBridge } from "../shared/agent-wire/host-uri-bridge";
16
+ import type { BridgeFrameType } from "../shared/agent-wire/protocol";
17
+ import {
18
+ BRIDGE_COMMAND_SCOPES,
19
+ type BridgeCommandScope,
20
+ isRpcCommandAllowed,
21
+ isRpcCommandType,
22
+ scopeForRpcCommand,
23
+ } from "../shared/agent-wire/scopes";
24
+ import { UiRequestBroker } from "../shared/agent-wire/ui-request-broker";
25
+ import type { BridgeUiResult } from "../shared/agent-wire/ui-result";
26
+ import { assertSafeBridgeBind, isBridgeTokenAuthorized } from "./auth";
27
+ import { type BridgePermissionRequestPayload, createBridgeClientBridge } from "./bridge-client-bridge";
28
+ import { BridgeExtensionUIContext, type BridgeUiRequestPayload } from "./bridge-ui-context";
29
+ import { BridgeEventStream } from "./event-stream";
30
+
31
+ const DEFAULT_BRIDGE_HOST = "127.0.0.1";
32
+ const DEFAULT_BRIDGE_PORT = 4077;
33
+
34
+ const SERVER_CAPABILITIES: readonly BridgeCapability[] = [
35
+ "events",
36
+ "prompt",
37
+ "permission",
38
+ "elicitation",
39
+ "ui.declarative",
40
+ "host_tools",
41
+ "host_uri",
42
+ ];
43
+
44
+ const DEFAULT_BRIDGE_SCOPES: readonly BridgeCommandScope[] = ["prompt"];
45
+ interface BridgeEndpointMatrix {
46
+ events: boolean;
47
+ commands: boolean;
48
+ control: boolean;
49
+ uiResponses: boolean;
50
+ hostToolResults: boolean;
51
+ hostUriResults: boolean;
52
+ }
53
+
54
+ const FAIL_CLOSED_BRIDGE_ENDPOINTS: BridgeEndpointMatrix = {
55
+ events: false,
56
+ commands: false,
57
+ control: false,
58
+ uiResponses: false,
59
+ hostToolResults: false,
60
+ hostUriResults: false,
61
+ };
62
+
63
+ const MAX_IDEMPOTENCY_RECORDS = 1_000;
64
+
65
+ const SERVER_FRAME_TYPES: readonly BridgeFrameType[] = [
66
+ "ready",
67
+ "event",
68
+ "response",
69
+ "ui_request",
70
+ "permission_request",
71
+ "host_tool_call",
72
+ "host_uri_request",
73
+ "reset",
74
+ "error",
75
+ ];
76
+
77
+ interface BridgeFetchHandlerOptions {
78
+ sessionId: string;
79
+ token: string;
80
+ eventStream?: BridgeEventStream;
81
+ commandDispatcher?: (command: RpcCommand) => Promise<RpcResponse>;
82
+ commandScopes?: readonly BridgeCommandScope[];
83
+ idempotencyCache?: BridgeIdempotencyCache;
84
+ permissionBroker?: UiRequestBroker<BridgePermissionRequestPayload, ClientBridgePermissionOutcome>;
85
+ uiBroker?: UiRequestBroker<BridgeUiRequestPayload, BridgeUiResult<unknown>>;
86
+ hostToolBridge?: RpcHostToolBridge;
87
+ hostUriBridge?: RpcHostUriBridge;
88
+ endpointMatrix?: Partial<BridgeEndpointMatrix>;
89
+ }
90
+
91
+ interface BridgeIdempotencyRecord {
92
+ route: string;
93
+ ownerToken?: string;
94
+ body: string | Promise<string>;
95
+ response: unknown | Promise<unknown>;
96
+ pending?: boolean;
97
+ }
98
+
99
+ type BridgeIdempotencyCache = Map<string, BridgeIdempotencyRecord>;
100
+
101
+ function idempotencyConflict(): Response {
102
+ return jsonResponse(409, { error: "idempotency_conflict" });
103
+ }
104
+
105
+ function cachedIdempotencyResponse(
106
+ cache: BridgeIdempotencyCache | undefined,
107
+ key: string | undefined,
108
+ record: Omit<BridgeIdempotencyRecord, "response">,
109
+ ): Response | Promise<Response> | undefined {
110
+ if (!key) return undefined;
111
+ const cached = cache?.get(key);
112
+ if (!cached) return undefined;
113
+ return Promise.resolve(cached.body).then(body => {
114
+ if (body !== record.body || cached.route !== record.route) return idempotencyConflict();
115
+ if (cached.ownerToken !== record.ownerToken)
116
+ return jsonResponse(403, { status: "rejected", code: "not_controller" });
117
+ return Promise.resolve(cached.response).then(response => jsonResponse(200, response));
118
+ });
119
+ }
120
+
121
+ function rememberIdempotencyResponse(
122
+ cache: BridgeIdempotencyCache | undefined,
123
+ key: string | undefined,
124
+ record: BridgeIdempotencyRecord,
125
+ ): void {
126
+ if (!key) return;
127
+ cache?.set(key, record);
128
+ if (cache && cache.size > MAX_IDEMPOTENCY_RECORDS) {
129
+ for (const [candidateKey, candidate] of cache) {
130
+ if (!candidate.pending) {
131
+ cache.delete(candidateKey);
132
+ break;
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ function jsonResponse(status: number, body: unknown): Response {
139
+ return new Response(JSON.stringify(body), {
140
+ status,
141
+ headers: { "Content-Type": "application/json" },
142
+ });
143
+ }
144
+
145
+ function parseBridgeScopes(value: string | undefined): readonly BridgeCommandScope[] {
146
+ if (!value?.trim()) return DEFAULT_BRIDGE_SCOPES;
147
+ const allowed = new Set(BRIDGE_COMMAND_SCOPES);
148
+ const scopes = new Set<BridgeCommandScope>(DEFAULT_BRIDGE_SCOPES);
149
+ for (const raw of value.split(",")) {
150
+ const scope = raw.trim();
151
+ if (!scope) continue;
152
+ if (!allowed.has(scope as BridgeCommandScope)) throw new Error(`Invalid GJC_BRIDGE_SCOPES entry: ${scope}`);
153
+ scopes.add(scope as BridgeCommandScope);
154
+ }
155
+ return [...scopes];
156
+ }
157
+
158
+ function hasScope(scopes: readonly BridgeCommandScope[] | undefined, scope: BridgeCommandScope): boolean {
159
+ return new Set(scopes ?? DEFAULT_BRIDGE_SCOPES).has(scope);
160
+ }
161
+ function bridgeEndpointMatrix(options: BridgeFetchHandlerOptions): BridgeEndpointMatrix {
162
+ return { ...FAIL_CLOSED_BRIDGE_ENDPOINTS, ...options.endpointMatrix };
163
+ }
164
+
165
+ function disabledEndpointResponse(endpoint: keyof BridgeEndpointMatrix): Response {
166
+ return jsonResponse(403, { error: "endpoint_disabled", endpoint });
167
+ }
168
+
169
+ function bridgeHelpResponse(matrix: BridgeEndpointMatrix): Response {
170
+ return jsonResponse(200, {
171
+ status: "experimental_gated",
172
+ message: "Bridge mode is experimental; session-control endpoints fail closed by default.",
173
+ endpoints: matrix,
174
+ });
175
+ }
176
+
177
+ function frameTypeForDispatchOutput(obj: RpcResponse | object): BridgeFrameType {
178
+ const type = typeof obj === "object" && obj !== null && "type" in obj ? (obj as { type?: unknown }).type : undefined;
179
+ if (type === "host_tool_call" || type === "host_tool_cancel") return "host_tool_call";
180
+ if (type === "host_uri_request" || type === "host_uri_cancel") return "host_uri_request";
181
+ if (type === "extension_ui_request") return "ui_request";
182
+ if (type === "response") return "response";
183
+ return "response";
184
+ }
185
+
186
+ export function createBridgeFetchHandler(options: BridgeFetchHandlerOptions): (request: Request) => Promise<Response> {
187
+ return async request => {
188
+ const endpointMatrix = bridgeEndpointMatrix(options);
189
+ const url = new URL(request.url);
190
+ if (request.method === "GET" && url.pathname === "/healthz") {
191
+ return jsonResponse(200, { status: "ok" });
192
+ }
193
+ if (request.method === "GET" && url.pathname === "/v1/help") {
194
+ return bridgeHelpResponse(endpointMatrix);
195
+ }
196
+
197
+ if (request.method === "GET" && url.pathname === `/v1/sessions/${options.sessionId}/events`) {
198
+ if (!isBridgeTokenAuthorized(request.headers.get("Authorization"), { token: options.token })) {
199
+ return jsonResponse(401, { error: "unauthorized" });
200
+ }
201
+ if (!endpointMatrix.events) return disabledEndpointResponse("events");
202
+ const lastSeqRaw = url.searchParams.get("last_seq");
203
+ if (lastSeqRaw !== null && !/^\d+$/.test(lastSeqRaw)) return jsonResponse(400, { error: "invalid_last_seq" });
204
+ const lastSeq = lastSeqRaw === null ? 0 : Number.parseInt(lastSeqRaw, 10);
205
+ return options.eventStream?.response(lastSeq) ?? jsonResponse(503, { error: "events_unavailable" });
206
+ }
207
+
208
+ if (!isBridgeTokenAuthorized(request.headers.get("Authorization"), { token: options.token })) {
209
+ return jsonResponse(401, { error: "unauthorized" });
210
+ }
211
+
212
+ if (request.method === "POST" && url.pathname === "/v1/handshake") {
213
+ let payload: BridgeHandshakeRequest;
214
+ try {
215
+ payload = (await request.json()) as BridgeHandshakeRequest;
216
+ } catch {
217
+ return jsonResponse(400, { error: "invalid_json" });
218
+ }
219
+ if (!isBridgeHandshakeRequest(payload)) {
220
+ return jsonResponse(400, { error: "invalid_request" });
221
+ }
222
+ return jsonResponse(
223
+ 200,
224
+ negotiateBridgeHandshake(payload, {
225
+ sessionId: options.sessionId,
226
+ capabilities: endpointMatrix.events ? SERVER_CAPABILITIES : [],
227
+ scopes: endpointMatrix.commands ? (options.commandScopes ?? DEFAULT_BRIDGE_SCOPES) : [],
228
+ endpoints: {
229
+ events: endpointMatrix.events ? `/v1/sessions/${options.sessionId}/events` : "",
230
+ commands: endpointMatrix.commands ? `/v1/sessions/${options.sessionId}/commands` : "",
231
+ uiResponses: endpointMatrix.uiResponses
232
+ ? `/v1/sessions/${options.sessionId}/ui-responses/{correlation_id}`
233
+ : "",
234
+ claimControl: endpointMatrix.control ? `/v1/sessions/${options.sessionId}/control:claim` : "",
235
+ disconnectControl: endpointMatrix.control
236
+ ? `/v1/sessions/${options.sessionId}/control:disconnect`
237
+ : "",
238
+ hostToolResults: endpointMatrix.hostToolResults
239
+ ? `/v1/sessions/${options.sessionId}/host-tool-results/{correlation_id}`
240
+ : "",
241
+ hostUriResults: endpointMatrix.hostUriResults
242
+ ? `/v1/sessions/${options.sessionId}/host-uri-results/{correlation_id}`
243
+ : "",
244
+ },
245
+ frameTypes: endpointMatrix.events ? SERVER_FRAME_TYPES : [],
246
+ }),
247
+ );
248
+ }
249
+
250
+ if (request.method === "POST" && url.pathname === `/v1/sessions/${options.sessionId}/commands`) {
251
+ if (!endpointMatrix.commands) return disabledEndpointResponse("commands");
252
+ if (!options.commandDispatcher) return jsonResponse(503, { error: "commands_unavailable" });
253
+ const idempotencyKey = request.headers.get("Idempotency-Key") ?? undefined;
254
+ const existingRecord = idempotencyKey ? options.idempotencyCache?.get(idempotencyKey) : undefined;
255
+ let body = "";
256
+ let payload: unknown;
257
+ let pendingResponse: PromiseWithResolvers<unknown> | undefined;
258
+ try {
259
+ if (existingRecord) {
260
+ body = await request.text();
261
+ const cached = cachedIdempotencyResponse(options.idempotencyCache, idempotencyKey, {
262
+ route: url.pathname,
263
+ body,
264
+ });
265
+ if (cached) return await cached;
266
+ } else {
267
+ const bodyPromise = request.text();
268
+ pendingResponse = Promise.withResolvers<unknown>();
269
+ void pendingResponse.promise.catch(() => undefined);
270
+ rememberIdempotencyResponse(options.idempotencyCache, idempotencyKey, {
271
+ route: url.pathname,
272
+ body: bodyPromise,
273
+ response: pendingResponse.promise,
274
+ pending: true,
275
+ });
276
+ body = await bodyPromise;
277
+ }
278
+ payload = JSON.parse(body) as unknown;
279
+ } catch {
280
+ options.idempotencyCache?.delete(idempotencyKey ?? "");
281
+ pendingResponse?.reject(new Error("invalid_json"));
282
+ return jsonResponse(400, { error: "invalid_json" });
283
+ }
284
+ const type =
285
+ typeof payload === "object" && payload !== null && "type" in payload
286
+ ? (payload as { type?: unknown }).type
287
+ : undefined;
288
+ if (!isRpcCommandType(type)) {
289
+ options.idempotencyCache?.delete(idempotencyKey ?? "");
290
+ pendingResponse?.reject(new Error("invalid_command"));
291
+ return jsonResponse(400, { error: "invalid_command" });
292
+ }
293
+ if (!isRpcCommand(payload)) {
294
+ options.idempotencyCache?.delete(idempotencyKey ?? "");
295
+ pendingResponse?.reject(new Error("invalid_command"));
296
+ return jsonResponse(400, { error: "invalid_command" });
297
+ }
298
+ const scopes = new Set(options.commandScopes ?? DEFAULT_BRIDGE_SCOPES);
299
+ if (!isRpcCommandAllowed(type, scopes)) {
300
+ options.idempotencyCache?.delete(idempotencyKey ?? "");
301
+ pendingResponse?.reject(new Error("scope_denied"));
302
+ return jsonResponse(403, { error: "scope_denied", scope: scopeForRpcCommand(type) });
303
+ }
304
+ try {
305
+ const response = await options.commandDispatcher(payload);
306
+ pendingResponse?.resolve(response);
307
+ const cachedRecord = idempotencyKey ? options.idempotencyCache?.get(idempotencyKey) : undefined;
308
+ if (cachedRecord) cachedRecord.pending = false;
309
+ return jsonResponse(200, response);
310
+ } catch (err) {
311
+ options.idempotencyCache?.delete(idempotencyKey ?? "");
312
+ pendingResponse?.reject(err);
313
+ throw err;
314
+ }
315
+ }
316
+ if (request.method === "POST" && url.pathname === `/v1/sessions/${options.sessionId}/control:claim`) {
317
+ if (!endpointMatrix.control) return disabledEndpointResponse("control");
318
+ if (!hasScope(options.commandScopes, "control"))
319
+ return jsonResponse(403, { error: "scope_denied", scope: "control" });
320
+ if (options.permissionBroker?.ownerToken || options.uiBroker?.ownerToken)
321
+ return jsonResponse(409, { error: "controller_busy" });
322
+ const ownerToken = request.headers.get("X-GJC-Bridge-Owner-Token") ?? crypto.randomUUID();
323
+ const permissionClaim = options.permissionBroker?.claimController(ownerToken);
324
+ const uiClaim = options.uiBroker?.claimController(ownerToken);
325
+ if (permissionClaim?.status === "busy" || uiClaim?.status === "busy")
326
+ return jsonResponse(409, { error: "controller_busy" });
327
+ return jsonResponse(200, { status: "claimed", ownerToken });
328
+ }
329
+ if (request.method === "POST" && url.pathname === `/v1/sessions/${options.sessionId}/control:disconnect`) {
330
+ if (!endpointMatrix.control) return disabledEndpointResponse("control");
331
+ if (!hasScope(options.commandScopes, "control"))
332
+ return jsonResponse(403, { error: "scope_denied", scope: "control" });
333
+ const ownerToken = request.headers.get("X-GJC-Bridge-Owner-Token") ?? "";
334
+ const permissionReleased = options.permissionBroker?.disconnectController(ownerToken) ?? true;
335
+ const uiReleased = options.uiBroker?.disconnectController(ownerToken) ?? true;
336
+ return permissionReleased && uiReleased
337
+ ? jsonResponse(200, { status: "released" })
338
+ : jsonResponse(403, { status: "rejected", code: "not_controller" });
339
+ }
340
+
341
+ const uiResponsePrefix = `/v1/sessions/${options.sessionId}/ui-responses/`;
342
+ if (request.method === "POST" && url.pathname.startsWith(uiResponsePrefix)) {
343
+ if (!endpointMatrix.uiResponses) return disabledEndpointResponse("uiResponses");
344
+ if (!hasScope(options.commandScopes, "control"))
345
+ return jsonResponse(403, { error: "scope_denied", scope: "control" });
346
+ const correlationId = decodeURIComponent(url.pathname.slice(uiResponsePrefix.length));
347
+ const ownerToken = request.headers.get("X-GJC-Bridge-Owner-Token") ?? "";
348
+ const idempotencyKey = request.headers.get("Idempotency-Key") ?? undefined;
349
+ let body = "";
350
+ let payload: unknown;
351
+ try {
352
+ body = await request.text();
353
+ const cached = cachedIdempotencyResponse(options.idempotencyCache, idempotencyKey, {
354
+ route: url.pathname,
355
+ ownerToken,
356
+ body,
357
+ });
358
+ if (cached) return await cached;
359
+ payload = JSON.parse(body) as unknown;
360
+ } catch {
361
+ return jsonResponse(400, { error: "invalid_json" });
362
+ }
363
+ const permissionResult = options.permissionBroker?.respond(
364
+ correlationId,
365
+ ownerToken,
366
+ payload as ClientBridgePermissionOutcome,
367
+ );
368
+ if (permissionResult?.status === "accepted") {
369
+ rememberIdempotencyResponse(options.idempotencyCache, idempotencyKey, {
370
+ route: url.pathname,
371
+ ownerToken,
372
+ body,
373
+ response: permissionResult,
374
+ });
375
+ return jsonResponse(200, permissionResult);
376
+ }
377
+ const uiResult = options.uiBroker?.respond(correlationId, ownerToken, payload as BridgeUiResult<unknown>);
378
+ if (uiResult?.status === "accepted") {
379
+ rememberIdempotencyResponse(options.idempotencyCache, idempotencyKey, {
380
+ route: url.pathname,
381
+ ownerToken,
382
+ body,
383
+ response: uiResult,
384
+ });
385
+ return jsonResponse(200, uiResult);
386
+ }
387
+ const rejection = uiResult ?? permissionResult ?? { status: "rejected", code: "unknown_request" };
388
+ return jsonResponse(
389
+ rejection.status === "rejected" && rejection.code === "not_controller" ? 403 : 409,
390
+ rejection,
391
+ );
392
+ }
393
+ const hostToolResultPrefix = `/v1/sessions/${options.sessionId}/host-tool-results/`;
394
+ if (request.method === "POST" && url.pathname.startsWith(hostToolResultPrefix)) {
395
+ if (!endpointMatrix.hostToolResults) return disabledEndpointResponse("hostToolResults");
396
+ if (!hasScope(options.commandScopes, "host_tools"))
397
+ return jsonResponse(403, { error: "scope_denied", scope: "host_tools" });
398
+ if (!options.hostToolBridge) return jsonResponse(503, { error: "host_tools_unavailable" });
399
+ const id = decodeURIComponent(url.pathname.slice(hostToolResultPrefix.length));
400
+ let payload: unknown;
401
+ try {
402
+ payload = await request.json();
403
+ } catch {
404
+ return jsonResponse(400, { error: "invalid_json" });
405
+ }
406
+ const frame = typeof payload === "object" && payload !== null ? { ...payload, id } : { id };
407
+ let handled = false;
408
+ if (isRpcHostToolUpdate(frame)) {
409
+ handled = options.hostToolBridge.handleUpdate(frame);
410
+ } else if (isRpcHostToolResult(frame)) {
411
+ handled = options.hostToolBridge.handleResult(frame);
412
+ } else {
413
+ return jsonResponse(400, { error: "invalid_host_tool_result" });
414
+ }
415
+ return handled ? jsonResponse(200, { status: "accepted" }) : jsonResponse(404, { error: "unknown_request" });
416
+ }
417
+
418
+ const hostUriResultPrefix = `/v1/sessions/${options.sessionId}/host-uri-results/`;
419
+ if (request.method === "POST" && url.pathname.startsWith(hostUriResultPrefix)) {
420
+ if (!endpointMatrix.hostUriResults) return disabledEndpointResponse("hostUriResults");
421
+ if (!hasScope(options.commandScopes, "host_uri"))
422
+ return jsonResponse(403, { error: "scope_denied", scope: "host_uri" });
423
+ if (!options.hostUriBridge) return jsonResponse(503, { error: "host_uri_unavailable" });
424
+ const id = decodeURIComponent(url.pathname.slice(hostUriResultPrefix.length));
425
+ let payload: unknown;
426
+ try {
427
+ payload = await request.json();
428
+ } catch {
429
+ return jsonResponse(400, { error: "invalid_json" });
430
+ }
431
+ const frame = typeof payload === "object" && payload !== null ? { ...payload, id } : { id };
432
+ if (!isRpcHostUriResult(frame)) return jsonResponse(400, { error: "invalid_host_uri_result" });
433
+ return options.hostUriBridge.handleResult(frame)
434
+ ? jsonResponse(200, { status: "accepted" })
435
+ : jsonResponse(404, { error: "unknown_request" });
436
+ }
437
+ return jsonResponse(404, { error: "not_found" });
438
+ };
439
+ }
440
+
441
+ export async function runBridgeMode(
442
+ session: AgentSession,
443
+ setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
444
+ ): Promise<never> {
445
+ const token = Bun.env.GJC_BRIDGE_TOKEN;
446
+ if (!token) {
447
+ throw new Error("GJC_BRIDGE_TOKEN is required for --mode bridge");
448
+ }
449
+ const hostname = Bun.env.GJC_BRIDGE_HOST ?? DEFAULT_BRIDGE_HOST;
450
+ const port = Bun.env.GJC_BRIDGE_PORT ? Number.parseInt(Bun.env.GJC_BRIDGE_PORT, 10) : DEFAULT_BRIDGE_PORT;
451
+ if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
452
+ throw new Error(`Invalid GJC_BRIDGE_PORT: ${Bun.env.GJC_BRIDGE_PORT}`);
453
+ }
454
+ const commandScopes = parseBridgeScopes(Bun.env.GJC_BRIDGE_SCOPES);
455
+
456
+ const certPath = Bun.env.GJC_BRIDGE_TLS_CERT;
457
+ const keyPath = Bun.env.GJC_BRIDGE_TLS_KEY;
458
+ const tlsConfigured = Boolean(certPath && keyPath);
459
+ assertSafeBridgeBind({ hostname, port, tlsConfigured });
460
+
461
+ const tls = tlsConfigured
462
+ ? {
463
+ cert: await Bun.file(certPath!).text(),
464
+ key: await Bun.file(keyPath!).text(),
465
+ }
466
+ : undefined;
467
+
468
+ const eventStream = new BridgeEventStream();
469
+ const sequencer = new BridgeFrameSequencer(session.sessionId);
470
+ const permissionBroker = new UiRequestBroker<BridgePermissionRequestPayload, ClientBridgePermissionOutcome>({
471
+ emitRequest: (correlationId, request) => {
472
+ eventStream.publish(sequencer.next("permission_request", request, correlationId));
473
+ },
474
+ });
475
+ const uiBroker = new UiRequestBroker<BridgeUiRequestPayload, BridgeUiResult<unknown>>({
476
+ emitRequest: (correlationId, request) => {
477
+ eventStream.publish(sequencer.next("ui_request", request, correlationId));
478
+ },
479
+ });
480
+ const uiContext = new BridgeExtensionUIContext({
481
+ broker: uiBroker,
482
+ emit: payload => eventStream.publish(sequencer.next("ui_request", payload)),
483
+ });
484
+ setToolUIContext?.(uiContext, true);
485
+ session.setClientBridge(createBridgeClientBridge(permissionBroker));
486
+ session.subscribe(event => eventStream.publish(toBridgeEventFrame(event, sequencer)));
487
+ const output = (obj: RpcResponse | object) => {
488
+ eventStream.publish(sequencer.next(frameTypeForDispatchOutput(obj), obj));
489
+ };
490
+ const hostToolBridge = new RpcHostToolBridge(output);
491
+ const hostUriBridge = new RpcHostUriBridge(output);
492
+ const idempotencyCache: BridgeIdempotencyCache = new Map();
493
+
494
+ Bun.serve({
495
+ hostname,
496
+ port,
497
+ ...(tls ? { tls } : {}),
498
+ fetch: createBridgeFetchHandler({
499
+ sessionId: session.sessionId,
500
+ token,
501
+ eventStream,
502
+ idempotencyCache,
503
+ permissionBroker,
504
+ uiBroker,
505
+ hostToolBridge,
506
+ hostUriBridge,
507
+ commandScopes,
508
+ commandDispatcher: command =>
509
+ dispatchRpcCommand(command, {
510
+ session,
511
+ output,
512
+ hostToolRegistry: hostToolBridge,
513
+ hostUriRegistry: hostUriBridge,
514
+ createUiContext: () => uiContext,
515
+ }),
516
+ }),
517
+ });
518
+
519
+ return new Promise<never>(() => {});
520
+ }