From 291b765aa7ef153051b408504d749f03e8e870b6 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 2 Sep 2026 17:11:38 -0400 Subject: [PATCH] fix(core): drop undefined permission metadata values before publishing requests --- packages/core/src/permission.ts | 6 +++++- packages/core/test/permission.test.ts | 29 ++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts index acc0705352c1..46cfa14a688b 100644 --- a/packages/core/src/permission.ts +++ b/packages/core/src/permission.ts @@ -190,7 +190,11 @@ const layer = Layer.effect( action: input.action, resources: input.resources, save: input.save, - metadata: input.metadata, + // Tools copy optional inputs straight into metadata; undefined values are not JSON and would + // fail the HttpApi response encoder when clients list pending requests. + metadata: + input.metadata && + Object.fromEntries(Object.entries(input.metadata).filter(([, value]) => value !== undefined)), source: input.source, message, } diff --git a/packages/core/test/permission.test.ts b/packages/core/test/permission.test.ts index 6dba198515b4..2eee91feb498 100644 --- a/packages/core/test/permission.test.ts +++ b/packages/core/test/permission.test.ts @@ -1,5 +1,5 @@ import { describe, expect } from "bun:test" -import { Cause, Deferred, Effect, Fiber, Layer } from "effect" +import { Cause, Deferred, Effect, Fiber, Layer, Schema } from "effect" import { Agent } from "@opencode-ai/core/agent" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" @@ -260,6 +260,33 @@ describe("Permission", () => { }), ) + it.effect("drops undefined metadata values so pending requests encode as JSON", () => + Effect.gen(function* () { + yield* setup() + // Tools copy optional model inputs straight into metadata, so omitted inputs arrive as undefined values. + const { service, request } = yield* waitForRequest({ + action: "glob", + resources: ["/project"], + metadata: { root: "/project", path: undefined, hidden: undefined, limit: undefined }, + }) + expect(request.metadata).toStrictEqual({ root: "/project" }) + expect(yield* service.forSession(request.sessionID)).toEqual([request]) + // HttpApi encodes JSON success bodies through Schema.toCodecJson, which rejects undefined values in Unknown. + const encode = Schema.encodeEffect(Schema.toCodecJson(Schema.Struct({ data: Schema.Array(Permission.Request) }))) + expect(yield* encode({ data: yield* service.forSession(request.sessionID) })).toEqual({ + data: [ + { + id: request.id, + sessionID: request.sessionID, + action: "glob", + resources: ["/project"], + metadata: { root: "/project" }, + }, + ], + }) + }), + ) + it.effect("defects when an asked permission is declined", () => Effect.gen(function* () { yield* setup()