Skip to content
Open
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/authorized-eve-approvers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@github-tools/sdk": patch
---

Eve GitHub tools can now require repository permission from an authenticated GitHub approver, with a separate Vercel Connect user credential for identity proof.
17 changes: 17 additions & 0 deletions apps/docs/content/docs/4.guide/5.vercel-connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,23 @@ export default connectGithubTools('github/my-connector', {
})
```

To authorize approval responders by repository permission, use a separate user-scoped credential for identity proof:

```ts [agent/tools/github.ts]
import { connectGithubApproverAuth, connectGithubTools } from '@github-tools/sdk/connect/eve'
import { githubRepositoryApprover } from '@github-tools/sdk/eve-runtime'

export default connectGithubTools('github/my-connector', {
preset: 'maintainer',
authorizeApprovalResponse: githubRepositoryApprover({
auth: connectGithubApproverAuth('github/my-connector'),
minimumPermission: 'write',
}),
})
```

The agent’s app-scoped token checks repository permission; the user-scoped credential only identifies the responder. `minimumPermission` accepts `read`, `triage`, `write` (default), `maintain`, or `admin`.

`connectGithubTools` mints the Connect token **lazily** (inside each tool `execute`). Do not `await getToken(...)` at module top level in `agent/tools/`, that runs at import/build time and fails without the Vercel OIDC header.

There is no dedicated starter for this path; new agents should use the [eve extension](#eve-extension) above.
Expand Down
9 changes: 8 additions & 1 deletion apps/docs/content/docs/5.api/2.reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ type EveGithubToolsOptions = {
include?: GithubToolName[]
exclude?: GithubToolName[]
requireApproval?: boolean | Partial<Record<GithubWriteToolName, EveApprovalValue>>
authorizeApprovalResponse?: EveResponseApprovalConfig
overrides?: EveToolOverrides
author?: CommitIdentity
committer?: CommitIdentity
Expand All @@ -323,9 +324,15 @@ type EveApprovalValue =
| 'always'
| 'once'
| 'never'
| Approval // from eve/tools
| ApprovalPolicy // from eve/tools

type EveResponseApprovalConfig =
| ApprovalResponsePolicy
| Partial<Record<GithubWriteToolName, ApprovalResponsePolicy>>
```

`githubRepositoryApprover(options)` from `@github-tools/sdk/eve-runtime` creates an `ApprovalResponsePolicy` that checks the authenticated responder’s repository permission. Its `minimumPermission` is `read`, `triage`, `write` (default), `maintain`, or `admin`. Pair it with `connectGithubApproverAuth(connector, params?)` from `@github-tools/sdk/connect/eve`, which creates the separate user-scoped credential used to identify the responder.

`include` is a set of tool names. Pass it alone to hand-pick an exact set, or alongside `preset` to add tools the preset is missing (the effective set is the **union** of both). `exclude` removes tool names from that resolved `preset` + `include` set, use it to drop a couple of tools from a larger preset. Also exports individual eve tool factories (`listPullRequests()`, `createIssue()`, …) for one-tool-per-file layouts. Approval supports `once`, predicates, and eve helper passthrough. Unlike the Workflow subpath, approval **is enforced** at runtime.

## `connectGithubTools(connector, options?)`
Expand Down
22 changes: 22 additions & 0 deletions apps/docs/content/docs/6.deprecated/1.eve.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,27 @@ This is eve's headline advantage over the boolean `needsApproval` on the AI SDK

Default (no `requireApproval`): all write tools β†’ `always()`. Unlisted write tools keep the `always()` fail-safe default. Read tools never require approval.

### Authorize approval responders

Use `authorizeApprovalResponse` to require repository permission from the authenticated person responding to an approval:

```ts [agent/tools/github.ts]
import { connectGithubApproverAuth } from '@github-tools/sdk/connect/eve'
import { createGithubTools } from '@github-tools/sdk/eve'
import { githubRepositoryApprover } from '@github-tools/sdk/eve-runtime'

export default createGithubTools({
authorizeApprovalResponse: githubRepositoryApprover({
auth: connectGithubApproverAuth('github/my-connector'),
minimumPermission: 'write',
}),
})
```

`connectGithubApproverAuth` uses a separate, user-scoped Connect credential to identify the responder. `githubRepositoryApprover` uses the agent token (`GITHUB_TOKEN` by default) to verify the responder’s permission for the tool input’s `owner` and `repo`. `minimumPermission` accepts `read`, `triage`, `write` (default), `maintain`, or `admin`.

Pass a response policy directly to apply it to every write tool, or pass a partial map keyed by write-tool name. Tools without string `owner` and `repo` inputs are rejected by the repository policy, so use a per-tool map when only selected repository tools should require it.

For durable HITL with the standard boolean/per-tool config, use [durable Workflow agents](/frameworks/vercel-workflow) with `WorkflowAgent`. See also [Control write safety](/guide/approval-control) for the AI SDK surface.

## Cherry-pick one tool per file
Expand All @@ -174,6 +195,7 @@ All presets (`code-review`, `issue-triage`, `repo-explorer`, `ci-ops`, `security
| `include` | Tool names to add on top of `preset` (union), or the full set standalone |
| `exclude` | Tool names to remove from the resolved `preset` + `include` set |
| `requireApproval` | Global, per-tool, or predicate approval (eve) |
| `authorizeApprovalResponse` | Global or per-write-tool responder authorization policy |
| `overrides` | Per-tool `description`, `approval`, `toModelOutput`, `outputSchema` |
| `author` / `committer` / `coAuthors` | Commit attribution for file/merge tools |

Expand Down
2 changes: 2 additions & 0 deletions apps/docs/skills/github-tools-agents/references/eve-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Tool names in the dynamic map match the AI SDK package (`listPullRequests`, `cre

- Default: write tools β†’ `always()`
- `'once'`, predicates, `always()` / `never()` passthrough
- `authorizeApprovalResponse` accepts a global or per-write-tool response policy
- `githubRepositoryApprover` from `@github-tools/sdk/eve-runtime` checks the authenticated responder's repository permission; pair it with `connectGithubApproverAuth` from `@github-tools/sdk/connect/eve`
- Unlike `createDurableGithubAgent`, eve approval **works durably**

## Cherry-pick
Expand Down
16 changes: 16 additions & 0 deletions packages/github-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,22 @@ Dynamic tools are named by their **bare map key**: the model sees `listPullReque

Default (no `requireApproval`): all write tools β†’ `always()`. Unlisted write tools keep the `always()` fail-safe default.

Use `authorizeApprovalResponse` to restrict who can settle approvals. The repository policy identifies the responder with a user-scoped Connect credential, then checks that user’s repository permission with the agent token:

```ts
import { connectGithubApproverAuth } from '@github-tools/sdk/connect/eve'
import { githubRepositoryApprover } from '@github-tools/sdk/eve-runtime'

export default createGithubTools({
authorizeApprovalResponse: githubRepositoryApprover({
auth: connectGithubApproverAuth('github/my-connector'),
minimumPermission: 'write',
}),
})
```

`minimumPermission` accepts `read`, `triage`, `write` (default), `maintain`, or `admin`. The policy requires string `owner` and `repo` tool inputs. Pass a per-tool map to `authorizeApprovalResponse` when only selected repository tools should use it.

Unlike the Workflow SDK subpath, eve approval **works durably**: gated tools pause the session until a human approves.

#### Cherry-picking (one tool per file)
Expand Down
2 changes: 1 addition & 1 deletion 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.34.0",
"workflow": "^4.5.0",
"zod": "^4.3.6"
},
Expand Down
28 changes: 28 additions & 0 deletions packages/github-tools/src/connect/eve-approver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it, vi } from 'vitest'
import { connect } from '@vercel/connect/eve'
import { connectGithubApproverAuth } from './eve-approver'

vi.mock('@vercel/connect/eve', () => ({ connect: vi.fn() }))

const mockedConnect = vi.mocked(connect)

describe('connectGithubApproverAuth', () => {
it('creates a user-scoped identity provider with read:user by default', () => {
connectGithubApproverAuth('github')

expect(mockedConnect).toHaveBeenCalledWith({
connector: 'github',
displayName: 'GitHub',
principalType: 'user',
tokenParams: { scopes: ['read:user'] },
})
})

it('preserves supplied token parameters but removes repository selection', () => {
connectGithubApproverAuth('github', { repositories: ['vercel/sdk'], scopes: ['user:email'] })

expect(mockedConnect).toHaveBeenLastCalledWith(expect.objectContaining({
tokenParams: { scopes: ['user:email'] },
}))
})
})
24 changes: 24 additions & 0 deletions packages/github-tools/src/connect/eve-approver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { connect } from '@vercel/connect/eve'
import type { ToolAuthProvider } from 'eve/tools'
import type { GithubConnectParams } from './types'

/**
* Creates the user-scoped GitHub provider used to prove an eve approval
* responder's GitHub identity. It is separate from the app-scoped write token.
*/
export function connectGithubApproverAuth(
connector: string,
params: GithubConnectParams = {},
): ToolAuthProvider {
const tokenParams = { ...params }
delete tokenParams.repositories
return connect({
connector,
displayName: 'GitHub',
principalType: 'user',
tokenParams: {
...tokenParams,
scopes: tokenParams.scopes ?? ['read:user'],
},
})
}
32 changes: 9 additions & 23 deletions packages/github-tools/src/connect/eve.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,28 @@
import { createGithubTools as createEveGithubTools } from '../eve'
import type { GithubConnectorInput } from './connector'
import { connectGithubApproverAuth } from './eve-approver'
import { connectGithubToken } from './token'
import type { ConnectGithubEveToolsOptions } from './types'

export { connectGithubApproverAuth }

/**
* Register eve GitHub tools backed by a Vercel Connect connector.
* Scopes are derived from `preset`, or from the resolved `include`/`exclude`
* tool set when those are set, unless overridden in `connect.scopes`.
*
* `connector` may be a static name or a resolver function β€” e.g. to pick a
* different connector per environment (production vs. preview) or tenant.
*
* @deprecated Use the mountable `@github-tools/eve-extension` instead and pass `connector`
* directly to `githubExtension(...)` β€” no separate Connect import is needed. This direct
* import is also **not durable** under multi-turn eve Workflow replay (`defineTool` inside
* `node_modules` is not hoisted); mount `@github-tools/eve-extension` instead
* (see https://github.com/vercel-labs/github-tools/issues/51 and
* https://github-tools.com/frameworks/eve-extension).
*
* Shared runtime helpers used by the extension are on `@github-tools/sdk/eve-runtime`
* (not deprecated).
* Scopes are derived from `preset` unless overridden in `connect.scopes`.
*
* TODO(eve-connect-bundle): eve's authored-module bundler inlines workspace-linked
* SDK code and code-splits `@vercel/connect` unless the agent sets
* `build.externalDependencies: ['@vercel/connect']` in `agent.ts`. Prefer the
* eve extension (pre-built) so that workaround is unnecessary.
* `build.externalDependencies: ['@vercel/connect']` in `agent.ts`. Remove that
* requirement when upstream eve externalizes this path.
*/
export function connectGithubTools(
connector: GithubConnectorInput,
connector: string,
options: ConnectGithubEveToolsOptions = {},
) {
const { connect, preset, include, exclude, ...rest } = options
const { connect, preset, ...rest } = options

return createEveGithubTools({
...rest,
preset,
include,
exclude,
token: connectGithubToken(connector, { preset, include, exclude, params: connect }),
token: connectGithubToken(connector, { preset, params: connect }),
})
}
3 changes: 3 additions & 0 deletions packages/github-tools/src/eve-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ export {
executeGithubEveTool,
} from './eve/build'
export { mapEveApprovalValue, resolveEveApproval, resolveEveToolApproval, isEveApprovalDisabled } from './eve/approval'
export { githubRepositoryApprover } from './eve/approver'
export type { GithubRepositoryApproverOptions } from './eve/approver'
export type {
EveApprovalConfig,
EveApprovalValue,
EveGithubToolsOptions,
EveResponseApprovalConfig,
EveToolFactoryOptions,
EveToolOverrides,
} from './eve/types'
Expand Down
71 changes: 38 additions & 33 deletions packages/github-tools/src/eve/approval.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,65 @@
import type { Approval, ApprovalPolicy } from 'eve/tools'
import type { Approval, ApprovalPolicy, ApprovalResponsePolicy } from 'eve/tools'
import type { GithubWriteToolName } from '../core/write-tools'
import { getEveApprovalHelpers } from './load-eve'
import type { EveApprovalConfig, EveApprovalValue } from './types'
import type { EveApprovalConfig, EveApprovalValue, EveResponseApprovalConfig } from './types'

/**
* `false` / `'never'` should omit the tool `approval` field entirely.
* eve treats a missing `approval` like `never()`; attaching `never()` is
* redundant and has caused approval UI noise on some channels.
*/
export function isEveApprovalDisabled(value: EveApprovalValue | undefined): boolean {
return value === false || value === 'never'
}

/** Convert the request-policy shorthand accepted by the public API to eve's policy. */
export function mapEveApprovalValue(value: EveApprovalValue): ApprovalPolicy {
if (typeof value === 'function') return value

const { always, never, once } = getEveApprovalHelpers()
if (value === true || value === 'always') return always() as ApprovalPolicy
if (value === false || value === 'never') return never() as ApprovalPolicy
if (value === 'once') return once() as ApprovalPolicy
return always() as ApprovalPolicy
}

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

return always()
function resolveRequestPolicy(
toolName: GithubWriteToolName,
config: EveApprovalConfig | undefined,
override?: EveApprovalValue,
): ApprovalPolicy {
if (override !== undefined) return mapEveApprovalValue(override)
if (config === undefined || config === true) return getEveApprovalHelpers().always() as ApprovalPolicy
if (config === false) return getEveApprovalHelpers().never() as ApprovalPolicy
return mapEveApprovalValue(config[toolName] ?? true)
}

export function resolveEveApproval(
toolName: GithubWriteToolName,
config: EveApprovalConfig | undefined,
): ApprovalPolicy {
if (config === undefined) return getEveApprovalHelpers().always()
if (config === true) return getEveApprovalHelpers().always()
if (config === false) return getEveApprovalHelpers().never()

const value = config[toolName]
if (value === undefined) return getEveApprovalHelpers().always()
return resolveRequestPolicy(toolName, config)
}

return mapEveApprovalValue(value)
function resolveResponsePolicy(
toolName: GithubWriteToolName,
config: EveResponseApprovalConfig | undefined,
): ApprovalResponsePolicy | undefined {
return typeof config === 'function' ? config : config?.[toolName]
}

/**
* Approval to attach on a write tool, or `undefined` to omit the field.
* Prefer this when building `defineTool` values so `false` / `'never'` do not
* attach a redundant `never()` handler.
*/
/** Resolve the complete approval definition without allowing request overrides to drop responder authorization. */
export function resolveEveToolApproval(
toolName: GithubWriteToolName,
config: EveApprovalConfig | undefined,
override?: EveApprovalValue,
responseConfig?: EveResponseApprovalConfig,
): 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 response = resolveResponsePolicy(toolName, responseConfig)
const disabled = override !== undefined
? isEveApprovalDisabled(override)
: config === false || (typeof config === 'object' && config !== null && isEveApprovalDisabled(config[toolName]))

// Omit disabled approvals unless a response policy was explicitly configured.
// In that case retain the complete definition so request-policy overrides do
// not silently discard responder authorization.
if (disabled && response === undefined) return undefined

const request = resolveRequestPolicy(toolName, config, override)
return response === undefined ? request : { request, response }
}
Loading
Loading