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
5 changes: 5 additions & 0 deletions .changeset/eve-approval-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@github-tools/sdk': patch
---

Object-shaped eve `ApprovalConfiguration` values (`{ request, response }`) passed via `requireApproval` or `overrides.approval` are now honored instead of being silently replaced by `always()`. Configurations pass through to `defineTool` unchanged so a `response` authorizer survives. Also declares the tested eve peer range (`>=0.44.0 <0.48.0` instead of `>=0.19.0`) and deprecates `MISSING_EVE_MESSAGE`, which has not been thrown since eve moved to static imports.
5 changes: 5 additions & 0 deletions .changeset/eve-extension-config-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@github-tools/eve-extension': patch
---

Validate `requireApproval` and `overrides` keys against the GitHub tool catalog at mount time — a mistyped tool name now fails config validation instead of being silently ignored. The eve peer range is bounded to the tested versions (`>=0.44.0 <0.48.0`).
46 changes: 46 additions & 0 deletions .github/workflows/eve-canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: eve canary

# The durable-stamp contract this repo depends on is not part of eve's
# documented API, and eve ships multiple releases a week. The AST guard in
# packages/github-tools-eve-extension/test/ only encodes the rules we know
# about; this job builds and tests against eve@latest on a schedule so
# upstream drift (as at eve 0.44) fails here before it reaches consumers.

on:
schedule:
- cron: '17 6 * * *'
workflow_dispatch:

permissions:
contents: read

env:
TURBO_TELEMETRY_DISABLED: 1

jobs:
canary:
name: Build & test against eve@latest
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false

- name: Set up pnpm and Node
uses: ./.github/actions/setup

# minimumReleaseAge is zeroed on purpose: the canary must see the actual latest
- name: Bump eve to latest everywhere
run: |
pnpm -r up 'eve@latest' --config.minimumReleaseAge=0
pnpm ls -r --depth -1 eve || true

- name: Build
run: pnpm exec turbo run build --filter=@github-tools/sdk --filter=@github-tools/eve-extension

- name: Typecheck
run: pnpm exec turbo run typecheck --filter=@github-tools/sdk --filter=@github-tools/eve-extension

- name: Test
run: pnpm exec turbo run test --filter=@github-tools/sdk --filter=@github-tools/eve-extension
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Ten presets (`code-review`, `issue-triage`, `repo-explorer`, `ci-ops`, `security

## eve extension durable callbacks (`packages/github-tools-eve-extension`)

On eve 0.44+, a missing durable descriptor on **any** dynamic-tool callback (`execute`, `toModelOutput`, `approval` / `approvalRequest`) discards the **entire** GitHub toolset. In `extension/tools/github.ts`, those three must be **direct** `defineTool` properties with inline functions (or identifiers). Conditional spreads and call expressions (`resolveEveApproval(...)`, `always()`) are invisible to eve's stamp. Callbacks may only close over a serializable tool `name` and re-read config via `buildSessionOptions()`. CI enforces this via `test/durable-define-tool.test.ts`.
On eve 0.44+, a missing durable descriptor on **any** dynamic-tool callback (`execute`, `toModelOutput`, `approval` / `approvalRequest`) discards the **entire** GitHub toolset. In `extension/tools/github.ts`, those three must be **direct** `defineTool` properties with inline functions (or identifiers). Conditional spreads and call expressions (`resolveEveApproval(...)`, `always()`) are invisible to eve's stamp. Callbacks may only close over a serializable tool `name` and re-read config via `buildSessionOptions()`. CI enforces this via `test/durable-define-tool.test.ts`. A scheduled canary (`.github/workflows/eve-canary.yml`) additionally builds and tests against `eve@latest` daily to catch upstream drift the static guard cannot see.

## Chat App Architecture (`apps/chat`)

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/2.frameworks/1.eve-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ Tools are exposed to the model as `<namespace>__<toolName>`, where `<namespace>`

## Pick exact tools

`preset` scopes to one of five predefined groups. To hand-pick tools instead, standalone or layered on top of a preset, use `include` and `exclude`:
`preset` scopes to one of the ten [predefined presets](/guide/presets). To hand-pick tools instead, standalone or layered on top of a preset, use `include` and `exclude`:

```ts [agent/extensions/github.ts]
import githubExtension from '@github-tools/eve-extension'
Expand Down
9 changes: 7 additions & 2 deletions packages/github-tools-eve-extension/extension/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import type { GithubTokenInput } from '@github-tools/sdk'
import type { GithubConnectorInput } from '@github-tools/sdk/connect'
import {
GITHUB_TOOL_NAMES,
GITHUB_WRITE_TOOLS,
type CommitIdentity,
type EveApprovalConfig,
type EveToolOverrides,
type GithubToolName,
type GithubToolPreset,
type GithubWriteToolName,
} from '@github-tools/sdk/eve-runtime'
import { defineExtension } from 'eve/extension'
import { z } from 'zod'
Expand Down Expand Up @@ -90,6 +92,7 @@ const presetNameSchema = z.enum([
'maintainer',
])
const toolNameSchema = z.enum(Object.values(GITHUB_TOOL_NAMES) as [GithubToolName, ...GithubToolName[]])
const writeToolNameSchema = z.enum(Object.values(GITHUB_WRITE_TOOLS) as [GithubWriteToolName, ...GithubWriteToolName[]])

const commitIdentitySchema = z.object({
name: z.string(),
Expand All @@ -114,8 +117,10 @@ const configSchema = z.object({
issueNumber: z.number().optional(),
ref: z.string().optional(),
}).optional(),
requireApproval: z.union([z.boolean(), z.record(z.string(), z.unknown())]).optional(),
overrides: z.record(z.string(), z.unknown()).optional(),
// Key validation only — a mistyped tool name would otherwise be silently
// ignored and the tool would keep its default behavior with no signal.
requireApproval: z.union([z.boolean(), z.partialRecord(writeToolNameSchema, z.unknown())]).optional(),
overrides: z.partialRecord(toolNameSchema, z.unknown()).optional(),
author: commitIdentitySchema.optional(),
committer: commitIdentitySchema.optional(),
coAuthors: z.array(commitIdentitySchema).optional(),
Expand Down
2 changes: 1 addition & 1 deletion packages/github-tools-eve-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
},
"peerDependencies": {
"@vercel/connect": ">=0.3.2",
"eve": ">=0.44.0"
"eve": ">=0.44.0 <0.48.0"
},
"peerDependenciesMeta": {
"@vercel/connect": {
Expand Down
4 changes: 2 additions & 2 deletions packages/github-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"@vercel/connect": ">=0.3.2",
"@workflow/ai": "^4.1.2",
"ai": "^6.0.97 || ^7.0.0",
"eve": ">=0.19.0",
"eve": ">=0.44.0 <0.48.0",
"workflow": "^4.5.0",
"zod": "^4.3.6"
},
Expand Down Expand Up @@ -103,7 +103,7 @@
"@vercel/connect": "^0.6.1",
"ai": "^7.0.58",
"eslint": "^10.8.0",
"eve": "~0.40.0",
"eve": "^0.46.1",
"tsdown": "^0.22.14",
"typescript": "^6.0.3",
"typescript-eslint": "^8.65.0",
Expand Down
20 changes: 18 additions & 2 deletions packages/github-tools/src/eve/approval.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ApprovalContext } from 'eve/tools'
import { describe, expect, it } from 'vitest'
import { always, never, once } from 'eve/tools/approval'
import { always, never, once, type ApprovalContext } from 'eve/tools/approval'
import {
isEveApprovalDisabled,
mapEveApprovalValue,
Expand Down Expand Up @@ -81,4 +80,21 @@ describe('resolveEveToolApproval', () => {
expect(resolveEveToolApproval('createIssue', { createIssue: 'once' })).toBeDefined()
expect(resolveEveToolApproval('createIssue', undefined, () => 'not-applicable')).toBeDefined()
})

it('passes object-shaped ApprovalConfiguration through unchanged', () => {
const configuration = {
request: always(),
response: () => ({ status: 'allowed' as const }),
}

expect(resolveEveToolApproval('createIssue', { createIssue: configuration })).toBe(configuration)
expect(resolveEveToolApproval('createIssue', undefined, configuration)).toBe(configuration)
})
})

describe('mapEveApprovalValue', () => {
it('extracts the request policy from an object-shaped ApprovalConfiguration', () => {
const request = () => 'user-approval' as const
expect(mapEveApprovalValue({ request })).toBe(request)
})
})
29 changes: 16 additions & 13 deletions packages/github-tools/src/eve/approval.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Approval, ApprovalPolicy } from 'eve/tools'
import type { Approval, ApprovalPolicy } from 'eve/tools/approval'
import type { GithubWriteToolName } from '../core/write-tools'
import { getEveApprovalHelpers } from './load-eve'
import type { EveApprovalConfig, EveApprovalValue } from './types'
Expand All @@ -14,14 +14,14 @@ export function isEveApprovalDisabled(value: EveApprovalValue | undefined): bool

export function mapEveApprovalValue(value: EveApprovalValue): ApprovalPolicy {
if (typeof value === 'function') return value
// Object-shaped eve `ApprovalConfiguration` — the request-time policy lives on `request`.
if (typeof value === 'object') return value.request

const { always, never, once } = getEveApprovalHelpers()

if (value === true || value === 'always') return always()
if (value === false || value === 'never') return never()
if (value === 'once') return once()

return always()
return once()
}

export function resolveEveApproval(
Expand All @@ -48,13 +48,16 @@ export function resolveEveToolApproval(
config: EveApprovalConfig | undefined,
override?: EveApprovalValue,
): Approval | undefined {
if (override !== undefined) {
if (isEveApprovalDisabled(override)) return undefined
return mapEveApprovalValue(override)
}
if (config === false) return undefined
if (typeof config === 'object' && config !== null && isEveApprovalDisabled(config[toolName])) {
return undefined
}
return resolveEveApproval(toolName, config)
const value = override !== undefined
? override
: typeof config === 'object' && config !== null
? config[toolName]
: config

if (value === undefined) return getEveApprovalHelpers().always()
if (isEveApprovalDisabled(value)) return undefined
// Pass policies and `ApprovalConfiguration` objects through unchanged so a
// response-time authorizer (`response`) survives onto `defineTool`.
if (typeof value === 'function' || typeof value === 'object') return value
return mapEveApprovalValue(value)
}
11 changes: 11 additions & 0 deletions packages/github-tools/src/eve/build.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { describe, expect, it, vi } from 'vitest'
import { PRESET_TOOLS } from '../core/presets'
import * as repositoryCore from '../core/repository'
import { ALL_GITHUB_TOOL_NAMES } from '../core/tool-names'
import { buildEveToolDefinition, buildEveToolMap, createEveGithubToolsDynamic, executeGithubEveTool, formatGithubEveToolOutput, hasGithubEveToolModelOutput, listResolvedEveToolNames } from './build'
import { createToolRegistry } from './registry'
import { getEveTools } from './load-eve'

describe('createGithubTools eve integration', () => {
// TypeScript catches typos in registry names but not omissions: a tool added
// to GITHUB_TOOL_NAMES and the AI SDK layer but forgotten here would silently
// never appear in eve. This pins the two catalogs together.
it('registers every GITHUB_TOOL_NAMES entry exactly once in the eve registry', () => {
const registryNames = createToolRegistry({ token: 'ghp_test' }).map(entry => entry.name)
expect(registryNames.sort()).toEqual([...ALL_GITHUB_TOOL_NAMES].sort())
expect(new Set(registryNames).size).toBe(registryNames.length)
})

it('resolves the same tool names as the AI SDK presets', () => {
for (const preset of Object.keys(PRESET_TOOLS) as Array<keyof typeof PRESET_TOOLS>) {
expect(listResolvedEveToolNames({ preset }).sort()).toEqual([...PRESET_TOOLS[preset]].sort())
Expand Down
20 changes: 13 additions & 7 deletions packages/github-tools/src/eve/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ function applyOverrides<T extends ToolDefinition>(
delete (rest as { approval?: unknown }).approval
return rest
}
return { ...next, approval: mapEveApprovalValue(override.approval) }
// Preserve policies and `ApprovalConfiguration` objects as-is; only literals need mapping.
const approval = typeof override.approval === 'function' || typeof override.approval === 'object'
? override.approval
: mapEveApprovalValue(override.approval)
return { ...next, approval }
}

export function buildEveToolDefinition(
Expand All @@ -72,15 +76,17 @@ export function buildEveToolDefinition(
? resolveEveToolApproval(entry.writeTool, options.requireApproval)
: undefined

const tool = defineTool({
// Annotated so defineTool resolves to its ToolDefinition overload — inference
// from the bare literal picks the background-tool overload on eve 0.45+.
const definition: ToolDefinition = {
description: entry.description,
inputSchema: entry.inputSchema,
...(approval && { approval }),
toModelOutput: (output: unknown) => formatGithubEveToolOutput(name, output),
execute: async (input) => runGithubToolStep(name, input as Record<string, unknown>, ctx),
})
}

return applyOverrides(tool, name, options.overrides)
return applyOverrides(defineTool(definition), name, options.overrides)
}

export function buildEveToolMap(options: EveGithubToolsOptions = {}): EveToolMap {
Expand All @@ -104,15 +110,15 @@ export function buildEveToolMap(options: EveGithubToolsOptions = {}): EveToolMap
? resolveEveToolApproval(entry.writeTool, options.requireApproval)
: undefined

const tool = defineTool({
const definition: ToolDefinition = {
description: entry.description,
inputSchema: entry.inputSchema,
...(approval && { approval }),
toModelOutput: (output: unknown) => formatGithubEveToolOutput(entry.name, output),
execute: async (input) => runGithubToolStep(entry.name, input as Record<string, unknown>, ctx),
})
}

tools[entry.name] = applyOverrides(tool, entry.name, options.overrides)
tools[entry.name] = applyOverrides(defineTool(definition), entry.name, options.overrides)
}

return tools
Expand Down
8 changes: 1 addition & 7 deletions packages/github-tools/src/eve/load-eve.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { describe, expect, it } from 'vitest'
import { MISSING_EVE_MESSAGE, getEveTools } from './load-eve'
import { getEveTools } from './load-eve'

describe('load-eve', () => {
it('exports a clear install message for the optional peer dependency', () => {
expect(MISSING_EVE_MESSAGE).toBe(
'The "eve" package is required to use @github-tools/sdk/eve. Install it with: pnpm add eve',
)
})

it('loads eve/tools when the peer dependency is installed', () => {
expect(getEveTools().defineTool).toBeTypeOf('function')
expect(getEveTools().defineDynamic).toBeTypeOf('function')
Expand Down
6 changes: 6 additions & 0 deletions packages/github-tools/src/eve/load-eve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import * as eveApproval from 'eve/tools/approval'
export type EveToolsModule = typeof import('eve/tools')
export type EveApprovalModule = typeof import('eve/tools/approval')

/**
* @deprecated Never thrown since eve moved to static ESM imports — a missing
* `eve` package now fails at module resolution with the bundler/runtime's own
* error. Kept only for backward compatibility of the public export; do not
* match on it. Will be removed in the next major.
*/
export const MISSING_EVE_MESSAGE =
'The "eve" package is required to use @github-tools/sdk/eve. Install it with: pnpm add eve'

Expand Down
3 changes: 2 additions & 1 deletion packages/github-tools/src/eve/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Approval, ToolModelOutput } from 'eve/tools'
import type { ToolModelOutput } from 'eve/tools'
import type { Approval } from 'eve/tools/approval'
import type { z } from 'zod'
import type { GithubToolsContext } from '../core/context'
import type { GithubToolPreset } from '../core/presets'
Expand Down
Loading
Loading