@nifrajs/coding-agent 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/EXTENSIONS.md +25 -0
  2. package/PERFORMANCE.md +19 -0
  3. package/README.md +69 -0
  4. package/REGISTRY.md +15 -0
  5. package/SECURITY.md +32 -0
  6. package/dist/approvals.d.ts +70 -0
  7. package/dist/approvals.d.ts.map +1 -0
  8. package/dist/approvals.js +174 -0
  9. package/dist/approvals.js.map +1 -0
  10. package/dist/capabilities.d.ts +20 -0
  11. package/dist/capabilities.d.ts.map +1 -0
  12. package/dist/capabilities.js +49 -0
  13. package/dist/capabilities.js.map +1 -0
  14. package/dist/cli.d.ts +24 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +380 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/context.d.ts +17 -0
  19. package/dist/context.d.ts.map +1 -0
  20. package/dist/context.js +66 -0
  21. package/dist/context.js.map +1 -0
  22. package/dist/deployment-adapters.d.ts +40 -0
  23. package/dist/deployment-adapters.d.ts.map +1 -0
  24. package/dist/deployment-adapters.js +147 -0
  25. package/dist/deployment-adapters.js.map +1 -0
  26. package/dist/diff.d.ts +17 -0
  27. package/dist/diff.d.ts.map +1 -0
  28. package/dist/diff.js +61 -0
  29. package/dist/diff.js.map +1 -0
  30. package/dist/errors.d.ts +9 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +35 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/extensions.d.ts +104 -0
  35. package/dist/extensions.d.ts.map +1 -0
  36. package/dist/extensions.js +284 -0
  37. package/dist/extensions.js.map +1 -0
  38. package/dist/handoffs.d.ts +97 -0
  39. package/dist/handoffs.d.ts.map +1 -0
  40. package/dist/handoffs.js +199 -0
  41. package/dist/handoffs.js.map +1 -0
  42. package/dist/healing.d.ts +56 -0
  43. package/dist/healing.d.ts.map +1 -0
  44. package/dist/healing.js +54 -0
  45. package/dist/healing.js.map +1 -0
  46. package/dist/host.d.ts +61 -0
  47. package/dist/host.d.ts.map +1 -0
  48. package/dist/host.js +369 -0
  49. package/dist/host.js.map +1 -0
  50. package/dist/index.d.ts +26 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +26 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/isolated-worker.d.ts +2 -0
  55. package/dist/isolated-worker.d.ts.map +1 -0
  56. package/dist/isolated-worker.js +155 -0
  57. package/dist/isolated-worker.js.map +1 -0
  58. package/dist/isolated.d.ts +52 -0
  59. package/dist/isolated.d.ts.map +1 -0
  60. package/dist/isolated.js +293 -0
  61. package/dist/isolated.js.map +1 -0
  62. package/dist/native.d.ts +92 -0
  63. package/dist/native.d.ts.map +1 -0
  64. package/dist/native.js +339 -0
  65. package/dist/native.js.map +1 -0
  66. package/dist/orchestration/artifact-port.d.ts +30 -0
  67. package/dist/orchestration/artifact-port.d.ts.map +1 -0
  68. package/dist/orchestration/artifact-port.js +59 -0
  69. package/dist/orchestration/artifact-port.js.map +1 -0
  70. package/dist/orchestration/catalog.d.ts +71 -0
  71. package/dist/orchestration/catalog.d.ts.map +1 -0
  72. package/dist/orchestration/catalog.js +46 -0
  73. package/dist/orchestration/catalog.js.map +1 -0
  74. package/dist/orchestration/compile.d.ts +59 -0
  75. package/dist/orchestration/compile.d.ts.map +1 -0
  76. package/dist/orchestration/compile.js +268 -0
  77. package/dist/orchestration/compile.js.map +1 -0
  78. package/dist/orchestration/dispatch.d.ts +115 -0
  79. package/dist/orchestration/dispatch.d.ts.map +1 -0
  80. package/dist/orchestration/dispatch.js +220 -0
  81. package/dist/orchestration/dispatch.js.map +1 -0
  82. package/dist/orchestration/durable-jobs.d.ts +35 -0
  83. package/dist/orchestration/durable-jobs.d.ts.map +1 -0
  84. package/dist/orchestration/durable-jobs.js +273 -0
  85. package/dist/orchestration/durable-jobs.js.map +1 -0
  86. package/dist/orchestration/effect-key.d.ts +23 -0
  87. package/dist/orchestration/effect-key.d.ts.map +1 -0
  88. package/dist/orchestration/effect-key.js +24 -0
  89. package/dist/orchestration/effect-key.js.map +1 -0
  90. package/dist/orchestration/evidence-store.d.ts +75 -0
  91. package/dist/orchestration/evidence-store.d.ts.map +1 -0
  92. package/dist/orchestration/evidence-store.js +183 -0
  93. package/dist/orchestration/evidence-store.js.map +1 -0
  94. package/dist/orchestration/hash.d.ts +12 -0
  95. package/dist/orchestration/hash.d.ts.map +1 -0
  96. package/dist/orchestration/hash.js +43 -0
  97. package/dist/orchestration/hash.js.map +1 -0
  98. package/dist/orchestration/host.d.ts +121 -0
  99. package/dist/orchestration/host.d.ts.map +1 -0
  100. package/dist/orchestration/host.js +334 -0
  101. package/dist/orchestration/host.js.map +1 -0
  102. package/dist/orchestration/index.d.ts +20 -0
  103. package/dist/orchestration/index.d.ts.map +1 -0
  104. package/dist/orchestration/index.js +19 -0
  105. package/dist/orchestration/index.js.map +1 -0
  106. package/dist/orchestration/policy.d.ts +63 -0
  107. package/dist/orchestration/policy.d.ts.map +1 -0
  108. package/dist/orchestration/policy.js +80 -0
  109. package/dist/orchestration/policy.js.map +1 -0
  110. package/dist/orchestration/recovery.d.ts +87 -0
  111. package/dist/orchestration/recovery.d.ts.map +1 -0
  112. package/dist/orchestration/recovery.js +197 -0
  113. package/dist/orchestration/recovery.js.map +1 -0
  114. package/dist/orchestration/tracer.d.ts +31 -0
  115. package/dist/orchestration/tracer.d.ts.map +1 -0
  116. package/dist/orchestration/tracer.js +141 -0
  117. package/dist/orchestration/tracer.js.map +1 -0
  118. package/dist/plans.d.ts +35 -0
  119. package/dist/plans.d.ts.map +1 -0
  120. package/dist/plans.js +63 -0
  121. package/dist/plans.js.map +1 -0
  122. package/dist/presets.d.ts +10 -0
  123. package/dist/presets.d.ts.map +1 -0
  124. package/dist/presets.js +70 -0
  125. package/dist/presets.js.map +1 -0
  126. package/dist/process.d.ts +6 -0
  127. package/dist/process.d.ts.map +1 -0
  128. package/dist/process.js +42 -0
  129. package/dist/process.js.map +1 -0
  130. package/dist/registry.d.ts +37 -0
  131. package/dist/registry.d.ts.map +1 -0
  132. package/dist/registry.js +51 -0
  133. package/dist/registry.js.map +1 -0
  134. package/dist/replay.d.ts +24 -0
  135. package/dist/replay.d.ts.map +1 -0
  136. package/dist/replay.js +154 -0
  137. package/dist/replay.js.map +1 -0
  138. package/dist/rpc.d.ts +51 -0
  139. package/dist/rpc.d.ts.map +1 -0
  140. package/dist/rpc.js +412 -0
  141. package/dist/rpc.js.map +1 -0
  142. package/dist/session-migration.d.ts +59 -0
  143. package/dist/session-migration.d.ts.map +1 -0
  144. package/dist/session-migration.js +315 -0
  145. package/dist/session-migration.js.map +1 -0
  146. package/dist/sessions.d.ts +74 -0
  147. package/dist/sessions.d.ts.map +1 -0
  148. package/dist/sessions.js +222 -0
  149. package/dist/sessions.js.map +1 -0
  150. package/dist/subagents.d.ts +51 -0
  151. package/dist/subagents.d.ts.map +1 -0
  152. package/dist/subagents.js +141 -0
  153. package/dist/subagents.js.map +1 -0
  154. package/dist/tools.d.ts +13 -0
  155. package/dist/tools.d.ts.map +1 -0
  156. package/dist/tools.js +37 -0
  157. package/dist/tools.js.map +1 -0
  158. package/dist/ui.d.ts +42 -0
  159. package/dist/ui.d.ts.map +1 -0
  160. package/dist/ui.js +93 -0
  161. package/dist/ui.js.map +1 -0
  162. package/dist/verification.d.ts +34 -0
  163. package/dist/verification.d.ts.map +1 -0
  164. package/dist/verification.js +91 -0
  165. package/dist/verification.js.map +1 -0
  166. package/dist/workflows.d.ts +98 -0
  167. package/dist/workflows.d.ts.map +1 -0
  168. package/dist/workflows.js +174 -0
  169. package/dist/workflows.js.map +1 -0
  170. package/package.json +84 -0
  171. package/src/approvals.ts +249 -0
  172. package/src/capabilities.ts +74 -0
  173. package/src/cli.ts +405 -0
  174. package/src/context.ts +81 -0
  175. package/src/deployment-adapters.ts +181 -0
  176. package/src/diff.ts +80 -0
  177. package/src/errors.ts +46 -0
  178. package/src/extensions.ts +402 -0
  179. package/src/handoffs.ts +299 -0
  180. package/src/healing.ts +93 -0
  181. package/src/host.ts +465 -0
  182. package/src/index.ts +165 -0
  183. package/src/isolated-worker.ts +177 -0
  184. package/src/isolated.ts +344 -0
  185. package/src/native.ts +478 -0
  186. package/src/orchestration/artifact-port.ts +75 -0
  187. package/src/orchestration/catalog.ts +102 -0
  188. package/src/orchestration/compile.ts +373 -0
  189. package/src/orchestration/dispatch.ts +383 -0
  190. package/src/orchestration/durable-jobs.ts +374 -0
  191. package/src/orchestration/effect-key.ts +35 -0
  192. package/src/orchestration/evidence-store.ts +233 -0
  193. package/src/orchestration/hash.ts +43 -0
  194. package/src/orchestration/host.ts +445 -0
  195. package/src/orchestration/index.ts +131 -0
  196. package/src/orchestration/policy.ts +116 -0
  197. package/src/orchestration/recovery.ts +271 -0
  198. package/src/orchestration/tracer.ts +177 -0
  199. package/src/plans.ts +105 -0
  200. package/src/presets.ts +90 -0
  201. package/src/process.ts +47 -0
  202. package/src/registry.ts +80 -0
  203. package/src/replay.ts +190 -0
  204. package/src/rpc.ts +598 -0
  205. package/src/session-migration.ts +401 -0
  206. package/src/sessions.ts +303 -0
  207. package/src/subagents.ts +210 -0
  208. package/src/tools.ts +52 -0
  209. package/src/ui.ts +137 -0
  210. package/src/verification.ts +133 -0
  211. package/src/workflows.ts +279 -0
package/src/rpc.ts ADDED
@@ -0,0 +1,598 @@
1
+ import {
2
+ type AgentBackend,
3
+ type AgentSessionSnapshot,
4
+ type ReloadResult,
5
+ resumeFromCursor,
6
+ } from "@nifrajs/agent-protocol"
7
+ import { readProjectDiff } from "./diff.ts"
8
+ import { publicErrorDetails } from "./errors.ts"
9
+ import type { ExtensionHost } from "./extensions.ts"
10
+ import { CodingAgentHost, type CodingAgentHostOptions } from "./host.ts"
11
+ import { readBoundedText } from "./process.ts"
12
+ import type { ContextWindowOptions, SessionStore } from "./sessions.ts"
13
+ import type { UiExtensionHost, UiExtensionManifest } from "./ui.ts"
14
+ import { runNifraVerification } from "./verification.ts"
15
+
16
+ export interface CodingAgentRpcServerOptions {
17
+ readonly backend: AgentBackend
18
+ readonly cwd: string
19
+ readonly hostname?: string
20
+ /** Remote binding is opt-in; the default only permits loopback hosts. */
21
+ readonly allowRemote?: boolean
22
+ readonly port?: number
23
+ /** A random token is generated when omitted. Use an explicit token only for managed local launchers. */
24
+ readonly authToken?: string
25
+ /** Include bounded exception stacks in RPC failures only for trusted loopback debugging. */
26
+ readonly exposeErrorStacks?: boolean
27
+ readonly maxBodyBytes?: number
28
+ readonly sessionStore?: SessionStore
29
+ readonly contextWindow?: ContextWindowOptions
30
+ readonly extensions?: ExtensionHost
31
+ readonly ui?: UiExtensionHost
32
+ readonly verifyAfterTurn?: readonly ("check" | "assure" | "test")[]
33
+ readonly maxRepairAttempts?: number
34
+ readonly verification?: CodingAgentHostOptions["verification"]
35
+ }
36
+
37
+ export interface CodingAgentRpcServerHandle {
38
+ readonly url: string
39
+ readonly token: string
40
+ stop(): Promise<void>
41
+ }
42
+
43
+ interface RpcRequest {
44
+ readonly method: string
45
+ readonly params?: unknown
46
+ }
47
+
48
+ interface RpcServerLike {
49
+ readonly url: URL
50
+ stop(closeActiveConnections?: boolean): void
51
+ }
52
+
53
+ /**
54
+ * Minimal loopback RPC surface for the CLI, Workbench, CI clients, and a future mobile companion.
55
+ * Turn output is SSE so clients do not need a WebSocket dependency; every event is still a versioned
56
+ * protocol event from the backend.
57
+ */
58
+ export class CodingAgentRpcServer {
59
+ readonly host: CodingAgentHost
60
+ private readonly options: CodingAgentRpcServerOptions
61
+ private readonly token: string
62
+ private readonly maxBodyBytes: number
63
+ private listener: RpcServerLike | undefined
64
+
65
+ constructor(options: CodingAgentRpcServerOptions) {
66
+ this.options = Object.freeze({ ...options })
67
+ this.host = new CodingAgentHost({
68
+ backend: options.backend,
69
+ ...(options.sessionStore === undefined ? {} : { sessionStore: options.sessionStore }),
70
+ ...(options.contextWindow === undefined ? {} : { contextWindow: options.contextWindow }),
71
+ ...(options.extensions === undefined ? {} : { extensions: options.extensions }),
72
+ ...(options.verifyAfterTurn === undefined
73
+ ? {}
74
+ : { verifyAfterTurn: options.verifyAfterTurn }),
75
+ ...(options.maxRepairAttempts === undefined
76
+ ? {}
77
+ : { maxRepairAttempts: options.maxRepairAttempts }),
78
+ exposeErrorStacks: options.exposeErrorStacks === true,
79
+ ...(options.verification === undefined ? {} : { verification: options.verification }),
80
+ })
81
+ this.token = options.authToken ?? crypto.randomUUID().replaceAll("-", "")
82
+ if (!/^[A-Za-z0-9._~-]{16,256}$/.test(this.token))
83
+ throw new TypeError("agent rpc: authToken must be a bounded token")
84
+ this.maxBodyBytes = options.maxBodyBytes ?? 1_048_576
85
+ if (!Number.isSafeInteger(this.maxBodyBytes) || this.maxBodyBytes < 1024)
86
+ throw new RangeError("agent rpc: maxBodyBytes must be at least 1024")
87
+ }
88
+
89
+ async start(): Promise<CodingAgentRpcServerHandle> {
90
+ if (this.listener !== undefined) throw new Error("agent rpc: server is already running")
91
+ const hostname = this.options.hostname ?? "127.0.0.1"
92
+ if (!this.options.allowRemote && !isLoopbackHost(hostname))
93
+ throw new Error("agent rpc: remote binding requires allowRemote: true")
94
+ if (this.options.exposeErrorStacks === true && this.options.allowRemote === true)
95
+ throw new Error("agent rpc: exposeErrorStacks is only allowed for local-only binding")
96
+ this.listener = Bun.serve({
97
+ hostname,
98
+ port: this.options.port ?? 0,
99
+ fetch: (request) => this.fetch(request),
100
+ })
101
+ const listener = this.listener
102
+ return {
103
+ url: listener.url.toString().replace(/\/$/, ""),
104
+ token: this.token,
105
+ stop: async () => {
106
+ await this.stop()
107
+ },
108
+ }
109
+ }
110
+
111
+ async stop(): Promise<void> {
112
+ const listener = this.listener
113
+ this.listener = undefined
114
+ await this.host.stop("rpc server stopped")
115
+ listener?.stop(true)
116
+ }
117
+
118
+ private async fetch(request: Request): Promise<Response> {
119
+ const cors = corsHeaders(request)
120
+ if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: cors })
121
+ const url = new URL(request.url)
122
+ if (url.pathname === "/health" && request.method === "GET")
123
+ return json({ ok: true, protocol: 1 }, 200, cors)
124
+ if (!authorized(request, this.token))
125
+ return json(
126
+ { error: { code: "unauthorized", message: "agent RPC authorization required" } },
127
+ 401,
128
+ cors,
129
+ )
130
+ if (url.pathname !== "/rpc" || request.method !== "POST")
131
+ return json(
132
+ { error: { code: "not_found", message: "unknown agent RPC endpoint" } },
133
+ 404,
134
+ cors,
135
+ )
136
+ const body = await this.readRequest(request)
137
+ if (!body.ok)
138
+ return json({ error: { code: "invalid_request", message: body.error } }, body.status, cors)
139
+ let rpc: RpcRequest
140
+ try {
141
+ rpc = parseRpcRequest(body.text)
142
+ } catch (error) {
143
+ return json(
144
+ { error: this.protocolError("invalid_request", error, "request body is invalid") },
145
+ 400,
146
+ cors,
147
+ this.options.exposeErrorStacks === true,
148
+ )
149
+ }
150
+ try {
151
+ return await this.dispatch(rpc, request, cors)
152
+ } catch (error) {
153
+ return json(
154
+ { error: this.protocolError("rpc_failed", error, "request could not be completed") },
155
+ 500,
156
+ cors,
157
+ this.options.exposeErrorStacks === true,
158
+ )
159
+ }
160
+ }
161
+
162
+ private async dispatch(
163
+ request: RpcRequest,
164
+ httpRequest: Request,
165
+ cors: Headers,
166
+ ): Promise<Response> {
167
+ switch (request.method) {
168
+ case "session.create": {
169
+ const params = record(request.params)
170
+ const snapshot =
171
+ this.host.snapshot ??
172
+ (await this.host.start({
173
+ cwd: this.options.cwd,
174
+ backend: this.options.backend.info.name,
175
+ ...(typeof params.sessionId === "string" ? { sessionId: params.sessionId } : {}),
176
+ }))
177
+ return json(snapshot, 200, cors)
178
+ }
179
+ case "session.snapshot": {
180
+ const snapshot = this.requireSnapshot()
181
+ return json(snapshot, 200, cors)
182
+ }
183
+ case "session.resume": {
184
+ const params = record(request.params)
185
+ if (
186
+ typeof params.sessionId !== "string" ||
187
+ !/^[A-Za-z0-9._:-]{1,128}$/.test(params.sessionId)
188
+ )
189
+ return json(
190
+ { error: { code: "invalid_session", message: "sessionId is required" } },
191
+ 422,
192
+ cors,
193
+ )
194
+ const snapshot =
195
+ this.host.snapshot ??
196
+ (await this.host.start({
197
+ cwd: this.options.cwd,
198
+ backend: this.options.backend.info.name,
199
+ sessionId: params.sessionId,
200
+ }))
201
+ return json(snapshot, 200, cors)
202
+ }
203
+ case "session.events": {
204
+ const snapshot = this.requireSnapshot()
205
+ const params = record(request.params)
206
+ const limit = params.limit === undefined ? 512 : Number(params.limit)
207
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 4096)
208
+ return json(
209
+ {
210
+ error: { code: "invalid_history_limit", message: "limit must be between 1 and 4096" },
211
+ },
212
+ 422,
213
+ cors,
214
+ )
215
+ const window = await this.host.history(limit)
216
+ // Legacy shape when no cursor is requested; a cursor opts into bounded snapshot + resume.
217
+ if (params.cursor === undefined) return json({ entries: window }, 200, cors)
218
+ const cursor = Number(params.cursor)
219
+ if (!Number.isSafeInteger(cursor) || cursor < -1)
220
+ return json(
221
+ { error: { code: "invalid_cursor", message: "cursor must be a safe integer >= -1" } },
222
+ 422,
223
+ cors,
224
+ )
225
+ return json({ snapshot, resume: resumeFromCursor(window, cursor) }, 200, cors)
226
+ }
227
+ case "session.checkpoint": {
228
+ this.requireSnapshot()
229
+ const params = record(request.params)
230
+ await this.host.checkpoint(params.payload)
231
+ return json({ ok: true }, 200, cors)
232
+ }
233
+ case "session.fork": {
234
+ this.requireSnapshot()
235
+ const params = record(request.params)
236
+ if (params.targetSessionId !== undefined && typeof params.targetSessionId !== "string")
237
+ return json(
238
+ { error: { code: "invalid_session", message: "targetSessionId must be a string" } },
239
+ 422,
240
+ cors,
241
+ )
242
+ return json(await this.host.fork(params.targetSessionId as string | undefined), 201, cors)
243
+ }
244
+ case "turn.send": {
245
+ const params = record(request.params)
246
+ if (
247
+ typeof params.message !== "string" ||
248
+ params.message.trim().length === 0 ||
249
+ params.message.length > this.maxBodyBytes
250
+ )
251
+ return json(
252
+ {
253
+ error: {
254
+ code: "invalid_message",
255
+ message: "message must be a non-empty bounded string",
256
+ },
257
+ },
258
+ 422,
259
+ cors,
260
+ )
261
+ const stream = this.host.prompt(params.message, httpRequest.signal)
262
+ const encoder = new TextEncoder()
263
+ const includeErrorStacks = this.options.exposeErrorStacks === true
264
+ const turnError = (error: unknown) => ({
265
+ code: "turn_failed",
266
+ ...publicErrorDetails(error, "turn could not be completed", includeErrorStacks),
267
+ })
268
+ const body = new ReadableStream<Uint8Array>({
269
+ async start(controller) {
270
+ try {
271
+ for await (const event of stream)
272
+ controller.enqueue(
273
+ encoder.encode(`data: ${serializeRpcValue(event, includeErrorStacks)}\n\n`),
274
+ )
275
+ controller.close()
276
+ } catch (error) {
277
+ controller.enqueue(
278
+ encoder.encode(`event: error\ndata: ${JSON.stringify(turnError(error))}\n\n`),
279
+ )
280
+ controller.close()
281
+ }
282
+ },
283
+ })
284
+ return new Response(body, {
285
+ status: 200,
286
+ headers: new Headers({
287
+ ...Object.fromEntries(cors),
288
+ "content-type": "text/event-stream; charset=utf-8",
289
+ "cache-control": "no-cache, no-transform",
290
+ connection: "keep-alive",
291
+ }),
292
+ })
293
+ }
294
+ case "session.reload": {
295
+ const result: ReloadResult = await this.host.reload()
296
+ return json(result, 200, cors)
297
+ }
298
+ case "session.compact": {
299
+ this.requireSnapshot()
300
+ return json(await this.host.compact("manual"), 200, cors)
301
+ }
302
+ case "approval.list": {
303
+ this.requireSnapshot()
304
+ return json({ pending: this.host.pendingApprovals }, 200, cors)
305
+ }
306
+ case "approval.request": {
307
+ const params = record(request.params)
308
+ if (typeof params.action !== "string" || typeof params.capability !== "string")
309
+ return json(
310
+ { error: { code: "invalid_approval", message: "action and capability are required" } },
311
+ 422,
312
+ cors,
313
+ )
314
+ this.requireSnapshot()
315
+ const approval = await this.host.offerApproval({
316
+ ...(typeof params.approvalId === "string"
317
+ ? { id: params.approvalId }
318
+ : { id: crypto.randomUUID().replaceAll("-", "") }),
319
+ action: params.action,
320
+ capability: params.capability,
321
+ ...(typeof params.turnId === "string" ? { turnId: params.turnId } : {}),
322
+ ...(typeof params.reason === "string" ? { reason: params.reason } : {}),
323
+ })
324
+ if (approval === undefined)
325
+ return json(
326
+ { error: { code: "approval_limit", message: "too many pending approvals" } },
327
+ 429,
328
+ cors,
329
+ )
330
+ return json(approval, 201, cors)
331
+ }
332
+ case "approval.resolve": {
333
+ const params = record(request.params)
334
+ if (typeof params.approvalId !== "string" || typeof params.approved !== "boolean")
335
+ return json(
336
+ {
337
+ error: { code: "invalid_approval", message: "approvalId and approved are required" },
338
+ },
339
+ 422,
340
+ cors,
341
+ )
342
+ this.requireSnapshot()
343
+ const decision = await this.host.resolveApproval(
344
+ params.approvalId,
345
+ params.approved,
346
+ typeof params.reason === "string" ? params.reason : undefined,
347
+ )
348
+ if (decision === undefined)
349
+ return json(
350
+ { error: { code: "approval_not_found", message: "approval is not pending" } },
351
+ 404,
352
+ cors,
353
+ )
354
+ return json(decision, 200, cors)
355
+ }
356
+ case "ui.snapshot": {
357
+ this.requireSnapshot()
358
+ return json(
359
+ this.options.ui === undefined
360
+ ? { revision: "0", active: [] }
361
+ : { revision: this.options.ui.currentRevision, active: this.options.ui.extensions },
362
+ 200,
363
+ cors,
364
+ )
365
+ }
366
+ case "ui.reload": {
367
+ this.requireSnapshot()
368
+ if (this.options.ui === undefined)
369
+ return json(
370
+ { error: { code: "ui_unavailable", message: "UI extension host is not configured" } },
371
+ 409,
372
+ cors,
373
+ )
374
+ const params = record(request.params)
375
+ if (!Array.isArray(params.manifests))
376
+ return json(
377
+ { error: { code: "invalid_ui", message: "manifests must be an array" } },
378
+ 422,
379
+ cors,
380
+ )
381
+ return json(this.options.ui.reload(params.manifests as UiExtensionManifest[]), 200, cors)
382
+ }
383
+ case "ui.preview": {
384
+ this.requireSnapshot()
385
+ if (this.options.ui === undefined)
386
+ return json(
387
+ { error: { code: "ui_unavailable", message: "UI extension host is not configured" } },
388
+ 409,
389
+ cors,
390
+ )
391
+ const params = record(request.params)
392
+ if (!Array.isArray(params.manifests))
393
+ return json(
394
+ { error: { code: "invalid_ui", message: "manifests must be an array" } },
395
+ 422,
396
+ cors,
397
+ )
398
+ return json(this.options.ui.preview(params.manifests as UiExtensionManifest[]), 200, cors)
399
+ }
400
+ case "workflow.list": {
401
+ this.requireSnapshot()
402
+ return json({ workflows: this.options.extensions?.availableWorkflows ?? [] }, 200, cors)
403
+ }
404
+ case "subagent.list": {
405
+ this.requireSnapshot()
406
+ return json({ subagents: this.options.extensions?.availableSubagents ?? [] }, 200, cors)
407
+ }
408
+ case "provider.list": {
409
+ this.requireSnapshot()
410
+ return json({ providers: this.options.extensions?.availableProviders ?? [] }, 200, cors)
411
+ }
412
+ case "workflow.run": {
413
+ this.requireSnapshot()
414
+ if (this.options.extensions === undefined)
415
+ return json(
416
+ { error: { code: "workflow_unavailable", message: "workflow host is not configured" } },
417
+ 409,
418
+ cors,
419
+ )
420
+ const params = record(request.params)
421
+ if (typeof params.name !== "string" || !/^[a-z][a-z0-9._-]{0,63}$/.test(params.name))
422
+ return json(
423
+ { error: { code: "invalid_workflow", message: "name must be a bounded workflow id" } },
424
+ 422,
425
+ cors,
426
+ )
427
+ const result = await this.options.extensions.runWorkflow(params.name, {
428
+ ...(httpRequest.signal === undefined ? {} : { signal: httpRequest.signal }),
429
+ ...(typeof params.maxSteps === "number" &&
430
+ Number.isSafeInteger(params.maxSteps) &&
431
+ params.maxSteps > 0
432
+ ? { maxSteps: Math.min(params.maxSteps, 512) }
433
+ : {}),
434
+ ...(typeof params.maxDepth === "number" &&
435
+ Number.isSafeInteger(params.maxDepth) &&
436
+ params.maxDepth > 0
437
+ ? { maxDepth: Math.min(params.maxDepth, 16) }
438
+ : {}),
439
+ exposeErrorStacks: this.options.exposeErrorStacks === true,
440
+ })
441
+ return json(result, result.ok ? 200 : 422, cors, this.options.exposeErrorStacks === true)
442
+ }
443
+ case "verification.run": {
444
+ const params = record(request.params)
445
+ const name = params.name
446
+ if (name !== "check" && name !== "assure" && name !== "test")
447
+ return json(
448
+ {
449
+ error: {
450
+ code: "invalid_verification",
451
+ message: "name must be check, assure, or test",
452
+ },
453
+ },
454
+ 422,
455
+ cors,
456
+ )
457
+ this.requireSnapshot()
458
+ return json(
459
+ await runNifraVerification(name, {
460
+ cwd: this.options.cwd,
461
+ exposeErrorStacks: this.options.exposeErrorStacks === true,
462
+ }),
463
+ 200,
464
+ cors,
465
+ this.options.exposeErrorStacks === true,
466
+ )
467
+ }
468
+ case "project.diff": {
469
+ this.requireSnapshot()
470
+ return json(
471
+ await readProjectDiff({
472
+ cwd: this.options.cwd,
473
+ exposeErrorStacks: this.options.exposeErrorStacks === true,
474
+ }),
475
+ 200,
476
+ cors,
477
+ this.options.exposeErrorStacks === true,
478
+ )
479
+ }
480
+ case "session.stop": {
481
+ await this.host.stop("rpc request")
482
+ return json({ ok: true }, 200, cors)
483
+ }
484
+ default:
485
+ return json(
486
+ { error: { code: "method_not_found", message: `unknown method: ${request.method}` } },
487
+ 404,
488
+ cors,
489
+ )
490
+ }
491
+ }
492
+
493
+ private requireSnapshot(): AgentSessionSnapshot {
494
+ if (this.host.snapshot === undefined) throw new Error("agent RPC session has not been created")
495
+ return this.host.snapshot
496
+ }
497
+
498
+ private protocolError(
499
+ code: string,
500
+ error: unknown,
501
+ fallback: string,
502
+ ): { readonly code: string; readonly message: string; readonly stack?: string } {
503
+ return { code, ...publicErrorDetails(error, fallback, this.options.exposeErrorStacks === true) }
504
+ }
505
+
506
+ private async readRequest(
507
+ request: Request,
508
+ ): Promise<
509
+ | { readonly ok: true; readonly text: string }
510
+ | { readonly ok: false; readonly status: 400 | 413; readonly error: string }
511
+ > {
512
+ const length = Number(request.headers.get("content-length") ?? "0")
513
+ if (Number.isFinite(length) && length > this.maxBodyBytes)
514
+ return { ok: false, status: 413, error: "request body is too large" }
515
+ const result = await readBoundedText(request.body, this.maxBodyBytes)
516
+ if (result.truncated) return { ok: false, status: 413, error: "request body is too large" }
517
+ return { ok: true, text: result.text }
518
+ }
519
+ }
520
+
521
+ function parseRpcRequest(text: string): RpcRequest {
522
+ let value: unknown
523
+ try {
524
+ value = JSON.parse(text)
525
+ } catch {
526
+ throw new Error("request body must be JSON")
527
+ }
528
+ const object = record(value)
529
+ if (typeof object.method !== "string" || !/^[a-z][a-z0-9._-]{1,63}$/.test(object.method))
530
+ throw new Error("request method is invalid")
531
+ return {
532
+ method: object.method,
533
+ ...(Object.hasOwn(object, "params") ? { params: object.params } : {}),
534
+ }
535
+ }
536
+
537
+ function record(value: unknown): Record<string, unknown> {
538
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return {}
539
+ return value as Record<string, unknown>
540
+ }
541
+
542
+ function authorized(request: Request, token: string): boolean {
543
+ const header = request.headers.get("authorization")
544
+ if (header === null || !header.startsWith("Bearer ")) return false
545
+ return constantTimeEqual(header.slice(7), token)
546
+ }
547
+
548
+ function constantTimeEqual(left: string, right: string): boolean {
549
+ const a = new TextEncoder().encode(left)
550
+ const b = new TextEncoder().encode(right)
551
+ let difference = a.length ^ b.length
552
+ const length = Math.max(a.length, b.length)
553
+ for (let index = 0; index < length; index++) difference |= (a[index] ?? 0) ^ (b[index] ?? 0)
554
+ return difference === 0
555
+ }
556
+
557
+ function corsHeaders(request: Request): Headers {
558
+ const headers = new Headers({
559
+ vary: "Origin",
560
+ "access-control-allow-headers": "authorization, content-type",
561
+ "access-control-allow-methods": "GET, POST, OPTIONS",
562
+ })
563
+ const origin = request.headers.get("origin")
564
+ if (origin !== null && /^https?:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(origin))
565
+ headers.set("access-control-allow-origin", origin)
566
+ return headers
567
+ }
568
+
569
+ function isLoopbackHost(hostname: string): boolean {
570
+ return (
571
+ hostname === "localhost" ||
572
+ hostname === "127.0.0.1" ||
573
+ hostname === "::1" ||
574
+ hostname === "[::1]"
575
+ )
576
+ }
577
+
578
+ function json(
579
+ value: unknown,
580
+ status: number,
581
+ inherited: Headers,
582
+ includeErrorStacks = false,
583
+ ): Response {
584
+ const headers = new Headers(inherited)
585
+ headers.set("content-type", "application/json; charset=utf-8")
586
+ // lgtm [js/stack-trace-exposure] the default replacer removes stack fields from protocol data;
587
+ // stacks are serialized only in explicitly opt-in, loopback-only diagnostics mode.
588
+ return new Response(serializeRpcValue(value, includeErrorStacks), { status, headers })
589
+ }
590
+
591
+ function serializeRpcValue(value: unknown, includeErrorStacks: boolean): string {
592
+ return JSON.stringify(
593
+ value,
594
+ includeErrorStacks
595
+ ? undefined
596
+ : (key: string, nested: unknown) => (key === "stack" ? undefined : nested),
597
+ )
598
+ }