| author | |
| committer | |
| log | 6424197a19a28d5992be555633a6e37fd0a58573 |
| tree | 6ff5d87ae10e57c21239149fea986312588927c3 |
| parent | 8d4a8b4f3179a054b545b252a1b4c005e9da7875 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>3 files changed, 63 insertions(+), 5 deletions(-)
packages/effect-codex-app-server/src/errors.ts+32-1| ... | ... | @@ -82,6 +82,11 @@ const payloadKind = (payload: unknown): CodexAppServerPayloadKind => { |
| 82 | 82 | return typeof payload; |
| 83 | 83 | }; |
| 84 | 84 | |
| 85 | const protocolMessageFields = ["id", "method", "params", "result", "error"] as const; | |
| 86 | ||
| 87 | export const CodexAppServerProtocolMessageField = Schema.Literals(protocolMessageFields); | |
| 88 | export type CodexAppServerProtocolMessageField = typeof CodexAppServerProtocolMessageField.Type; | |
| 89 | ||
| 85 | 90 | export interface CodexAppServerRequestDiagnostics { |
| 86 | 91 | readonly method?: string; |
| 87 | 92 | readonly requestId?: string; |
| ... | ... | @@ -157,7 +162,8 @@ export class CodexAppServerProtocolParseError extends Schema.TaggedErrorClass<Co |
| 157 | 162 | operation: CodexAppServerProtocolParseOperation, |
| 158 | 163 | method: Schema.optionalKey(Schema.String), |
| 159 | 164 | requestId: Schema.optionalKey(Schema.String), |
| 160 | detail: Schema.optionalKey(Schema.String), | |
| 165 | payloadKind: Schema.optionalKey(CodexAppServerPayloadKind), | |
| 166 | presentFields: Schema.optionalKey(Schema.Array(CodexAppServerProtocolMessageField)), | |
| 161 | 167 | issueCount: Schema.optionalKey(Schema.Number), |
| 162 | 168 | issueKinds: Schema.optionalKey(Schema.Array(CodexAppServerSchemaIssueKind)), |
| 163 | 169 | maximumPathDepth: Schema.optionalKey(Schema.Number), |
| ... | ... | @@ -196,6 +202,31 @@ export class CodexAppServerProtocolParseError extends Schema.TaggedErrorClass<Co |
| 196 | 202 | cause, |
| 197 | 203 | }); |
| 198 | 204 | } |
| 205 | ||
| 206 | static fromUnroutableMessage(message: unknown) { | |
| 207 | const diagnostics = { payloadKind: payloadKind(message) }; | |
| 208 | if (typeof message !== "object" || message === null || Array.isArray(message)) { | |
| 209 | return new CodexAppServerProtocolParseError({ | |
| 210 | operation: "route-wire-message", | |
| 211 | ...diagnostics, | |
| 212 | }); | |
| 213 | } | |
| 214 | ||
| 215 | const presentFields = protocolMessageFields.filter((field) => field in message); | |
| 216 | const method = | |
| 217 | "method" in message && typeof message.method === "string" ? message.method : undefined; | |
| 218 | const requestId = | |
| 219 | "id" in message && (typeof message.id === "string" || typeof message.id === "number") | |
| 220 | ? String(message.id) | |
| 221 | : undefined; | |
| 222 | return new CodexAppServerProtocolParseError({ | |
| 223 | operation: "route-wire-message", | |
| 224 | ...diagnostics, | |
| 225 | presentFields, | |
| 226 | ...(method === undefined ? {} : { method }), | |
| 227 | ...(requestId === undefined ? {} : { requestId }), | |
| 228 | }); | |
| 229 | } | |
| 199 | 230 | } |
| 200 | 231 | |
| 201 | 232 | export class CodexAppServerTransportError extends Schema.TaggedErrorClass<CodexAppServerTransportError>()( |
packages/effect-codex-app-server/src/protocol.test.ts+30| ... | ... | @@ -311,6 +311,36 @@ it.layer(NodeServices.layer)("effect-codex-app-server protocol", (it) => { |
| 311 | 311 | }), |
| 312 | 312 | ); |
| 313 | 313 | |
| 314 | it.effect("describes unroutable messages with safe structural diagnostics", () => | |
| 315 | Effect.gen(function* () { | |
| 316 | const secret = "codex-unroutable-secret-sentinel"; | |
| 317 | const { stdio, input } = yield* makeInMemoryStdio(); | |
| 318 | const termination = yield* Deferred.make<CodexError.CodexAppServerError>(); | |
| 319 | yield* CodexProtocol.makeCodexAppServerPatchedProtocol({ | |
| 320 | stdio, | |
| 321 | onTermination: (error) => Deferred.succeed(termination, error).pipe(Effect.asVoid), | |
| 322 | }); | |
| 323 | ||
| 324 | yield* Queue.offer( | |
| 325 | input, | |
| 326 | encodeJsonl({ id: true, method: "thread/start", params: { token: secret } }), | |
| 327 | ); | |
| 328 | ||
| 329 | const error = yield* Deferred.await(termination); | |
| 330 | assert.instanceOf(error, CodexError.CodexAppServerProtocolParseError); | |
| 331 | assert.deepInclude(error, { | |
| 332 | operation: "route-wire-message", | |
| 333 | method: "thread/start", | |
| 334 | payloadKind: "object", | |
| 335 | presentFields: ["id", "method", "params"], | |
| 336 | }); | |
| 337 | assert.isUndefined(error.requestId); | |
| 338 | assert.notProperty(error, "detail"); | |
| 339 | assert.notProperty(error, "cause"); | |
| 340 | assert.notInclude(error.message, secret); | |
| 341 | }), | |
| 342 | ); | |
| 343 | ||
| 314 | 344 | it.effect("classifies an input stream ending without inventing a cause", () => |
| 315 | 345 | Effect.gen(function* () { |
| 316 | 346 | const { stdio, input } = yield* makeInMemoryStdio(); |
packages/effect-codex-app-server/src/protocol.ts+1-4| ... | ... | @@ -310,10 +310,7 @@ export const makeCodexAppServerPatchedProtocol = Effect.fn("makeCodexAppServerPa |
| 310 | 310 | return handleResponse(message); |
| 311 | 311 | } |
| 312 | 312 | return Effect.fail( |
| 313 | new CodexError.CodexAppServerProtocolParseError({ | |
| 314 | detail: "Received protocol message in an unknown shape", | |
| 315 | operation: "route-wire-message", | |
| 316 | }), | |
| 313 | CodexError.CodexAppServerProtocolParseError.fromUnroutableMessage(message), | |
| 317 | 314 | ); |
| 318 | 315 | }; |
| 319 | 316 |