From fc0e46a76e9f4f3967cf4321967120af916bd900 Mon Sep 17 00:00:00 2001 From: benpankow Date: Wed, 29 Jul 2026 11:27:27 -0700 Subject: [PATCH 1/2] docs: plan authorized HITL responders Signed-off-by: benpankow --- research/hitl-approver-authorization.md | 627 ++++++++++++++++++++++++ 1 file changed, 627 insertions(+) create mode 100644 research/hitl-approver-authorization.md diff --git a/research/hitl-approver-authorization.md b/research/hitl-approver-authorization.md new file mode 100644 index 000000000..460a74e93 --- /dev/null +++ b/research/hitl-approver-authorization.md @@ -0,0 +1,627 @@ +--- +issue: 1021 +status: proposed +last_updated: "2026-07-29" +--- + +# Authorize who may settle a HITL approval + +## Goal + +Close the authorization gap in [#1021](https://github.com/vercel/eve/issues/1021): a +channel can authenticate the person clicking an approval control, but eve +currently accepts any matching `inputResponse` as the terminal approval. + +The proof of concept should make a Slack + GitHub agent usable without an +application-owned identity directory: + +```text +Slack responder clicks Allow + -> eve obtains proof of that responder's GitHub identity through an + existing authorization challenge + -> GitHub policy determines whether that identity may allow this action + -> eligible: settle the original approval and execute as the agent + -> ineligible: keep the original approval pending and give the responder + private feedback + -> any authenticated participant may instead Cancel the proposed action +``` + +The GitHub write credential remains separate from the approver credential. An +agent GitHub App / installation token (or a self-hosted agent's configured +PAT/app-token provider) performs the write; a responder's user-scoped GitHub +authorization only proves who is approving and supports the eligibility check. + +## Non-goals + +The PoC must not introduce an eve identity product: + +- no canonical-user table, account-link store, or SCIM/IdP integration; +- no generic directory, RBAC, or policy-decision API; +- no inference that a Slack user and GitHub user are the same person from a + display name or email; +- no Slack-only enforcement embedded in rendered Block Kit controls. + +The durable response-authorization boundary should be generic because tools, +connections, channels, `eve/client`, and proxied subagents share it. The +turnkey policy and identity proof are GitHub-specific and belong in +`github-tools`. + +## Current state + +- A signed Slack interaction already becomes `session.auth.current` for its + resume turn. `buildSlackAuthContext()` namespaces it by workspace and Slack + user ID. +- `packages/eve/src/harness/input-requests.ts` treats an `approve` response as + terminal: it records the approval key, clears the pending input batch, and + appends the approval response to history. +- Tool and connection OAuth is already durable. `ctx.getToken()` in a tool + emits `authorization.required`, parks, and retries after callback + completion. Its order today is **approve, then sign in**, because it runs + only from tool execution after eve has accepted approval. +- `@github-tools/sdk/eve` maps `requireApproval` into eve's request-time + `Approval` predicate. It has no response-time authorization support. +- `github-tools` currently uses Connect without a subject parameter, so its + helper receives Connect's app-scoped installation token and performs GitHub + operations as the installed app. That is correctly separate from the + approver identity. +- **Confirmed in the Connex implementation:** the native `github` connector + supports both `app` and `user` subjects. Its user path redirects to + GitHub's OAuth authorize endpoint, exchanges the callback code for a GitHub + user token, and reads `/user`; Connex stores that user-scoped authorization + against the requested subject and exposes the stable numeric GitHub user ID + as `externalSubject` (and through token-info userinfo). The app path mints a + GitHub App installation token. +- This Connect path is available to a Vercel-linked deployment: the SDK uses + the deployment's Vercel OIDC token to call Connect. `github-tools`' current + `connectGithubToken()` uses that app/install-token path. On a self-hosted + eve deployment, the current `github-tools` path is a static token or async + agent-token provider; it has no built-in per-user GitHub OAuth. +- eve itself already supports self-hosted interactive OAuth through + `defineInteractiveAuthorization`: the author provides token lookup/storage, + start URL/PKCE state, and code exchange; eve owns callback URL generation, + durable park/resume, and challenge delivery. The response-authorizer must + accept this ordinary eve provider shape rather than require Connect. + +## Proposed product shape + +Extend eve approval definitions with a response-time authorizer alongside the +existing request-time gate. Naming is intentionally illustrative: + +```ts +approval: { + policy: always(), + async authorizeResponse({ request, responder, session, auth }) { + const identity = await auth.getToken(githubApproverAuth); + return githubApprover.isEligible({ identity, request }); + }, +} +``` + +Function shorthand remains the request-time-only form. The object form adds an +opt-in response authorizer. The callback receives the stable original request +(`toolName`, typed/raw tool input, `callId`, `requestId`), verified responder, +read-only session identity/lineage, and only `auth.getToken` / `auth.requireAuth`. +It receives no sandbox, skill, model, channel, execution, or mutable-state +capability. + +The default interaction is **Allow / Cancel**. Allow invokes the authorizer; +Cancel is ordinary authenticated conversation flow control and does not. An +Allow candidate may be allowed, rejected with an authored `safeReason`, parked +on an authorization challenge, failed, timed out, or made stale by another +terminal settlement. Errors fail only that candidate and fail closed. Durable +stream events carry candidate and settlement lifecycle; delivery only +acknowledges ingestion. + +## Required invariants + +1. **Verified responder only.** A response authorizer receives the identity + derived by the authenticated ingress route/webhook, never body-supplied + identity. +2. **No unauthorized terminal effect.** A rejected responder cannot Allow, + clear, or otherwise settle the pending request. Cancel follows normal + authenticated conversation flow control. +3. **Candidate binding.** An Allow challenge is bound to `{ requestId, +responder principal }`. A callback or later delivery from another user + cannot settle that candidate. +4. **Single winner.** Concurrent/retried clicks settle a request at most once. + A stale candidate completing after another valid response is harmless. +5. **Durability.** A restart survives the original approval, candidate + response, challenge wait, and retry point. +6. **No optimistic closure.** Channels may only replace/disable an approval + card after an accepted terminal response. Rejection leaves shared controls + available. +7. **Channel agnosticism.** Slack, custom channels, and `eve/client` carry the + same responder context and consume the same durable lifecycle events. + Subagent proxies preserve identity for the child request. +8. **Per-request authorization, batch-gated execution.** Every candidate is + authorized and settled independently, but tool execution/model continuation + waits until the original input batch is terminal. +9. **Durable-first settlement.** Terminal state and audit history commit before + best-effort channel notification; notification failure cannot roll back the + decision. + +## PoC plan + +### 1. Provide GitHub approver auth for hosted and self-hosted agents + +The PoC has the same response-authorizer contract in both environments: it +uses a normal user-scoped eve auth provider to obtain a human GitHub identity. +Only the provider implementation and token ownership differ. + +| Deployment | Agent write credential | Responder identity credential | +| ------------------------------ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| Vercel-hosted / Connect-linked | Existing GitHub App installation token | New `github-tools` eve provider backed by Connect's GitHub **user** subject | +| Self-hosted | Existing `token` / `GITHUB_TOKEN` / authored async app-token provider | Author-provided `defineInteractiveAuthorization` GitHub OAuth provider and durable token store | + +**Vercel-hosted path.** The Connex source confirms that the GitHub connector +has the required user OAuth implementation: + +- `GitHubConnexClientDriver.supportedSubjectTypes` includes `user` and `app`; +- `userTokens.getAuthorizationUrl` redirects to + `https://github.com/login/oauth/authorize` and `completeAuthorization` + exchanges the OAuth code for the human's user token; +- `userTokens.getUserInfo` calls GitHub `/user` and returns `sub` as the + stable numeric GitHub ID; +- user tokens are stored and keyed against the requested Connect user subject; + app tokens are separately minted from the GitHub App installation. + +Extend `@github-tools/sdk` with an eve-facing Connect user provider. It should +request a token for the active responder principal, call `getTokenResponse()` +(not only `getToken()`), map `UserAuthorizationRequiredError` to eve's existing +interactive authorization signal, and use `externalSubject` or `/user` as the +stable approver ID. Retain `connectGithubToken()` for app-scoped execution. + +This hosted path needs no agent-owned OAuth client, link table, or token store: +Connex durably associates the GitHub grant with the responder subject. + +**Self-hosted path.** Do not make Connect a requirement. The GitHub approver +helper accepts an authored `ToolAuthProvider`, including an +`defineInteractiveAuthorization` provider that implements GitHub OAuth. eve +already hosts the OAuth callback and challenge/park/resume lifecycle. The +self-hosting application supplies its GitHub OAuth client configuration and +durable per-principal token lookup/store; `github-tools` uses the resulting +user token only to identify and authorize the approver. The PoC documents this +provider shape but does not add a generic token database or managed OAuth +service to eve or `github-tools`. + +### 2. Add durable response authorization in eve + +Implement the response authorizer on both authored tools and connection tools. + +- Extend public approval definitions and validation to accept the new + request-time + response-time configuration while retaining existing function + shorthand for request-time policies. +- Preserve the original approval request/action data in pending HITL state so + the callback receives a stable request even after a deployment/restart. +- Add an auth-only callback context backed by the same scoped authorization + machinery used by `ToolContext.getToken()` (`execution/tool-auth.ts` and + `runtime/connections/scoped-authorization.ts`). +- Represent a candidate response and auth wait explicitly in durable state; + do not overload a successful approval record. +- On callback completion, retry validation of the candidate response before + recording approved tools, clearing the pending request, or appending terminal + tool-response history. +- Keep authorization policy resolution live rather than serializing callbacks: + a resumed session resolves the currently deployed tool/connection definition. + +### 3. Add durable candidate and settlement events for channels and clients + +Delivery acknowledges ingestion only. Emit correlated durable stream events for +candidate pending, authorization-required, rejected, failed, timed-out, and +stale outcomes, plus terminal request settlement. Persist settlement history so +channels can repair stale UI later; a mandatory notification outbox is not part +of the first pass. + +For Slack specifically: + +- acknowledge the Slack webhook immediately; +- show candidate-specific checking, OAuth, and `safeReason` feedback only to + the authenticated responder; +- leave shared controls actionable while any Allow candidate is nonterminal; +- replace/disable the card only after accepted durable settlement; +- render **Allow / Cancel**, with Cancel using normal interaction auth. + +`eve/client`, hooks, custom channels, and subagent proxies consume the same safe +stream lifecycle; ephemeral Slack delivery is not a core API. + +### 4. Implement the GitHub policy in `github-tools` + +Add a GitHub-specific approver policy/helper that composes with the eve +response-authorizer API and is opt-in on GitHub write tools. + +Initial eligibility is a showcase repository-role check, not CODEOWNERS. Apply +GitHub `write` permission only to a narrow allowlist of issue/comment/label-style +operations. Exclude branch-sensitive, merge, protected-ref, workflow-definition, +repository-administration, repository creation/fork, and gist operations. +GitHub's native enforcement against the agent App remains authoritative. + +The helper should: + +- use a responder-scoped GitHub auth provider solely to identify the human + GitHub account; this is Connect user OAuth on Vercel or an author-supplied + interactive provider when self-hosted; +- use the configured agent credential (Connect installation token on Vercel, + or the existing token/provider when self-hosted) to read repository policy + as needed and perform the eventual write; +- return an explicit `safeReason`, never an execution error, for an ineligible + responder; +- keep `requireApproval` and optional `authorizeApprovalResponse` separate; + omission preserves existing request-time-only behavior, and a per-tool map + opts only supported operations into responder authorization. + +Likely touch points in `github-tools` are `packages/github-tools/src/eve/types.ts`, +`eve/approval.ts`, `eve/build.ts`, the Connect helpers, and the eve extension +configuration/schema. The exact option belongs in that repository after the +GitHub credential capability is confirmed. + +### 5. Test the end-to-end state machine + +Add eve unit/integration coverage for: + +- accepted Allow and unauthorised Cancel, including their atomic race; +- rejected Allow with the request still pending; +- missing GitHub authorization -> `authorization.required` -> callback -> + retry/accept or retry/reject; +- silent duplicate delivery and concurrent candidates; only one terminal + settlement; +- callback after another responder settles, cancellation, stale continuation, + and process/redeploy recovery; +- batched requests: independent candidate outcomes do not lose unrelated + requests, and execution remains batch-gated; +- subagent-proxied approval and responder auth; +- Slack card behavior and private rejection feedback; client/custom-channel + structured outcome. + +In `github-tools`, test that the helper identifies the responder using the +human credential, reads policy with the agent credential, and maps each +eligible/ineligible result to eve's response-authorizer outcomes. + +## Recorded decisions + +### Settlement ordering + +Durable settlement happens first. After the authorizer allows a response, eve +atomically records the terminal settlement before emitting a best-effort channel +notification. Channel delivery failure cannot roll back or block an otherwise +valid approval. + +Accepted settlement remains available as durable history for logging and +observability, and channels can use that history to repair stale UI later. A +mandatory durable notification outbox is not required for the first pass. + +### Batched approval responses + +Authorize and settle each candidate independently. One delivery may produce +mixed accepted, rejected, authorization-required, and stale outcomes; accepted +responses must not be held back by unrelated candidates that remain pending. +No unvalidated response may flow through merely because another response in the +same delivery was accepted. + +Avoid creating approval batches whose requests have dramatically different +response-authorization requirements. Tool and agent design should separate such +operations where practical, but runtime correctness must not depend on authors +doing so. + +Accepted candidates are recorded durably per request, but tool execution and +model continuation remain gated until every request in the original input batch +has reached a terminal accepted settlement. This preserves current provider and +AI SDK batch-history constraints while keeping authorization decisions +independent. + +### Missing response authorizer after deployment change + +Fail closed and retain the request when durable pending state says response +authorization is required but the current deployment cannot resolve the tool or +its authorizer. Never silently downgrade to ordinary approval settlement. +Surface a safe, nonterminal retry message to the responder, retain the request +for deployment repair or explicit cancellation, and record detailed tool/policy +resolution diagnostics for operators and observability. + +### Concurrent candidates + +Allow multiple responders to validate independently against the same pending +request. Each candidate and authorization challenge is durably bound to its +request ID, proposed decision, and authenticated responder principal. The first +allowed candidate to settle wins atomically; every later completion is stale and +has no terminal effect. + +Rejected, failed, timed-out, or abandoned candidates do not block other +responders. Candidate-specific status, OAuth challenges, and rejection feedback +remain private to that responder while the shared approval control stays +available until a winner settles it. + +### Response-authorizer capability boundary + +Expose a narrow auth capability plus read-only session metadata. The callback +receives the stable request, verified responder, proposed decision, +`auth.getToken` / `auth.requireAuth`, and read-only session identity and lineage +(`id`, `auth.initiator`, `parent`, and `turn`). It does not receive sandbox, +skills, model, channel, tool execution, or mutable session-state capability. + +Keep the verified current responder separate from the session initiator. Both +are available so policies can intentionally distinguish the person attempting +to settle the response from the person who initiated the session. + +### Authorizer errors and timeouts + +Fail only the candidate, nonterminally. An authorizer timeout or unexpected +error never consumes or settles the request, never blocks concurrent +candidates, and does not trigger automatic retries of arbitrary policy code. +Keep shared controls available and give the responder generic private retry +feedback; record detailed errors only for operators and observability. + +Apply a 10-second timeout to each authorizer attempt. An explicit +authorization-required result is not an authorizer error and keeps its candidate +durably pending through the normal challenge lifecycle. + +### Approval decisions and conversation escape hatch + +The default approval interaction is **Allow / Cancel**. Allow is the only +policy-authorized decision: it runs the response authorizer and may execute the +action after acceptance. Cancel means “do not run this proposed tool call and +continue once the batch resolves”; it does not invoke the response authorizer +or claim an authoritative policy denial. + +Treat Cancel like ordinary authenticated conversation flow control. It needs no +additional approval-specific authorization beyond the channel's normal ingress +and interaction authorization, consistent with eve's existing cancel/reset +surfaces. Record the authenticated actor for audit and observability. A +specialized, policy-authorized Deny action is not part of the first-pass default +contract. + +### Rejection and error feedback + +Show an authorizer's explicit safe rejection reason verbatim and privately to +the candidate responder. Name the public result field `safeReason` to make its +presentation semantics clear. Authors must not place credentials, sensitive +policy data, or operator diagnostics in it. + +Never expose thrown errors, timeout details, provider payloads, or internal +diagnostics directly. Map those failures to generic private retry feedback and +retain full details only in operator logs and observability. + +### Candidate lifetime and abandoned authorization + +Expire a candidate with its authorization challenge. Use the provider/challenge +expiry when available and a 10-minute eve fallback otherwise. Expiry marks only +that candidate timed out; it does not settle or block the shared request, and a +responder may start a fresh candidate. + +A callback after expiry or after another candidate settles is stale and +harmless. Retain timed-out and stale outcomes in durable history and +observability while allowing expired candidate state to be cleaned up. + +### Duplicate candidate delivery + +Deduplicate active candidates by `{ requestId, decision, responder principal }`. +A repeated click while that candidate is validating or waiting for +authorization creates no new candidate, runs no authorizer work, emits no new +feedback, and does not re-deliver or refresh the authorization challenge. + +After the candidate expires or reaches a failed/rejected terminal outcome, a +later click may create a fresh candidate. + +### Cancel racing active candidates + +Cancel and allowed candidates share one atomic terminal-settlement race. If +Cancel settles first, the action does not run, every active Allow candidate is +invalidated, and later authorization callbacks are stale and harmless. If an +allowed candidate settles first, a later Cancel is stale. + +Active validation never blocks Cancel, and Cancel cannot revoke an Allow after +accepted durable settlement. + +### Deployment changes during validation + +Resolve and re-run the currently deployed response authorizer whenever a +candidate resumes, including after authorization callback. Durable state stores +request and candidate data, never callback code or a pinned policy revision. +Policy fixes and restrictions therefore apply immediately to in-progress +candidates. + +If the current tool or authorizer cannot be resolved, follow the recorded +fail-closed behavior and retain the request. Do not route execution to old +deployment code or automatically invalidate every candidate solely because a +deployment changed. + +### Durable audit history and retention + +Persist structured candidate attempts and terminal settlements under the normal +eve session/run retention policy rather than introducing a separate identity or +audit database. Records include candidate/request IDs, proposed decision, +verified responder principal identifiers, status, timestamps, safe rejection +reason, authorization provider label when safe, and an observed policy/runtime +revision for diagnostics only. Terminal settlement records the winning actor or +cancelling actor and outcome. + +Retain rejected, failed, timed-out, and stale attempts as well as the winner. +Never persist OAuth URLs, authorization codes, access or refresh tokens, PKCE +secrets, raw provider errors, or broad responder attributes by default. + +### Subagent responder identity and policy ownership + +Preserve the verified root-channel responder through the trusted proxy chain to +the child. The child owns the pending request and candidates, resolves and runs +its currently deployed response authorizer, owns authorization challenges and +callbacks, records canonical audit history, and executes the tool. The parent +and root own routing and presentation, not policy decisions. + +For local subagents, use the existing capability-protected durable hook path and +delivery-time auth propagation. For remote agents, forward responder identity +only through the existing opt-in trusted-forwarder boundary; validate and +audit-stamp the forwarding hop, and fail closed when the remote does not accept +forwarded principals. + +Keep canonical candidate and settlement history in the child. Persist a durable +root-session projection containing the child/request/candidate correlation, +safe status needed for responder feedback, and settlement coordinates for +unified observability and UI repair, without creating a second authoritative +policy record. + +### Public channel and client outcome contract + +Durable stream events are the canonical candidate and settlement outcome +surface. Delivery acknowledges ingestion only; it does not wait for or return a +final policy result, because authorization may complete long after the inbound +request and webhook acknowledgement. + +Expose correlated lifecycle events for candidate pending, +authorization-required, rejected, failed, timed-out, and stale outcomes, plus +terminal request settlement. Include only safe presentation fields on the +public stream. Slack, `eve/client`, custom channels, hooks, and subagent proxies +consume the same lifecycle. A future immediate command-result API may add +correlation convenience without replacing stream-event authority. + +### Cancellation wire semantics + +Change the approval option ID directly from `"deny"` to `"cancel"`. Render and +persist Allow/Cancel consistently across the protocol, history, events, clients, +and channels. This is an intentional pre-1.0 breaking change; do not retain a +legacy `"deny"` fallback after migration. + +### github-tools first-pass eligibility scope + +Ship the repository-role policy as a showcase for a narrow allowlist of +repository-scoped operations whose authorization is honestly approximated by a +repository role, initially issue/comment/label-style actions. Default the +minimum GitHub permission to `write` and identify the human with the +user-scoped credential while reading policy and executing with the separate +agent credential. + +Do not apply this default policy to branch-sensitive, merge, protected-ref, +workflow-definition, repository-administration, repository-creation/fork, or +gist operations. Repository `write` does not imply authority over `main`, +CODEOWNER status, ruleset satisfaction, or branch-protection bypass. Preserve +GitHub's native enforcement against the agent GitHub App and document this as a +showcase repository-role check, not CODEOWNERS authorization. + +### Hosted provider identity evidence + +Extend eve's owned `TokenResult` with an optional `providerSubject`: the stable +subject for the provider account represented by the credential. The +`@vercel/connect/eve` adapter uses `getTokenResponse()` and maps Connect's +`externalSubject` into that eve-owned field without exposing the full Connect +response or third-party API. Self-hosted providers may return equivalent +subject evidence. + +github-tools may use this evidence as the stable GitHub account identifier and +fall back to GitHub `/user` only when a provider does not supply it. Token +ownership and identity evidence remain distinct from the app-scoped execution +credential. + +### Self-hosted provider responsibility + +Document the ordinary eve `ToolAuthProvider` contract rather than shipping a +GitHub OAuth helper or token store in the first pass. Self-hosted applications +may supply their own `defineInteractiveAuthorization` provider, OAuth client +configuration, durable token persistence, refresh/revocation behavior, and +optional stable provider subject. github-tools consumes the provider without +owning those concerns. + +Keep responder authorization opt-in. Applications that do not configure an +approver provider retain the existing request-time HITL behavior with no +response-time identity or eligibility check; do not make Connect or self-hosted +OAuth mandatory for existing github-tools users. + +### github-tools opt-in authoring shape + +Keep request-time approval and responder authorization as separate options. +`requireApproval` continues to control whether a tool call prompts, while an +optional `authorizeApprovalResponse` accepts a generic eve response authorizer +or per-tool map. Omitting it preserves existing request-time HITL semantics with +no responder identity or eligibility check. + +Provide composable helpers such as `githubRepositoryApprover({ auth, +agentToken, minimumPermission })` and `connectGithubApproverAuth(connector)`, but +do not infer response authorization from environment or token type. Once an +authorizer is configured for a request, provider/configuration failure is +fail-closed and never falls back to legacy settlement. Warn or reject when a +response authorizer is configured for tools whose request-time approval is +disabled. + +## Remaining open questions + +1. **Public API naming.** Finalize names for the object-form request policy, + response authorizer, candidate events, and terminal settlement event. + `TokenResult.providerSubject` is decided. +2. **Connection-tool lookup.** Specify the durable policy-required marker and + live definition resolver uniformly for authored, dynamic, framework, and + connection-derived tools so a missing authorizer always fails closed. +3. **Batch execution bridge.** Define the exact AI SDK/provider history shape + for per-request durable settlement while execution/model continuation stays + gated on the original batch. +4. **Candidate expiry scheduling.** Decide whether timeout events are emitted by + a scheduled durable wake-up or lazily when the request is next touched. The + 10-minute fallback semantics are fixed. +5. **Root projection schema.** Finalize which safe child candidate fields and + stream coordinates are copied into root durable history for observability + and UI repair. +6. **Showcase allowlist.** Finalize the exact github-tools operations covered by + the repository-`write` helper; issue/comment/label-style scope is decided, + but the concrete names need API review. + +## Implementation map and starting references + +### eve: public approval and response settlement + +| Area | Start here | Why it matters | +| ------------------------------------ | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Request-time approval API | `packages/eve/src/public/definitions/approval.ts` | `Approval`, `ApprovalContext`, and `ApprovalStatus` are the existing function-only request-time contract. Extend it without breaking function shorthand. | +| Tool public type and overloads | `packages/eve/src/public/definitions/tool.ts` | `ToolDefinition.approval`, `ToolContext`, and `defineTool()` overloads need the response-authorizer configuration and its narrower auth context. Connection definitions use the same `Approval` type. | +| Current irreversible HITL transition | `packages/eve/src/harness/input-requests.ts` | `resolvePendingInput()` presently records approved tools then clears the whole pending batch. This is the point that must validate a candidate response first. `PendingInputBatch`, `recordApprovedTools()`, `buildToolResponseParts()`, and `buildRejectedActionBatch()` define the state/history consequences to preserve. | +| Pending request schema | `packages/eve/src/runtime/input/types.ts` | Inspect `InputRequest` / `InputResponse` before deciding whether request metadata must be expanded or can be retained separately. | +| Runtime tool assembly | `packages/eve/src/harness/tools.ts` and `packages/eve/src/harness/tool-loop.ts` | These resolve authored tool/connection definitions, approval keys, and tool execution. They are the likely bridge from a durable request back to the current response-authorizer callback after restart/redeploy. | + +### eve: reuse—not duplicate—the authorization challenge machinery + +| Area | Start here | Why it matters | +| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Auth-capable tool wrapper | `packages/eve/src/execution/tool-auth.ts` | `createToolExecuteWithAuth()` builds `ToolContext.getToken()` / `requireAuth()`, catches authorization-required failures, and returns a park signal. The response-authorizer needs a deliberately narrower analogue, not a second OAuth implementation. | +| Shared token/cache/principal/challenge mechanics | `packages/eve/src/runtime/connections/scoped-authorization.ts` | `resolveScopedToken()`, `startScopedAuthorization()`, and `completeScopedAuthorization()` already bind a user principal, cache by `(scope, principal)`, create a callback challenge, and complete it after resume. Reuse these semantics while binding the challenge to the approval candidate. | +| Authorization signal and durable callback model | `packages/eve/src/harness/authorization.ts` | `requestAuthorization()`, `getAuthorizationResult()`, and `getHookUrl()` define the framework signal. Its header explains what survives an OAuth park and why Connect strategies do not need to journal PKCE state. | +| Authorization persistence / resume | `packages/eve/src/harness/authorization.ts`, `packages/eve/src/execution/workflow-entry.ts`, and `packages/eve/src/execution/turn-workflow.ts` | Follow the existing `setPendingAuthorization` / callback-resume path before adding a candidate-response state machine. Search these files for `PendingAuthorization` and `authorization.required`. | +| Self-hosted provider contract | `docs/connections/overview.mdx` under **Self-hosted interactive OAuth**, plus `packages/eve/src/runtime/connections/types.ts` | `defineInteractiveAuthorization` documents the host-owned token lookup, PKCE/start, and code exchange contract. Eve owns callback/park/resume; do not add a second self-hosted OAuth system. | +| Hosted Connect semantics | `docs/guides/auth-and-route-protection.md` under **Tool and connection auth**, and `docs/connections/overview.mdx` | Documents `ctx.getToken(connect(...))`, `authorization.required`, user vs app principals, and the current order of approval then sign-in that this feature must change for approver validation. | + +### eve: delivery, UI, and subagent paths + +| Area | Start here | Why it matters | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Slack response ingress | `packages/eve/src/public/channels/slack/interactions.ts` | Signed Slack actions are converted to `{ inputResponses }` plus `buildSlackAuthContext(...)`. It currently schedules `updateAnsweredHitlCard()` immediately; response acceptance must drive that update instead. | +| Slack HITL rendering | `packages/eve/src/public/channels/slack/hitl.ts` and `packages/eve/src/public/channels/slack/defaults.ts` | Native cards, request IDs, and default `input.requested` rendering. Preserve cards after a rejected candidate. | +| Channel delivery contract | `packages/eve/src/channel/types.ts` | `DeliverHookPayload` carries delivery-time auth through the durable hook. Any accepted/rejected/stale outcome should be designed alongside this contract. | +| HTTP/client path | `packages/eve/src/public/channels/eve.ts`, `packages/eve/src/client/session.ts`, and `docs/guides/client/messages.mdx` | `inputResponses` are accepted through the default HTTP channel/client. Add equivalent structured feedback rather than relying on Slack ephemerals. | +| Subagent routing | `packages/eve/src/execution/subagent-hitl-proxy.ts` and `packages/eve/src/harness/proxy-input-requests.ts` | Input response IDs are routed from the root channel to a parked child. Preserve responder auth and candidate/challenge ownership through this proxy. | + +### `github-tools`: current credential and approval integration + +The checked-out research source is `/var/folders/yh/8gqnf245537683p2rnghc2rh0000gn/T/tmp.F6f7RJ9E4d/github-tools`; obtain a fresh clone of `vercel-labs/github-tools` before implementation. + +| Area | Start here | Why it matters | +| ----------------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Existing Connect token helper | `packages/github-tools/src/connect/token.ts` | `connectGithubToken()` calls plain `@vercel/connect.getToken()`. It is not an eve auth provider and cannot emit `authorization.required`. | +| Why it is app-scoped | `packages/github-tools/src/connect/params.ts` and `connect/types.ts` | `buildConnectTokenParams()` pins `subject: { type: "app" }`; this is the agent installation credential, not the approver credential. | +| Eve tool construction | `packages/github-tools/src/eve/build.ts` | Builds each `defineTool()` with a request-time `approval` and an execute closure that uses the pre-resolved token resolver. Its `TODO(eve-auth)` explicitly notes the missing eve-managed auth integration. | +| Approval mapping | `packages/github-tools/src/eve/approval.ts` and `eve/types.ts` | Maps `requireApproval` and overrides to eve's current request-time `Approval`; extend this mapping for the GitHub approver policy without disrupting existing users. | +| Agent token fallback | `packages/github-tools/src/core/token.ts` | Self-hosted execution currently resolves `token` / `GITHUB_TOKEN` or an async agent-token supplier, with no session/principal input. Keep this behavior for the write credential. | +| Public hosted setup docs | `apps/docs/content/docs/3.guide/4.tokens-and-auth.md` and `packages/github-tools-eve-extension/README.md` | Documents Connect as a Vercel-hosted installation-token path and static token fallback; update the description when adding responder identity auth. | + +### Connect / Connex evidence for the hosted user OAuth path + +These source files are in `/Users/ben/repos/api/wt1`; they establish that the +native GitHub Connect connector supports both credentials needed by the hosted +PoC. + +| Area | Start here | Why it matters | +| --------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| GitHub driver subject modes | `packages/connex/src/client-types/github/client-driver.ts` | Declares `supportedSubjectTypes: ["user", "app"]`. `resolveAppToken()` mints the installation token; `getAuthorizationUrl()` and `completeAuthorization()` implement GitHub user OAuth; `getUserInfo()` calls `/user` and returns numeric GitHub ID as `sub`. | +| Connect token response | `node_modules/@vercel/connect/dist/token.d.ts` | `getTokenResponse()` returns `externalSubject`; `getToken()` discards it. Use the former for approver identity. | +| Connex returned identity | `packages/connex/src/connex-token.ts` | `externalSubject` is read from stored authorization/token metadata and returned with a token. `getConnexTokenInfo(..., { fetchUserInfo: true })` exposes OIDC-style user info. | +| Hosted OAuth behavior docs | `apps/connex-docs/docs/dataflows-oauthgateway.md` | Explicitly documents GitHub user ID as provider `sub`; useful confirmation of stable identity semantics. | + +## Future seam, deliberately not a v1 feature + +The eve response-authorizer contract is the two-way door. A later real-world +enterprise integration can provide its own evidence/eligibility decision at +that boundary. Do not publish a generic identity-directory framework until a +concrete integration establishes the required lifecycle and data model. From eb05158e815afc859fc70f6d8cd4011fc7bf33ad Mon Sep 17 00:00:00 2001 From: benpankow Date: Wed, 29 Jul 2026 11:34:30 -0700 Subject: [PATCH 2/2] feat(eve): replace approval denial with cancel Signed-off-by: benpankow --- .changeset/approval-allow-cancel.md | 5 ++ docs/concepts/sessions-runs-and-streaming.md | 2 +- docs/guides/frontend/overview.mdx | 2 +- docs/tools/human-in-the-loop.md | 2 +- docs/tutorial/ship-it.mdx | 2 +- packages/eve/src/channel/resolve-text.test.ts | 2 +- packages/eve/src/cli/dev/tui/runner.test.ts | 2 +- packages/eve/src/cli/dev/tui/runner.ts | 2 +- .../eve/src/client/message-reducer.test.ts | 14 +++--- .../eve/src/evals/runner/execute-task.test.ts | 4 +- .../src/execution/subagent-adapter.test.ts | 2 +- .../subagent-hitl-proxy.integration.test.ts | 6 +-- .../src/execution/subagent-hitl-proxy.test.ts | 4 +- .../eve/src/execution/workflow-steps.test.ts | 2 +- .../eve/src/harness/input-extraction.test.ts | 14 +++--- packages/eve/src/harness/input-extraction.ts | 4 +- .../eve/src/harness/input-requests.test.ts | 28 +++++------ packages/eve/src/harness/input-requests.ts | 8 ++-- .../src/harness/stale-input-responses.test.ts | 4 +- ...nerate-approval-resume.integration.test.ts | 2 +- packages/eve/src/harness/tool-loop.test.ts | 32 ++++++------- .../channels/chat-sdk/chatSdkChannel.test.ts | 8 ++-- .../src/public/channels/discord/hitl.test.ts | 2 +- packages/eve/src/public/channels/eve.test.ts | 6 +-- .../src/public/channels/linear/hitl.test.ts | 8 ++-- .../src/public/channels/slack/blocks.test.ts | 6 +-- .../src/public/channels/slack/hitl.test.ts | 28 +++++------ .../eve/src/public/channels/slack/hitl.ts | 14 +++--- .../channels/slack/slackChannel.test.ts | 48 +++++++++---------- .../src/public/channels/teams/hitl.test.ts | 6 +-- .../eve/src/public/channels/teams/hitl.ts | 2 +- .../channels/teams/teamsChannel.test.ts | 4 +- .../src/public/channels/telegram/hitl.test.ts | 4 +- packages/eve/src/react/use-eve-agent.test.ts | 2 +- packages/eve/src/runtime/input/types.test.ts | 2 +- packages/eve/src/runtime/input/types.ts | 2 +- packages/eve/src/vue/use-eve-agent.test.ts | 2 +- 37 files changed, 146 insertions(+), 141 deletions(-) create mode 100644 .changeset/approval-allow-cancel.md diff --git a/.changeset/approval-allow-cancel.md b/.changeset/approval-allow-cancel.md new file mode 100644 index 000000000..30de64ad4 --- /dev/null +++ b/.changeset/approval-allow-cancel.md @@ -0,0 +1,5 @@ +--- +"eve": minor +--- + +Tool approval responses now use `cancel` instead of `deny`, while retaining `approve` for the positive response, aligning the public protocol with the user-facing flow-control semantics. diff --git a/docs/concepts/sessions-runs-and-streaming.md b/docs/concepts/sessions-runs-and-streaming.md index 6e2a9fbb7..99fa238c3 100644 --- a/docs/concepts/sessions-runs-and-streaming.md +++ b/docs/concepts/sessions-runs-and-streaming.md @@ -85,7 +85,7 @@ curl -X POST http://127.0.0.1:2000/eve/v1/session/ \ The follow-up reuses the same durable session: same history, same state. -If the session is waiting on a human-in-the-loop approval, a matching text reply such as `approve` or `deny` answers the approval. Other follow-up text is held until the approval is answered, so an unrelated message does not implicitly deny the pending tool call. +If the session is waiting on a human-in-the-loop approval, a matching text reply such as `approve` or `cancel` answers the approval. Other follow-up text is held until the approval is answered, so an unrelated message does not implicitly deny the pending tool call. If the session is waiting on `ask_question`, a follow-up message clears that pending request before the model continues. An exact option match or permitted freeform response answers the question; any other message marks the question unanswered and starts the follow-up turn. diff --git a/docs/guides/frontend/overview.mdx b/docs/guides/frontend/overview.mdx index 06ee672bb..ff55c57c9 100644 --- a/docs/guides/frontend/overview.mdx +++ b/docs/guides/frontend/overview.mdx @@ -123,7 +123,7 @@ if (request) { } ``` -`request.prompt` and `request.options` give you what you need to render the approve and deny UI. The default reducer marks the part as responded immediately, then updates it again once eve streams the resumed result. +`request.prompt` and `request.options` give you what you need to render the approve and cancel UI. The default reducer marks the part as responded immediately, then updates it again once eve streams the resumed result. ## Authorization prompts diff --git a/docs/tools/human-in-the-loop.md b/docs/tools/human-in-the-loop.md index 3d0e1db4f..01e008f2a 100644 --- a/docs/tools/human-in-the-loop.md +++ b/docs/tools/human-in-the-loop.md @@ -100,7 +100,7 @@ Approvals and questions share one protocol: 1. The model requests input (an approval, or an `ask_question`). 2. eve emits an `input.requested` stream event carrying the pending requests. 3. The turn parks at `session.waiting`, durably, for as long as it takes. -4. The client answers with `inputResponses` (structured, keyed by `requestId`) or a normal follow-up `message`. A follow-up whose text matches an option ID, option label, or numeric option index resolves automatically, including approval options such as `approve` and `deny`. +4. The client answers with `inputResponses` (structured, keyed by `requestId`) or a normal follow-up `message`. A follow-up whose text matches an option ID, option label, or numeric option index resolves automatically, including approval options such as `approve` and `cancel`. The run picks back up exactly where it parked. Because the pause is durable, nothing is held in memory while it waits — the process can restart and the parked turn survives. diff --git a/docs/tutorial/ship-it.mdx b/docs/tutorial/ship-it.mdx index 2db857152..cd97365a1 100644 --- a/docs/tutorial/ship-it.mdx +++ b/docs/tutorial/ship-it.mdx @@ -73,7 +73,7 @@ export default function Page() { } ``` -`agent.data.messages` and `agent.status` cover most chat UIs. The hook also surfaces HITL prompts (the spend approval from [Step 8](./guard-the-spend)), so the dashboard can render approve/deny controls. For the full API, see [Frontend](../guides/frontend/overview). +`agent.data.messages` and `agent.status` cover most chat UIs. The hook also surfaces HITL prompts (the spend approval from [Step 8](./guard-the-spend)), so the dashboard can render approve/cancel controls. For the full API, see [Frontend](../guides/frontend/overview). ## Replace `placeholderAuth` diff --git a/packages/eve/src/channel/resolve-text.test.ts b/packages/eve/src/channel/resolve-text.test.ts index efafab930..0c5ebc507 100644 --- a/packages/eve/src/channel/resolve-text.test.ts +++ b/packages/eve/src/channel/resolve-text.test.ts @@ -8,7 +8,7 @@ const APPROVAL_REQUEST: InputRequest = { display: "confirmation", options: [ { id: "approve", label: "Approve", style: "primary" }, - { id: "deny", label: "Deny", style: "danger" }, + { id: "cancel", label: "Cancel", style: "danger" }, ], prompt: 'Approve tool "bash"?', requestId: "req-1", diff --git a/packages/eve/src/cli/dev/tui/runner.test.ts b/packages/eve/src/cli/dev/tui/runner.test.ts index 2a82253fc..d86b63123 100644 --- a/packages/eve/src/cli/dev/tui/runner.test.ts +++ b/packages/eve/src/cli/dev/tui/runner.test.ts @@ -905,7 +905,7 @@ describe("EveTUIRunner native continuation state", () => { display: "confirmation", options: [ { id: "approve", label: "Approve" }, - { id: "deny", label: "Deny" }, + { id: "cancel", label: "Cancel" }, ], prompt: "Approve get_weather?", requestId: "request-1", diff --git a/packages/eve/src/cli/dev/tui/runner.ts b/packages/eve/src/cli/dev/tui/runner.ts index 851402668..d52557a50 100644 --- a/packages/eve/src/cli/dev/tui/runner.ts +++ b/packages/eve/src/cli/dev/tui/runner.ts @@ -868,7 +868,7 @@ export class EveTUIRunner { const response = await this.#renderer.readToolApproval(request, { title }); responses.push({ requestId: request.approvalId, - optionId: response.approved ? "approve" : "deny", + optionId: response.approved ? "approve" : "cancel", }); this.#pendingInputRequests.delete(request.approvalId); } diff --git a/packages/eve/src/client/message-reducer.test.ts b/packages/eve/src/client/message-reducer.test.ts index 1bde0298f..1adf4f961 100644 --- a/packages/eve/src/client/message-reducer.test.ts +++ b/packages/eve/src/client/message-reducer.test.ts @@ -373,7 +373,7 @@ describe("defaultMessageReducer", () => { display: "confirmation", options: [ { id: "approve", label: "Yes", style: "primary" }, - { id: "deny", label: "No", style: "danger" }, + { id: "cancel", label: "No", style: "danger" }, ], prompt: "Approve tool call: bash", requestId: "approval_1", @@ -409,7 +409,7 @@ describe("defaultMessageReducer", () => { display: "confirmation", options: [ { id: "approve", label: "Yes", style: "primary" }, - { id: "deny", label: "No", style: "danger" }, + { id: "cancel", label: "No", style: "danger" }, ], prompt: "Approve tool call: bash", requestId: "approval_1", @@ -443,7 +443,7 @@ describe("defaultMessageReducer", () => { display: "confirmation", options: [ { id: "approve", label: "Yes", style: "primary" }, - { id: "deny", label: "No", style: "danger" }, + { id: "cancel", label: "No", style: "danger" }, ], prompt: "Approve tool call: bash", requestId: "approval_1", @@ -458,7 +458,7 @@ describe("defaultMessageReducer", () => { data = reducer.reduce(data, { data: { createdAt: 1, - responses: [{ optionId: "deny", requestId: "approval_1" }], + responses: [{ optionId: "cancel", requestId: "approval_1" }], }, type: "client.input.responded", }); @@ -487,12 +487,12 @@ describe("defaultMessageReducer", () => { display: "confirmation", options: [ { id: "approve", label: "Yes", style: "primary" }, - { id: "deny", label: "No", style: "danger" }, + { id: "cancel", label: "No", style: "danger" }, ], prompt: "Approve tool call: bash", requestId: "approval_1", }, - inputResponse: { optionId: "deny", requestId: "approval_1" }, + inputResponse: { optionId: "cancel", requestId: "approval_1" }, kind: "tool-call", name: "bash", }, @@ -522,7 +522,7 @@ describe("defaultMessageReducer", () => { display: "confirmation", options: [ { id: "approve", label: "Yes", style: "primary" }, - { id: "deny", label: "No", style: "danger" }, + { id: "cancel", label: "No", style: "danger" }, ], prompt: "Approve tool call: bash", requestId: "approval_1", diff --git a/packages/eve/src/evals/runner/execute-task.test.ts b/packages/eve/src/evals/runner/execute-task.test.ts index 11a7f281c..4aead71c4 100644 --- a/packages/eve/src/evals/runner/execute-task.test.ts +++ b/packages/eve/src/evals/runner/execute-task.test.ts @@ -81,7 +81,7 @@ describe("executeTask", () => { const request = t.requireInputRequest({ display: "confirmation", input: { command: "pwd" }, - optionIds: ["approve", "deny"], + optionIds: ["approve", "cancel"], prompt: /Approve/, toolName: "bash", }); @@ -720,7 +720,7 @@ function inputRequested( display: "confirmation", options: [ { id: "approve", label: "Approve" }, - { id: "deny", label: "Deny" }, + { id: "cancel", label: "Cancel" }, ], prompt: "Approve?", requestId, diff --git a/packages/eve/src/execution/subagent-adapter.test.ts b/packages/eve/src/execution/subagent-adapter.test.ts index 9daa0f835..7e5752beb 100644 --- a/packages/eve/src/execution/subagent-adapter.test.ts +++ b/packages/eve/src/execution/subagent-adapter.test.ts @@ -55,7 +55,7 @@ function sampleRequest(): InputRequest { }, options: [ { id: "approve", label: "Approve" }, - { id: "deny", label: "Deny" }, + { id: "cancel", label: "Cancel" }, ], prompt: "Approve?", requestId: "req-1", diff --git a/packages/eve/src/execution/subagent-hitl-proxy.integration.test.ts b/packages/eve/src/execution/subagent-hitl-proxy.integration.test.ts index 95b759c6e..ec67ec7d0 100644 --- a/packages/eve/src/execution/subagent-hitl-proxy.integration.test.ts +++ b/packages/eve/src/execution/subagent-hitl-proxy.integration.test.ts @@ -142,7 +142,7 @@ function buildApprovalRequest(requestId: string): InputRequest { display: "confirmation", options: [ { id: "approve", label: "Approve", style: "primary" }, - { id: "deny", label: "Deny", style: "danger" }, + { id: "cancel", label: "Cancel", style: "danger" }, ], prompt: "Approve?", requestId, @@ -421,7 +421,7 @@ describe("subagent HITL proxy → concurrent-descendant routing", () => { payload: { inputResponses: [ { optionId: "approve", requestId: "req-a" }, - { optionId: "deny", requestId: "req-b" }, + { optionId: "cancel", requestId: "req-b" }, ], }, state: parkedSession.state, @@ -440,7 +440,7 @@ describe("subagent HITL proxy → concurrent-descendant routing", () => { inputResponses: [{ optionId: "approve", requestId: "req-a" }], }); expect(byChild.get("subagent:parent:call-b")).toEqual({ - inputResponses: [{ optionId: "deny", requestId: "req-b" }], + inputResponses: [{ optionId: "cancel", requestId: "req-b" }], }); // A response whose requestId does not match any proxy entry diff --git a/packages/eve/src/execution/subagent-hitl-proxy.test.ts b/packages/eve/src/execution/subagent-hitl-proxy.test.ts index 0421a9ed4..ea207f6eb 100644 --- a/packages/eve/src/execution/subagent-hitl-proxy.test.ts +++ b/packages/eve/src/execution/subagent-hitl-proxy.test.ts @@ -35,7 +35,7 @@ describe("routeDeliverPayload", () => { payload: { inputResponses: [ { optionId: "approve", requestId: "req-a" }, - { optionId: "deny", requestId: "req-b" }, + { optionId: "cancel", requestId: "req-b" }, { optionId: "ignore", requestId: "req-parent" }, ], }, @@ -46,7 +46,7 @@ describe("routeDeliverPayload", () => { const childA = routed.forChildren.find((c) => c.childContinuationToken === "child-a"); const childB = routed.forChildren.find((c) => c.childContinuationToken === "child-b"); expect(childA?.payload.inputResponses).toEqual([{ optionId: "approve", requestId: "req-a" }]); - expect(childB?.payload.inputResponses).toEqual([{ optionId: "deny", requestId: "req-b" }]); + expect(childB?.payload.inputResponses).toEqual([{ optionId: "cancel", requestId: "req-b" }]); expect(routed.forSelf?.inputResponses).toEqual([ { optionId: "ignore", requestId: "req-parent" }, diff --git a/packages/eve/src/execution/workflow-steps.test.ts b/packages/eve/src/execution/workflow-steps.test.ts index ed804a817..9668410c8 100644 --- a/packages/eve/src/execution/workflow-steps.test.ts +++ b/packages/eve/src/execution/workflow-steps.test.ts @@ -1484,7 +1484,7 @@ describe("runProxySubagentEventStep", () => { }, options: [ { id: "approve", label: "Approve" }, - { id: "deny", label: "Deny" }, + { id: "cancel", label: "Cancel" }, ], prompt: "Approve?", requestId: "req-1", diff --git a/packages/eve/src/harness/input-extraction.test.ts b/packages/eve/src/harness/input-extraction.test.ts index c818179de..cfe4ff35f 100644 --- a/packages/eve/src/harness/input-extraction.test.ts +++ b/packages/eve/src/harness/input-extraction.test.ts @@ -12,7 +12,7 @@ describe("extractQuestionInputRequests", () => { toolCalls: [ { input: { - options: [{ id: "yes", label: "Yes" }], + options: [{ id: "yes", label: "Approve" }], prompt: "Continue?", }, toolCallId: "call-1", @@ -27,14 +27,14 @@ describe("extractQuestionInputRequests", () => { action: { callId: "call-1", input: { - options: [{ id: "yes", label: "Yes" }], + options: [{ id: "yes", label: "Approve" }], prompt: "Continue?", }, kind: "tool-call", toolName: "ask_question", }, display: "select", - options: [{ id: "yes", label: "Yes" }], + options: [{ id: "yes", label: "Approve" }], prompt: "Continue?", requestId: "call-1", }, @@ -118,8 +118,8 @@ describe("extractToolApprovalInputRequests", () => { allowFreeform: false, display: "confirmation", options: [ - { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "approve", label: "Approve" }, + { id: "cancel", label: "Cancel" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -155,8 +155,8 @@ describe("extractToolApprovalInputRequests", () => { allowFreeform: false, display: "confirmation", options: [ - { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "approve", label: "Approve" }, + { id: "cancel", label: "Cancel" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", diff --git a/packages/eve/src/harness/input-extraction.ts b/packages/eve/src/harness/input-extraction.ts index eb1cedc0b..f151e8ff4 100644 --- a/packages/eve/src/harness/input-extraction.ts +++ b/packages/eve/src/harness/input-extraction.ts @@ -157,8 +157,8 @@ function extractApprovalRequests(input: { allowFreeform: false, display: "confirmation", options: [ - { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "approve", label: "Approve" }, + { id: "cancel", label: "Cancel" }, ], prompt: `Approve tool call: ${toolCall.toolName}`, requestId: approval.approvalId, diff --git a/packages/eve/src/harness/input-requests.test.ts b/packages/eve/src/harness/input-requests.test.ts index 65fc4586c..246b5ae79 100644 --- a/packages/eve/src/harness/input-requests.test.ts +++ b/packages/eve/src/harness/input-requests.test.ts @@ -142,7 +142,7 @@ describe("resolvePendingInput", () => { display: "confirmation", options: [ { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "cancel", label: "No" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -266,7 +266,7 @@ describe("resolvePendingInput", () => { display: "confirmation", options: [ { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "cancel", label: "No" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -296,7 +296,7 @@ describe("resolvePendingInput", () => { // Deliver an approval response AND a message simultaneously. const result = resolvePendingInput({ stepInput: { - inputResponses: [{ requestId: "approval-1", optionId: "deny" }], + inputResponses: [{ requestId: "approval-1", optionId: "cancel" }], message: "Ignore that and say hi instead.", }, session, @@ -333,7 +333,7 @@ describe("resolvePendingInput", () => { display: "confirmation", options: [ { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "cancel", label: "No" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -392,7 +392,7 @@ describe("resolvePendingInput", () => { display: "confirmation", options: [ { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "cancel", label: "No" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -456,7 +456,7 @@ describe("resolvePendingInput", () => { display: "confirmation", options: [ { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "cancel", label: "No" }, ], prompt: "Approve tool call: vercel__list_projects", requestId: "approval-1", @@ -522,7 +522,7 @@ describe("resolvePendingInput", () => { display: "confirmation", options: [ { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "cancel", label: "No" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -551,7 +551,7 @@ describe("resolvePendingInput", () => { const result = resolvePendingInput({ stepInput: { - inputResponses: [{ requestId: "approval-1", optionId: "deny" }], + inputResponses: [{ requestId: "approval-1", optionId: "cancel" }], }, session, }); @@ -591,7 +591,7 @@ describe("resolvePendingInput", () => { display: "confirmation", options: [ { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "cancel", label: "No" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -603,7 +603,7 @@ describe("resolvePendingInput", () => { const result = resolvePendingInput({ stepInput: { - inputResponses: [{ requestId: "approval-1", optionId: "deny" }], + inputResponses: [{ requestId: "approval-1", optionId: "cancel" }], }, session, }); @@ -648,7 +648,7 @@ describe("resolvePendingInput", () => { display: "confirmation", options: [ { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "cancel", label: "No" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -684,7 +684,7 @@ describe("resolvePendingInput", () => { display: "confirmation", options: [ { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "cancel", label: "No" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -724,7 +724,7 @@ describe("resolvePendingInput", () => { display: "confirmation", options: [ { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "cancel", label: "No" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -783,7 +783,7 @@ describe("resolvePendingInput", () => { display: "confirmation", options: [ { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "cancel", label: "No" }, ], prompt: "Approve tool call: linear_whoami", requestId: "approval-1", diff --git a/packages/eve/src/harness/input-requests.ts b/packages/eve/src/harness/input-requests.ts index 7d412e222..8118fe91f 100644 --- a/packages/eve/src/harness/input-requests.ts +++ b/packages/eve/src/harness/input-requests.ts @@ -505,7 +505,7 @@ function resolveApprovalOutcome(response: InputResponse | undefined): { }; } - if (response.optionId === "deny") { + if (response.optionId === "cancel") { return { approved: false, reason: TOOL_EXECUTION_DENIED_MESSAGE, @@ -604,7 +604,7 @@ function buildToolResponsePartsForRequest( }, ]; /* - * On denial (explicit "deny" or auto-deny when the user continues + * On denial (explicit "cancel" or auto-deny when the user continues * without responding), splice in the matching `execution-denied` * tool-result. AI SDK's `streamText` synthesizes this for the * current turn's `initialResponseMessages`, but that synthesis is @@ -643,12 +643,12 @@ function buildToolResponsePartsForRequest( ]; } -/** Shared approval predicate: a request whose options are exactly `approve` / `deny`. */ +/** Shared approval predicate: a request whose options are exactly `allow` / `cancel`. */ export function isApprovalRequest(request: InputRequest): boolean { return ( request.options?.length === 2 && request.options[0]?.id === "approve" && - request.options[1]?.id === "deny" + request.options[1]?.id === "cancel" ); } diff --git a/packages/eve/src/harness/stale-input-responses.test.ts b/packages/eve/src/harness/stale-input-responses.test.ts index b0575e568..c51829025 100644 --- a/packages/eve/src/harness/stale-input-responses.test.ts +++ b/packages/eve/src/harness/stale-input-responses.test.ts @@ -61,10 +61,10 @@ it("converts a stale approval into a non-authorizing user message", () => { throw new Error("Expected the stale response to be converted."); } - expect(result.displayMessage).toBe("Yes"); + expect(result.displayMessage).toBe("Approve"); expect(result.stepInput.inputResponses).toBeUndefined(); expect(result.stepInput.message).toEqual(expect.stringContaining("Approve tool call: bash")); - expect(result.stepInput.message).toEqual(expect.stringContaining('"label": "Yes"')); + expect(result.stepInput.message).toEqual(expect.stringContaining('"label": "Approve"')); expect(result.stepInput.message).toEqual( expect.stringContaining("This does not authorize an earlier action"), ); diff --git a/packages/eve/src/harness/tool-loop-generate-approval-resume.integration.test.ts b/packages/eve/src/harness/tool-loop-generate-approval-resume.integration.test.ts index bae7a600a..965947322 100644 --- a/packages/eve/src/harness/tool-loop-generate-approval-resume.integration.test.ts +++ b/packages/eve/src/harness/tool-loop-generate-approval-resume.integration.test.ts @@ -65,7 +65,7 @@ function createPendingApprovalSession(): HarnessSession { display: "confirmation", options: [ { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "cancel", label: "No" }, ], prompt: "Approve tool call: bash", requestId: approvalRequest.approvalId, diff --git a/packages/eve/src/harness/tool-loop.test.ts b/packages/eve/src/harness/tool-loop.test.ts index 74ea2c3ed..9d8f2339d 100644 --- a/packages/eve/src/harness/tool-loop.test.ts +++ b/packages/eve/src/harness/tool-loop.test.ts @@ -428,8 +428,8 @@ function createPendingBashApprovalSession(): HarnessSession { allowFreeform: false, display: "confirmation", options: [ - { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "approve", label: "Approve" }, + { id: "cancel", label: "Cancel" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -478,8 +478,8 @@ function createPendingProtectedActionApprovalSession(): HarnessSession { allowFreeform: false, display: "confirmation", options: [ - { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "approve", label: "Approve" }, + { id: "cancel", label: "Cancel" }, ], prompt: "Approve tool call: protected_action", requestId: "approval-1", @@ -2283,8 +2283,8 @@ describe("createToolLoopHarness", () => { allowFreeform: false, display: "confirmation", options: [ - { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "approve", label: "Approve" }, + { id: "cancel", label: "Cancel" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -6401,8 +6401,8 @@ describe("createToolLoopHarness", () => { allowFreeform: false, display: "confirmation", options: [ - { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "approve", label: "Approve" }, + { id: "cancel", label: "Cancel" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -6588,8 +6588,8 @@ describe("createToolLoopHarness", () => { allowFreeform: false, display: "confirmation", options: [ - { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "approve", label: "Approve" }, + { id: "cancel", label: "Cancel" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -6646,7 +6646,7 @@ describe("createToolLoopHarness", () => { expect(hasDeferredStepInput(firstResult.session)).toBe(true); const deniedResult = await createToolLoopHarness(config)(firstResult.session, { - inputResponses: [{ requestId: "approval-1", optionId: "deny" }], + inputResponses: [{ requestId: "approval-1", optionId: "cancel" }], }); expect(typeof deniedResult.next).toBe("function"); @@ -6752,8 +6752,8 @@ describe("createToolLoopHarness", () => { allowFreeform: false, display: "confirmation", options: [ - { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "approve", label: "Approve" }, + { id: "cancel", label: "Cancel" }, ], prompt: "Approve tool call: guarded_echo", requestId: "approval-1", @@ -6999,8 +6999,8 @@ describe("createToolLoopHarness", () => { allowFreeform: false, display: "confirmation", options: [ - { id: "approve", label: "Yes" }, - { id: "deny", label: "No" }, + { id: "approve", label: "Approve" }, + { id: "cancel", label: "Cancel" }, ], prompt: "Approve tool call: bash", requestId: "approval-1", @@ -7059,7 +7059,7 @@ describe("createToolLoopHarness", () => { // Step 2: user denies the approval; the deferred message is NOT in this call. const deniedResult = await createToolLoopHarness(config)(firstResult.session, { - inputResponses: [{ requestId: "approval-1", optionId: "deny" }], + inputResponses: [{ requestId: "approval-1", optionId: "cancel" }], }); expect(typeof deniedResult.next).toBe("function"); const step2Last = generateCalls[0]?.at(-1); diff --git a/packages/eve/src/public/channels/chat-sdk/chatSdkChannel.test.ts b/packages/eve/src/public/channels/chat-sdk/chatSdkChannel.test.ts index 8fde03791..028142f9e 100644 --- a/packages/eve/src/public/channels/chat-sdk/chatSdkChannel.test.ts +++ b/packages/eve/src/public/channels/chat-sdk/chatSdkChannel.test.ts @@ -507,7 +507,7 @@ describe("chatSdkChannel", () => { display: "confirmation", options: [ { id: "approve", label: "Approve", style: "primary" }, - { id: "deny", label: "Deny", style: "danger" }, + { id: "cancel", label: "Cancel", style: "danger" }, ], prompt: "Deploy?", requestId: "request-1", @@ -534,11 +534,11 @@ describe("chatSdkChannel", () => { value: "approve", }, { - id: "eve_input:request-1:deny", - label: "Deny", + id: "eve_input:request-1:cancel", + label: "Cancel", style: "danger", type: "button", - value: "deny", + value: "cancel", }, ], type: "actions", diff --git a/packages/eve/src/public/channels/discord/hitl.test.ts b/packages/eve/src/public/channels/discord/hitl.test.ts index aa63b35e3..4a9ed7d42 100644 --- a/packages/eve/src/public/channels/discord/hitl.test.ts +++ b/packages/eve/src/public/channels/discord/hitl.test.ts @@ -42,7 +42,7 @@ describe("renderInputRequestComponents", () => { display: "confirmation", options: [ { id: "approve", label: "Approve", style: "primary" }, - { id: "deny", label: "Deny", style: "danger" }, + { id: "cancel", label: "Cancel", style: "danger" }, ], }), ); diff --git a/packages/eve/src/public/channels/eve.test.ts b/packages/eve/src/public/channels/eve.test.ts index 909fcf753..9beffca28 100644 --- a/packages/eve/src/public/channels/eve.test.ts +++ b/packages/eve/src/public/channels/eve.test.ts @@ -642,7 +642,7 @@ describe("eveChannel — onMessage", () => { const response = await handler.fetch( createJsonMessageRequest({ continuationToken: "http:existing", - inputResponses: [{ requestId: "req-1", optionId: "deny" }], + inputResponses: [{ requestId: "req-1", optionId: "cancel" }], }), ); @@ -1284,7 +1284,7 @@ describe("eveChannel — continue session HITL (inputResponses)", () => { const response = await handler.fetch( createJsonMessageRequest({ continuationToken: "http:existing", - inputResponses: [{ requestId: "req-1", optionId: "deny" }], + inputResponses: [{ requestId: "req-1", optionId: "cancel" }], }), ); @@ -1292,7 +1292,7 @@ describe("eveChannel — continue session HITL (inputResponses)", () => { expect(handler.send).toHaveBeenCalledTimes(1); const payload = handler.send.mock.calls[0]?.[0] as SendPayload; expect(payload.message).toBeUndefined(); - expect(payload.inputResponses).toEqual([{ requestId: "req-1", optionId: "deny" }]); + expect(payload.inputResponses).toEqual([{ requestId: "req-1", optionId: "cancel" }]); }); }); diff --git a/packages/eve/src/public/channels/linear/hitl.test.ts b/packages/eve/src/public/channels/linear/hitl.test.ts index bf9e12e89..b645e0693 100644 --- a/packages/eve/src/public/channels/linear/hitl.test.ts +++ b/packages/eve/src/public/channels/linear/hitl.test.ts @@ -21,14 +21,14 @@ describe("Linear HITL helpers", () => { makeRequest({ options: [ { id: "approve", label: "Approve" }, - { id: "deny", label: "Deny", description: "Stop the deployment" }, + { id: "cancel", label: "Cancel", description: "Stop the deployment" }, ], }), ]); expect(rendered).toContain("Approve deployment?"); expect(rendered).toContain("1. Approve"); - expect(rendered).toContain("2. Deny - Stop the deployment"); + expect(rendered).toContain("2. Cancel - Stop the deployment"); expect(rendered).not.toContain("eve-input"); expect(rendered).not.toContain("