Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/zen-bearer-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@jevvy/permissions": patch
---

## Fixes

Fix Jevvy Permissions authentication with OpenCode Zen by sending API keys as bearer tokens. Keep the default model and configured model selection unchanged.
2 changes: 1 addition & 1 deletion packages/core/src/zen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ export const createZenClient = (
provider: "zen",
url: ZEN_SYSTEMONE_URL,
model,
headers: { "x-api-key": apiKey },
headers: { Authorization: `Bearer ${apiKey}` },
})
36 changes: 35 additions & 1 deletion packages/core/test/zen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ describe("Zen client", () => {

expect(headers.get("accept")).toBe("application/json")
expect(headers.get("content-type")).toBe("application/json")
expect(headers.get("x-api-key")).toBe("secret")
expect(headers.get("authorization")).toBe("Bearer secret")
expect(headers.has("x-api-key")).toBe(false)

const body = init?.body

Expand All @@ -44,6 +45,39 @@ describe("Zen client", () => {
})
}))

it.effect("preserves the default model and complete shell command with bearer authentication", () => Effect.gen(function*() {
const transport = vi.fn<typeof fetch>().mockResolvedValue(Response.json({
model: "jev-1.13-free",
answers: { routine: { type: "noul", noul: 0.9 } },
}))

const command = "cd packages/core && npm test > result.txt 2>&1; git status | cat"

yield* createZenClient("secret").evaluate({
state: { action: "shell", resource: command },
questions: { routine: { type: "noul", instructions: "Is this routine?" } },
}).pipe(Effect.provideService(FetchHttpClient.Fetch, transport))

expect(transport).toHaveBeenCalledOnce()

const [url, init] = transport.mock.calls[0] ?? []
const headers = new Headers(init?.headers)

expect(String(url)).toBe(ZEN_SYSTEMONE_URL)
expect(init?.method).toBe("POST")
expect(headers.get("authorization")).toBe("Bearer secret")
expect(headers.has("x-api-key")).toBe(false)
expect(init?.body).toBeInstanceOf(Uint8Array)

if (!(init?.body instanceof Uint8Array)) return

expect(JSON.parse(new TextDecoder().decode(init.body))).toEqual({
model: "jev-1.13-free",
state: { action: "shell", resource: command },
questions: { routine: { type: "noul", instructions: "Is this routine?" } },
})
}))

it("rejects missing, malformed, and out-of-range answers", () => {
const questions = { routine: { type: "noul" as const, instructions: "Routine?" } }

Expand Down
41 changes: 40 additions & 1 deletion packages/permissions/test/engine.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, expect, it, vi } from "@effect/vitest"
import { Cause, Effect, Exit, Fiber } from "effect"
import { TestClock } from "effect/testing"
import { JevProviderError } from "@jevvy/core"
import * as FetchHttpClient from "effect/http/FetchHttpClient"
import { createZenClient, JevProviderError } from "@jevvy/core"
import type { JevClient, JevProviderError as JevProviderErrorType, JevRequest, JevResult } from "@jevvy/core"
import { createPermissionReviewer } from "../src/engine.ts"
import type { ApprovalQuestions } from "../src/questions.ts"
Expand All @@ -21,6 +22,44 @@ const fakeClient = (
): JevClient => ({ evaluate })

describe("permission reviewer", () => {
it.effect("abstains on Zen authentication failure without caching it and retries the complete command", () => Effect.gen(function*() {
const transport = vi.fn<typeof fetch>()
.mockResolvedValueOnce(Response.json({
type: "error",
error: { type: "AuthError", message: "Invalid API key" },
}, { status: 401 }))
.mockResolvedValue(Response.json({ answers: answer(0.01).answers, model: "jev-1.13-free" }))

const reviewer = yield* createPermissionReviewer({
evaluate: (request) => createZenClient("secret").evaluate(request).pipe(Effect.provideService(FetchHttpClient.Fetch, transport)),
})

const command = "cd packages/core && npm test > result.txt 2>&1; git status | cat"

expect(yield* reviewer.review({ action: "shell", resources: [command] })).toMatchObject({
effect: "ask",
reason: "unavailable",
failure: { provider: "zen", kind: "authentication", status: 401, code: "AuthError" },
})
expect(yield* reviewer.review({ action: "shell", resources: [command] })).toMatchObject({ effect: "allow" })
expect(transport).toHaveBeenCalledTimes(2)

for (const [, init] of transport.mock.calls) {
const headers = new Headers(init?.headers)

expect(headers.get("authorization")).toBe("Bearer secret")
expect(headers.has("x-api-key")).toBe(false)
expect(init?.body).toBeInstanceOf(Uint8Array)

if (!(init?.body instanceof Uint8Array)) continue

expect(JSON.parse(new TextDecoder().decode(init.body))).toMatchObject({
model: "jev-1.13-free",
state: { action: "shell", resource: command },
})
}
}))

it.effect("allows only when every resource independently passes every question", () => Effect.gen(function*() {
const requests: JevRequest[] = []

Expand Down
Loading