From 883ba4d8b251f22107b240438bb3c73f78f4b014 Mon Sep 17 00:00:00 2001 From: Jonathan Haas Date: Sat, 11 Jul 2026 06:48:09 -0700 Subject: [PATCH 1/5] design company relay architecture --- .../specs/2026-07-11-company-relay-design.md | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-11-company-relay-design.md diff --git a/docs/superpowers/specs/2026-07-11-company-relay-design.md b/docs/superpowers/specs/2026-07-11-company-relay-design.md new file mode 100644 index 0000000..653fcf2 --- /dev/null +++ b/docs/superpowers/specs/2026-07-11-company-relay-design.md @@ -0,0 +1,130 @@ +# Pigeon Company Relay Design + +## Purpose + +Replace Pigeon's process-local evaluation relay with a durable company service that connects employees' Codex installations. Slack is the human notification and low-risk approval surface; Codex remains the only surface that can authorize workspace reads or writes and execute delegated work. + +The first production deployment targets one company Slack workspace, one Pigeon organization, and employee-managed Codex desktop installations. Multi-tenant hosting, billing, cross-company delegation, and unattended execution are outside this design. + +## Authorization boundary + +Pigeon supports three ordered scopes: + +1. `discuss_only`: no workspace access. The recipient may approve or reject in Slack or Codex. +2. `read_only`: workspace inspection and safe diagnostics. Slack can display and deep-link the request, but approval requires native confirmation in the recipient's Codex. +3. `workspace_write`: edits within one approved workspace root. Approval requires native confirmation in the recipient's Codex. + +A recipient may narrow a request but never widen it. Company policy may force Codex confirmation for any scope, repository, team, or data classification. Slack never receives a capability that can authorize filesystem access. A Slack approval for `discuss_only` creates only a discussion capability. + +## Architecture + +The system has four bounded components: + +- **Relay API:** authenticates clients, validates commands, stores durable state, issues short-lived capabilities, and exposes HTTPS plus WebSocket or server-sent event streams. +- **Postgres:** stores organizations, identities, linked Slack accounts, device registrations, delegations, append-only events, approvals, capabilities, and delivery cursors. +- **Slack app:** posts App Home and DM notifications, handles interactive low-risk decisions, and creates links that open the corresponding request in Codex. +- **Pigeon Codex gateway:** runs on the employee's machine as the plugin MCP server, maintains an outbound authenticated relay connection, renders the inbox, performs native confirmation, maps effective scope to local Codex permissions, and executes through the Codex app-server adapter. + +The relay coordinates work but never receives OpenAI credentials, local file contents, chain-of-thought, or unrestricted command output. Execution remains local to the recipient's Codex environment. + +## Identity and device trust + +Company identity is anchored in Slack for the first release. Installation begins with a relay-generated device-link URL. The employee completes Slack OAuth, and the relay binds the Slack workspace ID and Slack user ID to a Pigeon user. Email addresses are display metadata and are not identity keys. + +Each Codex installation creates a non-exportable device key when platform facilities permit, otherwise an encrypted local Ed25519 key. The relay registers the public key and returns a revocable device credential. All delegation commands are signed by the device and authenticated over TLS. An organization administrator can revoke a user or individual device. + +The relay accepts events only when organization, user, device, and Slack linkage are active. Slack request signatures are verified independently. OAuth tokens and device credentials are encrypted at rest with a managed KMS key. + +## Delegation and approval flow + +1. The sender's gateway submits a signed delegation containing recipient, objective, workspace hint, requested scope, expiry, and idempotency key. +2. The relay validates policy, stores the request and first event transactionally, and enqueues notification delivery. +3. The Slack app sends the recipient a DM and updates Pigeon App Home. The recipient's connected gateways receive the same event. +4. For `discuss_only`, Slack presents Approve, Reject, and Open in Codex. Approval records the Slack actor and issues a single-use discussion capability. +5. For `read_only` or `workspace_write`, Slack presents Reject and Open in Codex. The Codex inbox permits scope narrowing and requests native confirmation showing sender, objective, local workspace resolution, and effective authority. +6. The gateway exchanges the accepted native confirmation for a single-use execution lease. The lease is bound to delegation, recipient, device, resolved workspace root, effective scope, expiry, and repository revision when available. +7. The local adapter starts one Codex task. Progress is reduced to explicit status events and user-safe summaries before transmission. +8. The gateway records completion, failure, or cancellation. The relay updates Slack and the sender's Codex. + +The authoritative lifecycle is `pending -> approved -> running -> completed`, with `rejected`, `expired`, `cancelled`, and `failed` terminal alternatives. Every transition uses optimistic concurrency and an idempotency key. + +## Slack experience + +Pigeon uses DMs for actionable notifications and App Home for the durable inbox. Channel posting is optional and off by default to avoid leaking objectives or repository names. + +A request message shows sender, objective, requested scope, expiry, and a minimal workspace label. It does not show absolute local paths. Slack actions are handled asynchronously: the app acknowledges immediately, then the worker validates current state and updates the message. Stale or duplicate interactions return the current authoritative state. + +For elevated scopes, **Open in Codex** uses a signed, short-lived deep link containing only an opaque request ID. If deep linking is unavailable, the message instructs the recipient to open the Pigeon inbox in Codex. Slack rejection is allowed for every scope because it grants no authority. + +Thread replies are not treated as protocol commands. Discussion may occur in Slack, but decisions enter the system only through verified interactive actions or Codex tools. + +## Data model and retention + +Core records are `organizations`, `users`, `slack_links`, `devices`, `delegations`, `delegation_events`, `approvals`, `capabilities`, `delivery_attempts`, and `policy_rules`. + +`delegations` contains the current materialized state for efficient reads. `delegation_events` is append-only and contains actor, source surface, transition, timestamp, request correlation ID, and redacted metadata. Capabilities are stored as hashes, expire within minutes, and are marked consumed in the same transaction that advances state. + +Objectives and result summaries are retained for 30 days by default; audit metadata is retained for one year. Organizations can shorten both periods. Absolute workspace paths, file contents, prompts, tool transcripts, secrets, and model reasoning are never stored by the relay. Deletion removes message content while retaining minimal security audit facts where company policy requires them. + +## Reliability and operations + +The relay runs as at least two stateless application instances behind a load balancer. Postgres is the source of truth. A transactional outbox drives Slack and gateway delivery, avoiding a separate message broker initially. Workers retry transient failures with bounded exponential backoff and dead-letter persistent failures for operator review. + +Gateways reconnect using a persisted cursor and receive all events after their last acknowledged sequence. Event delivery is at least once; command effects are exactly once through idempotency and state-transition constraints. Only one active execution lease may exist per delegation. + +Operational signals include request latency, notification latency, connected devices, approval latency, execution starts, terminal outcomes, retry counts, Slack API errors, invalid signatures, rejected transitions, and capability replay attempts. Alerts cover sustained delivery failure, database saturation, signature anomalies, and growing outbox lag. + +## Security controls + +- TLS for every network connection and managed encryption at rest. +- Slack request verification, minimal OAuth scopes, token rotation, and immediate revocation handling. +- Signed device commands with timestamp and nonce replay protection. +- Short-lived, audience-bound, single-use capabilities stored only as hashes. +- Organization and recipient checks on every read and transition. +- Local workspace allowlists and canonical-path checks in the gateway. +- Policy enforcement both when a request is created and immediately before execution. +- Structured redaction at the gateway and relay; no raw model transcript ingestion. +- Rate limits per organization, sender, recipient, device, and Slack action. +- Append-only audit events exported to the company's security logging system. +- Administrative device revocation and an organization-wide execution kill switch. + +Compromise of Slack alone can approve only `discuss_only` requests. Compromise of the relay cannot access local workspaces without a recipient device, native confirmation for elevated scopes, and a valid execution lease. + +## Failure behavior + +If Slack is unavailable, requests remain visible and actionable in Codex. If a recipient is offline, the relay retains the request until expiry. If the relay connection drops during execution, the gateway continues only under its already-issued lease, buffers bounded status events, and reconciles when connectivity returns. + +Policy changes or device revocation invalidate unconsumed capabilities and prevent new execution leases. Cancellation interrupts the local Codex task when possible; late completion events are recorded for diagnosis but cannot move a cancelled delegation back to completed. Slack message update failures do not change authoritative state. + +## Deployment sequence + +1. Deploy a single-company staging relay, Postgres, Slack app, and transactional outbox worker. +2. Add device linking, identity mapping, durable event streaming, and App Home/DM notifications. +3. Enable Slack approval for `discuss_only` and Codex deep links for elevated requests. +4. Replace the fake adapter with the local Codex app-server adapter and enforce execution leases. +5. Pilot with one engineering team using `discuss_only` and `read_only` only. +6. Complete security review, audit export, revocation exercises, and recovery testing. +7. Enable `workspace_write` for explicitly allowed repositories and teams. + +## Testing and acceptance + +Unit tests cover policy evaluation, Slack signature verification, device signatures, scope narrowing, capability binding and consumption, state transitions, idempotency, expiry, redaction, and canonical workspace resolution. + +Integration tests run two isolated gateways against Postgres and the relay, with fake Slack and Codex adapters. They verify reconnect, duplicate delivery, concurrent approval, revocation, cancellation races, outbox retry, Slack-only discussion approval, and mandatory Codex confirmation for elevated scopes. + +End-to-end staging tests use two real Slack users and two Codex installations. The release is accepted when: + +- a sender can delegate to a linked teammate and both Slack and Codex receive the request; +- Slack can approve only `discuss_only` authority; +- elevated scopes require native Codex confirmation and can be narrowed; +- one approval starts at most one local task; +- offline recipients, reconnects, retries, expiry, cancellation, and revocation behave deterministically; +- no relay record or log contains local file contents, absolute workspace paths, secrets, prompts, or model reasoning; +- audit exports reconstruct every security-relevant transition; +- a Slack or relay compromise alone cannot authorize workspace access. + +## Initial technology choices + +Keep the current TypeScript codebase. Implement the relay as a small Node service with Postgres, HTTPS, and WebSocket or server-sent event endpoints. Use Slack's official SDK for OAuth, App Home, DMs, and interactive actions. Use a migration tool compatible with Postgres, a managed KMS for secrets, and the company's existing container platform and observability stack. + +Do not introduce Kafka, Redis, Kubernetes-specific operators, or a policy language in the first release. The Postgres outbox, explicit TypeScript policy functions, and existing deployment platform are sufficient until measured load or organizational complexity proves otherwise. From 64f11653a94c13de650daaefd1ed09ca02d3e677 Mon Sep 17 00:00:00 2001 From: Jonathan Haas Date: Sat, 11 Jul 2026 06:49:15 -0700 Subject: [PATCH 2/5] plan company relay implementation --- .../plans/2026-07-11-company-relay.md | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-11-company-relay.md diff --git a/docs/superpowers/plans/2026-07-11-company-relay.md b/docs/superpowers/plans/2026-07-11-company-relay.md new file mode 100644 index 0000000..a9cac67 --- /dev/null +++ b/docs/superpowers/plans/2026-07-11-company-relay.md @@ -0,0 +1,140 @@ +# Pigeon Company Relay Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship a deployable, Postgres-backed Pigeon relay with signed device authentication, Slack notifications and low-risk approvals, and a Codex gateway client for durable cross-window delegation. + +**Architecture:** A stateless TypeScript HTTP service owns durable delegation state in Postgres and records an append-only event stream plus transactional delivery outbox. The local MCP gateway signs relay commands, polls recipient events, performs elevated approval in Codex, and never sends workspace content to the relay. A Slack adapter posts requests and accepts only reject or `discuss_only` approval actions. + +**Tech Stack:** Node.js 20+, TypeScript, Express 5, Postgres 15+, `pg`, Zod, Slack Web API over HTTPS, Vitest, Docker Compose. + +## Global Constraints + +- Slack may approve only `discuss_only`; `read_only` and `workspace_write` require native Codex confirmation. +- Scope may be narrowed and never widened. +- Relay records must not contain absolute workspace paths, file contents, prompts, tool transcripts, secrets, or model reasoning. +- Capabilities are audience-bound, short-lived, single-use, and stored as hashes. +- Postgres is authoritative; delivery is at least once and command effects are idempotent. +- Do not add Kafka, Redis, or a policy language. + +--- + +### Task 1: Durable protocol and store + +**Files:** +- Create: `src/relay/types.ts` +- Create: `src/relay/store.ts` +- Create: `src/relay/memory-store.ts` +- Create: `src/relay/store.test.ts` +- Modify: `src/protocol.ts` + +**Interfaces:** +- Produces: `RelayStore`, `RelayCommand`, `RelayEvent`, `DeviceIdentity`, `CreateDelegationInput`, and `MemoryRelayStore`. +- `RelayStore.createDelegation(input, actor)` returns the stored delegation and event atomically. +- `RelayStore.transition(id, expectedVersion, command, actor)` enforces state and scope rules. + +- [ ] Write tests for idempotent creation, recipient isolation, narrowing, invalid widening, optimistic concurrency, expiry, terminal states, and redacted workspace labels. +- [ ] Run `pnpm vitest run src/relay/store.test.ts` and verify the new module is missing. +- [ ] Add exact Zod schemas and the `RelayStore` interface; implement the deterministic memory store used by unit tests. +- [ ] Run `pnpm vitest run src/relay/store.test.ts` and verify all store tests pass. +- [ ] Commit with `git commit -m "feat: define durable relay store"`. + +### Task 2: Postgres persistence and transactional outbox + +**Files:** +- Create: `src/relay/postgres.ts` +- Create: `src/relay/migrations/001_relay.sql` +- Create: `src/relay/postgres.test.ts` +- Modify: `package.json` +- Modify: `pnpm-lock.yaml` + +**Interfaces:** +- Consumes: `RelayStore` from Task 1. +- Produces: `PostgresRelayStore`, `migrate(pool)`, `claimOutbox(limit)`, `completeOutbox(id)`, and `failOutbox(id, error)`. + +- [ ] Write integration tests gated by `PIGEON_TEST_DATABASE_URL` for atomic create/event/outbox writes, duplicate idempotency keys, concurrent transitions, event cursors, and retry claims. +- [ ] Run the tests without the database variable and verify they skip cleanly; run them against Docker Postgres and verify they fail before implementation. +- [ ] Add `pg`, the migration, parameterized queries, transactions, row locking, uniqueness constraints, and `FOR UPDATE SKIP LOCKED` outbox claims. +- [ ] Run Postgres integration tests and verify all cases pass. +- [ ] Commit with `git commit -m "feat: persist relay state in postgres"`. + +### Task 3: Signed relay HTTP API + +**Files:** +- Create: `src/relay/auth.ts` +- Create: `src/relay/auth.test.ts` +- Create: `src/relay/app.ts` +- Create: `src/relay/app.test.ts` +- Create: `src/relay/main.ts` +- Modify: `package.json` + +**Interfaces:** +- Consumes: `RelayStore` and device public keys. +- Produces: `createRelayApp({ store, devices, clock })`, `verifyDeviceRequest`, and HTTP endpoints `POST /v1/delegations`, `GET /v1/events`, `POST /v1/delegations/:id/approve`, `POST /v1/delegations/:id/reject`, `POST /v1/delegations/:id/start`, `POST /v1/delegations/:id/complete`, plus `GET /healthz`. + +- [ ] Write tests for signatures, timestamp skew, nonce replay, tenant/recipient isolation, idempotency, Slack approval restrictions, Codex elevated approval, redacted responses, and stable error codes. +- [ ] Run the focused tests and verify failure before implementation. +- [ ] Implement canonical request signing, in-memory nonce replay protection, Zod request validation, actor/source authorization, and JSON error middleware. +- [ ] Run focused API tests and verify they pass. +- [ ] Commit with `git commit -m "feat: add signed relay api"`. + +### Task 4: Slack notifications and safe interactive actions + +**Files:** +- Create: `src/slack/client.ts` +- Create: `src/slack/client.test.ts` +- Create: `src/slack/actions.ts` +- Create: `src/slack/actions.test.ts` +- Modify: `src/relay/app.ts` +- Modify: `src/relay/postgres.ts` + +**Interfaces:** +- Produces: `SlackClient.postDelegation`, `SlackClient.updateDelegation`, `verifySlackRequest`, `handleSlackAction`, and an outbox delivery handler. +- Slack approval calls the relay transition only when the requested and effective scopes are `discuss_only`; rejection is permitted for every scope. + +- [ ] Write tests using a fake Slack HTTP server for Block Kit payload redaction, immediate action acknowledgement, request signature verification, stale actions, rejection, low-risk approval, and elevated-scope denial. +- [ ] Run focused Slack tests and verify failure before implementation. +- [ ] Implement minimal Slack Web API calls, verified interactive routes, opaque IDs, DM request blocks, and outbox retry classification. +- [ ] Run focused Slack and API tests and verify they pass. +- [ ] Commit with `git commit -m "feat: connect slack approval workflow"`. + +### Task 5: Codex gateway relay client + +**Files:** +- Create: `src/relay/client.ts` +- Create: `src/relay/client.test.ts` +- Modify: `src/server.ts` +- Modify: `src/gateway.ts` +- Modify: `plugins/pigeon/.mcp.json` + +**Interfaces:** +- Produces: `RelayClient.createDelegation`, `RelayClient.events`, `RelayClient.approve`, `RelayClient.reject`, and `RelayClient.complete`. +- The existing MCP tools use the remote client when `PIGEON_RELAY_URL`, `PIGEON_DEVICE_ID`, and `PIGEON_DEVICE_PRIVATE_KEY` are present; otherwise they retain the evaluation-only memory relay. + +- [ ] Write tests for canonical signing, event cursors, retry-safe commands, remote inbox rendering, Slack-approved discussion work, and mandatory native confirmation for elevated work. +- [ ] Run focused tests and verify failure before implementation. +- [ ] Implement the signed HTTP client and gateway adapter, keeping private keys local and sending only workspace labels. +- [ ] Run gateway, widget, client, and protocol tests and verify they pass. +- [ ] Commit with `git commit -m "feat: connect codex gateway to relay"`. + +### Task 6: Deployment, operations, and release verification + +**Files:** +- Create: `Dockerfile` +- Create: `docker-compose.yml` +- Create: `.env.example` +- Create: `docs/company-relay.md` +- Create: `scripts/smoke-relay.mjs` +- Modify: `README.md` +- Modify: `.github/workflows/ci.yml` +- Modify: `package.json` + +**Interfaces:** +- Produces: `pnpm relay`, `pnpm migrate`, `pnpm smoke:relay`, a health endpoint, container image, local Postgres stack, and operator setup documentation. + +- [ ] Add a smoke script that registers two fixture devices, creates a delegation, reads it as the recipient, approves it through the allowed surface, and observes completion. +- [ ] Add container/deployment files with non-secret examples, health checks, migration startup, retention and revocation guidance, and Slack app configuration. +- [ ] Run `pnpm test`, `pnpm typecheck`, `pnpm build`, `pnpm validate:plugin`, and the Docker-backed smoke test. +- [ ] Inspect tracked files and build output for credentials, private keys, absolute personal paths, and raw workspace content. +- [ ] Commit with `git commit -m "docs: ship company relay deployment"`. +- [ ] Push the branch, open a draft pull request against `evalops/pigeon:main`, and report validation evidence and remaining production prerequisites. From 72086a3d11aab31262a6445240ee5f640a4dbbe8 Mon Sep 17 00:00:00 2001 From: Jonathan Haas Date: Sat, 11 Jul 2026 06:49:57 -0700 Subject: [PATCH 3/5] chore: ignore local worktrees --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2a7c6d6..d7588c0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ dist/ .pigeon/ .venv/ coverage/ +.worktrees/ *.log From 8db15a24b28168b032181a9799560ff62a79c554 Mon Sep 17 00:00:00 2001 From: Jonathan Haas Date: Sat, 11 Jul 2026 09:50:41 -0700 Subject: [PATCH 4/5] feat: use agent kit for durable delegation --- README.md | 24 ++++++--- SECURITY.md | 4 +- package.json | 3 +- pnpm-lock.yaml | 10 ++++ pnpm-workspace.yaml | 1 + src/gateway.test.ts | 55 ++++++++++++-------- src/gateway.ts | 120 +++++++++++++++++++++++++++++++++----------- src/server.ts | 32 ++++++++---- 8 files changed, 178 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 0b46026..160a803 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Tiny, approval-gated delegation between teammates' Codex agents. -Pigeon packages an MCP server, an embedded delegation inbox, signed authority envelopes, scope narrowing, and a native MCP confirmation before work can begin. The relay never receives Codex or OpenAI credentials. +Pigeon packages an MCP server, an embedded delegation inbox, scope narrowing, and native MCP confirmation. EvalOps Agent Kit owns authentication, durable Agent Runtime state, local authority, and Codex app-server execution; Pigeon never receives Platform refresh credentials or OpenAI credentials. ## What works @@ -10,10 +10,10 @@ Pigeon packages an MCP server, an embedded delegation inbox, signed authority en - `open_pigeon_inbox` renders the embedded inbox. - The recipient can reject or narrow `workspace_write` → `read_only` → `discuss_only`. - Approve triggers MCP `elicitation/create`; cancelling it starts nothing. -- Confirmation capabilities are short-lived and single-use. -- Delegation state transitions and Ed25519 envelopes are tested. +- Agent Kit records the confirmed scope and performs execution under a fenced Platform lease. +- Pigeon production mode never falls back to process-local state. -The included transport is intentionally an in-process evaluation relay. It proves the full approval and execution contract without exposing a machine on the network. A production deployment must replace `InMemoryRelay` with an authenticated durable transport and connect `CodexAdapter` to the local Codex app-server; those boundaries are explicit in `src/gateway.ts`. +Pigeon connects only to the owner-only Agent Kit Unix socket. Platform Agent Runtime is the durable ledger, and the Agent Kit daemon connects outbound to Platform and Codex app-server. ## Run @@ -25,6 +25,16 @@ pnpm validate:plugin node dist/server.js ``` +The MCP process requires stable coordinates from Agent Kit enrollment: + +```bash +export EVALOPS_AGENT_SOCKET=/tmp/evalops-agent-kit.sock +export PIGEON_ORGANIZATION_ID=org_... +export PIGEON_WORKSPACE_ID=workspace_... +export PIGEON_USER_PRINCIPAL_ID=user_... +export PIGEON_DEVICE_PRINCIPAL_ID=device_... +``` + The MCP process uses stdio. Install the repo-local marketplace, then install `pigeon` and start a new Codex task so tools and skills reload. ## Approval contract @@ -33,15 +43,15 @@ The MCP process uses stdio. Install the repo-local marketplace, then install `pi 2. Recipient reviews the request in the embedded inbox. 3. Recipient may narrow, never widen, authority. 4. The host displays a native confirmation containing sender, objective, workspace, and final scope. -5. Only the resulting one-use capability can start the adapter. +5. Pigeon sends the narrowed decision to Agent Kit; the daemon records evidence and executes only after all Platform and local gates pass. ## Development -Node 20+ is supported. Tests are intentionally deterministic and require no API keys. +Node 22+ is supported. Tests are deterministic and require no API keys. ```bash ./node_modules/.bin/vitest run ./node_modules/.bin/tsc -p tsconfig.json --noEmit ``` -See [SECURITY.md](SECURITY.md) before replacing the in-process relay. +See [SECURITY.md](SECURITY.md) for the daemon and Platform authority boundaries. diff --git a/SECURITY.md b/SECURITY.md index ad30a67..bcf67f0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,6 +2,6 @@ Please report vulnerabilities privately through GitHub's security-advisory flow. -Pigeon treats the recipient gateway as the authority boundary. Effective scope cannot exceed requested scope; confirmation tokens are short-lived, single-use, and stored as hashes. Never expose a Codex app-server directly to the internet, forward OpenAI credentials through a relay, log private keys, or enable non-interactive production approval. +Pigeon treats the Agent Kit daemon as the machine authority boundary. Effective scope cannot exceed requested scope, and native confirmation is required before Pigeon sends an elevated approval decision. Never expose the Agent Kit socket or Codex app-server to the network, put Platform credentials in plugin configuration, or enable non-interactive workspace writes. -The current relay is evaluation-only and in-process. A network transport must add authenticated membership, replay protection, durable monotonic event sequences, rate limits, encryption in transit, and an append-only redacted audit log before production use. +Platform Agent Runtime owns authenticated membership, replay protection, leases, durable events, and audit evidence. The local socket is owner-only; integrations receive scoped capabilities from the daemon. Pigeon must not log objectives, absolute paths, credentials, model responses, or raw daemon frames. diff --git a/package.json b/package.json index 93d4994..c2d21a2 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "type": "module", - "engines": { "node": ">=20" }, + "engines": { "node": ">=22" }, "scripts": { "build": "tsc -p tsconfig.json", "dev": "tsx src/server.ts", @@ -13,6 +13,7 @@ "validate:plugin": "python3 scripts/validate-plugin.py" }, "dependencies": { + "@evalops/agent-kit": "github:evalops/agent-kit#7b96bd0effb8e491fccffab1071975f8818253fa", "@modelcontextprotocol/ext-apps": "^1.0.1", "@modelcontextprotocol/sdk": "^1.25.3", "express": "^5.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e203a41..189ac63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@evalops/agent-kit': + specifier: github:evalops/agent-kit#7b96bd0effb8e491fccffab1071975f8818253fa + version: git+ssh://git@github.com/evalops/agent-kit.git#7b96bd0effb8e491fccffab1071975f8818253fa '@modelcontextprotocol/ext-apps': specifier: ^1.0.1 version: 1.7.4(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) @@ -404,6 +407,11 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@evalops/agent-kit@git+ssh://git@github.com/evalops/agent-kit.git#7b96bd0effb8e491fccffab1071975f8818253fa': + resolution: {commit: 7b96bd0effb8e491fccffab1071975f8818253fa, repo: git@github.com:evalops/agent-kit.git, type: git} + version: 0.1.0 + engines: {node: '>=22'} + '@exodus/bytes@1.15.1': resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2070,6 +2078,8 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@evalops/agent-kit@git+ssh://git@github.com/evalops/agent-kit.git#7b96bd0effb8e491fccffab1071975f8818253fa': {} + '@exodus/bytes@1.15.1': {} '@hono/node-server@1.19.14(hono@4.12.28)': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4dd398c..842ad20 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,3 +3,4 @@ packages: - apps/* allowBuilds: esbuild: true + '@evalops/agent-kit@git+ssh://git@github.com/evalops/agent-kit.git#7b96bd0effb8e491fccffab1071975f8818253fa': true diff --git a/src/gateway.test.ts b/src/gateway.test.ts index 0a635d5..31632e1 100644 --- a/src/gateway.test.ts +++ b/src/gateway.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; +import type { RunRecord, SubmitRequest } from "@evalops/agent-kit"; import { CapabilityStore, generateIdentity, signEnvelope, verifyEnvelope } from "./security.js"; -import { Gateway, InMemoryRelay } from "./gateway.js"; +import { Gateway, type AgentKitTransport } from "./gateway.js"; describe("security", () => { it("detects tampering and consumes confirmation once", () => { @@ -15,26 +16,40 @@ describe("security", () => { }); }); -describe("gateway", () => { - it("requires confirmation, narrows authority, and runs exactly once", async () => { - const relay = new InMemoryRelay(); - const run = vi.fn(async () => ({ summary: "done" })); - const a = new Gateway("jon", relay, { run }); - const b = new Gateway("alex", relay, { run }); - const delegation = a.delegate("alex", "inspect tests", "/repo", "workspace_write"); - expect(b.inbox()).toHaveLength(1); - const prepared = b.prepareApproval(delegation.id, "read_only"); - await b.confirmApproval(delegation.id, prepared.capability); - await expect(b.confirmApproval(delegation.id, prepared.capability)).rejects.toThrow("invalid_confirmation"); - expect(run).toHaveBeenCalledTimes(1); - expect(b.get(delegation.id)?.effectiveScope).toBe("read_only"); +describe("Agent Kit gateway", () => { + it("submits, reads inbox, narrows authority, and never runs Codex in Pigeon", async () => { + const fake = new FakeAgentKit(); + const gateway = new Gateway(identity("jon"), fake); + const delegation = await gateway.delegate("alex", "inspect tests", "github_repo:evalops/pigeon", "workspace_write"); + const recipient = new Gateway(identity("alex"), fake); + expect(await recipient.inbox()).toHaveLength(1); + const prepared = await recipient.prepareApproval(delegation.id, "read_only"); + expect(prepared.confirmation.effectiveScope).toBe("read_only"); + await recipient.confirmApproval(delegation.id, "read_only"); + expect((await recipient.get(delegation.id))?.effectiveScope).toBe("read_only"); + expect(fake.approve).toHaveBeenCalledTimes(1); }); - it("rejects widened authority", () => { - const relay = new InMemoryRelay(); - const b = new Gateway("alex", relay, { run: async () => ({ summary: "" }) }); - const a = new Gateway("jon", relay, { run: async () => ({ summary: "" }) }); - const delegation = a.delegate("alex", "chat", "/repo", "read_only"); - expect(() => b.prepareApproval(delegation.id, "workspace_write")).toThrow("scope_widening"); + it("rejects widened authority before calling the daemon", async () => { + const fake = new FakeAgentKit(); + const sender = new Gateway(identity("jon"), fake); + const delegation = await sender.delegate("alex", "chat", "github_repo:evalops/pigeon", "read_only"); + const recipient = new Gateway(identity("alex"), fake); + await expect(recipient.prepareApproval(delegation.id, "workspace_write")).rejects.toThrow("scope_widening"); + expect(fake.approve).not.toHaveBeenCalled(); }); }); + +const identity = (userPrincipalId: string) => ({ organizationId: "org", workspaceId: "workspace", userPrincipalId, devicePrincipalId: `${userPrincipalId}-device` }); + +class FakeAgentKit implements AgentKitTransport { + private runs = new Map(); + approve = vi.fn(async (id: string, scope: "discuss_only" | "read_only" | "workspace_write") => { + const run = await this.getRun(id); run.effective_scope = scope; return run; + }); + async submit(request: SubmitRequest) { const run = { run_id: `run_${this.runs.size + 1}`, request, state: "queued" as const, events: [] }; this.runs.set(run.run_id, run); return run; } + async inbox(recipient: string) { return [...this.runs.values()].filter(run => run.request.recipient_principal_id === recipient && !run.effective_scope); } + async getRun(id: string) { const run = this.runs.get(id); if (!run) throw Object.assign(new Error("missing"), { code: "not_found" }); return run; } + async reject(id: string) { const run = await this.getRun(id); run.state = "rejected"; return run; } + async cancel(id: string) { const run = await this.getRun(id); run.state = "cancelled"; return run; } +} diff --git a/src/gateway.ts b/src/gateway.ts index 6fce04a..966bc58 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -1,37 +1,97 @@ import { randomUUID } from "node:crypto"; -import { canNarrowScope, type Delegation, type Scope, transition } from "./protocol.js"; -import { CapabilityStore } from "./security.js"; +import type { AuthorityScope, RunRecord, SubmitRequest } from "@evalops/agent-kit"; +import { canNarrowScope, type Delegation, type Scope } from "./protocol.js"; -export interface CodexAdapter { run(delegation: Delegation, signal?: AbortSignal): Promise<{ summary: string }> } +export interface AgentKitTransport { + submit(request: SubmitRequest): Promise; + inbox(recipientPrincipalId: string): Promise; + getRun(runId: string): Promise; + approve(runId: string, scope: AuthorityScope): Promise; + reject(runId: string, reason: string): Promise; + cancel(runId: string, reason: string): Promise; +} -export class InMemoryRelay { - #events: Delegation[] = []; - publish(d: Delegation) { const i = this.#events.findIndex(x => x.id === d.id); if (i >= 0) this.#events[i] = structuredClone(d); else this.#events.push(structuredClone(d)); } - forRecipient(name: string) { return this.#events.filter(x => x.recipient === name).map(x => structuredClone(x)); } - get(id: string) { const d = this.#events.find(x => x.id === id); return d && structuredClone(d); } +export interface GatewayIdentity { + organizationId: string; + workspaceId: string; + userPrincipalId: string; + devicePrincipalId: string; } export class Gateway { - #caps = new CapabilityStore(); - constructor(public readonly name: string, private relay: InMemoryRelay, private adapter: CodexAdapter) {} - delegate(recipient: string, objective: string, workspace: string, requestedScope: Scope): Delegation { - const now = Date.now(); - const d: Delegation = { id: randomUUID(), sender: this.name, recipient, objective, workspace, requestedScope, state: "pending", createdAt: now, expiresAt: now + 15 * 60_000, idempotencyKey: randomUUID() }; - this.relay.publish(d); return d; - } - inbox() { return this.relay.forRecipient(this.name).filter(d => d.state === "pending"); } - get(id: string) { return this.relay.get(id); } - prepareApproval(id: string, effectiveScope: Scope) { - const d = this.mustOwn(id); if (!canNarrowScope(d.requestedScope, effectiveScope)) throw new Error("scope_widening"); - d.effectiveScope = effectiveScope; this.relay.publish(d); - return { capability: this.#caps.issue(id), confirmation: { sender: d.sender, objective: d.objective, workspace: d.workspace, effectiveScope } }; - } - async confirmApproval(id: string, capability: string) { - if (!this.#caps.consume(capability, id)) throw new Error("invalid_confirmation"); - const d = this.mustOwn(id); d.state = transition(d.state, "approve"); d.state = transition(d.state, "start"); this.relay.publish(d); - try { const result = await this.adapter.run(d); d.state = transition(d.state, "complete"); this.relay.publish(d); return result; } - catch (error) { d.state = transition(d.state, "fail"); this.relay.publish(d); throw error; } - } - reject(id: string) { const d = this.mustOwn(id); d.state = transition(d.state, "reject"); this.relay.publish(d); } - private mustOwn(id: string) { const d = this.relay.get(id); if (!d || d.recipient !== this.name) throw new Error("not_found"); if (d.expiresAt < Date.now()) throw new Error("expired"); return d; } + constructor(private readonly identity: GatewayIdentity, private readonly client: AgentKitTransport) {} + + async delegate(recipient: string, objective: string, resourceId: string, requestedScope: Scope): Promise { + const run = await this.client.submit({ + organization_id: this.identity.organizationId, + workspace_id: this.identity.workspaceId, + sender_principal_id: this.identity.userPrincipalId, + recipient_principal_id: recipient, + target_capability: "codex.app-server", + resource_id: resourceId, + objective, + requested_scope: requestedScope, + idempotency_key: `pigeon:${randomUUID()}`, + }); + return project(run); + } + + async inbox(): Promise { + return (await this.client.inbox(this.identity.userPrincipalId)).map(project); + } + + async get(id: string): Promise { + try { return project(await this.client.getRun(id)); } + catch (error) { + if (isNotFound(error)) return undefined; + throw error; + } + } + + async prepareApproval(id: string, effectiveScope: Scope) { + const delegation = await this.mustOwn(id); + if (!canNarrowScope(delegation.requestedScope, effectiveScope)) throw new Error("scope_widening"); + return { confirmation: { sender: delegation.sender, objective: delegation.objective, workspace: delegation.workspace, effectiveScope } }; + } + + async confirmApproval(id: string, effectiveScope: Scope): Promise { + await this.mustOwn(id); + return project(await this.client.approve(id, effectiveScope)); + } + + async reject(id: string, reason = "recipient rejected"): Promise { + await this.mustOwn(id); + return project(await this.client.reject(id, reason)); + } + + async cancel(id: string, reason = "sender cancelled"): Promise { + return project(await this.client.cancel(id, reason)); + } + + private async mustOwn(id: string): Promise { + const delegation = await this.get(id); + if (!delegation || delegation.recipient !== this.identity.userPrincipalId) throw new Error("not_found"); + return delegation; + } +} + +function project(run: RunRecord): Delegation { + const state = run.state === "queued" ? (run.effective_scope ? "approved" : "pending") : run.state; + return { + id: run.run_id, + sender: run.request.sender_principal_id, + recipient: run.request.recipient_principal_id, + objective: run.request.objective, + workspace: run.request.resource_id, + requestedScope: run.request.requested_scope, + effectiveScope: run.effective_scope, + state, + createdAt: 0, + expiresAt: Number.MAX_SAFE_INTEGER, + idempotencyKey: run.request.idempotency_key, + }; +} + +function isNotFound(error: unknown): boolean { + return typeof error === "object" && error !== null && "code" in error && (error as { code: unknown }).code === "not_found"; } diff --git a/src/server.ts b/src/server.ts index 71f4015..85dfc67 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,31 +2,41 @@ import { readFileSync } from "node:fs"; import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; -import { Gateway, InMemoryRelay } from "./gateway.js"; +import { AgentKitClient } from "@evalops/agent-kit"; +import { Gateway } from "./gateway.js"; import { ScopeSchema } from "./protocol.js"; import { widgetHtml } from "./widget.js"; -const relay = new InMemoryRelay(); -const teammate = process.env.PIGEON_USER ?? "local"; -const gateway = new Gateway(teammate, relay, { run: async d => ({ summary: `Delegation ${d.id} accepted; configure PIGEON_CODEX_COMMAND for execution.` }) }); +const required = (name: string) => { + const value = process.env[name]; + if (!value) throw new Error(`${name} is required; enroll Agent Kit and configure Pigeon with stable principal coordinates.`); + return value; +}; +const teammate = required("PIGEON_USER_PRINCIPAL_ID"); +const gateway = new Gateway({ + organizationId: required("PIGEON_ORGANIZATION_ID"), + workspaceId: required("PIGEON_WORKSPACE_ID"), + userPrincipalId: teammate, + devicePrincipalId: required("PIGEON_DEVICE_PRINCIPAL_ID"), +}, new AgentKitClient({ socketPath: process.env.EVALOPS_AGENT_SOCKET ?? "/tmp/evalops-agent-kit.sock" })); const server = new McpServer({ name: "pigeon", version: "0.1.0" }, { instructions: "Use Pigeon only for explicit teammate delegation. Open the inbox to review requests. Approval always requires native confirmation." }); const UI = "ui://pigeon/inbox-v1.html"; const result = (data: unknown, message: string, ui = false) => ({ structuredContent: data as Record, content: [{ type: "text" as const, text: message }], ...(ui ? { _meta: { ui: { resourceUri: UI }, "openai/outputTemplate": UI } } : {}) }); server.registerResource("pigeon-inbox", new ResourceTemplate(UI, { list: undefined }), { mimeType: "text/html;profile=mcp-app", _meta: { ui: { csp: { connectDomains: [], resourceDomains: [] }, prefersBorder: true }, "openai/widgetDescription": "Approval-gated teammate delegation inbox" } }, async uri => ({ contents: [{ uri: uri.href, mimeType: "text/html;profile=mcp-app", text: widgetHtml }] })); -server.registerTool("open_pigeon_inbox", { title: "Open Pigeon inbox", description: "Use this when the user wants to review teammate delegation requests.", inputSchema: {}, annotations: { readOnlyHint: true }, _meta: { ui: { resourceUri: UI }, "openai/outputTemplate": UI } }, async () => result({ delegations: gateway.inbox() }, "Opened Pigeon inbox.", true)); +server.registerTool("open_pigeon_inbox", { title: "Open Pigeon inbox", description: "Use this when the user wants to review teammate delegation requests.", inputSchema: {}, annotations: { readOnlyHint: true }, _meta: { ui: { resourceUri: UI }, "openai/outputTemplate": UI } }, async () => result({ delegations: await gateway.inbox() }, "Opened Pigeon inbox.", true)); server.registerTool("list_teammates", { description: "Use this when the user wants to see available delegation teammates.", inputSchema: {}, annotations: { readOnlyHint: true } }, async () => result({ teammates: [teammate] }, "Listed teammates.")); -server.registerTool("delegate_to_teammate", { description: "Use this when the user explicitly asks another teammate's Codex to perform bounded work.", inputSchema: { recipient: z.string(), objective: z.string(), workspace: z.string(), requestedScope: ScopeSchema }, annotations: { readOnlyHint: false, openWorldHint: true } }, async args => { const d = gateway.delegate(args.recipient, args.objective, args.workspace, args.requestedScope); return result({ delegation: d }, `Delegation ${d.id} is waiting for ${d.recipient}.`); }); -server.registerTool("get_delegation", { description: "Use this when checking a delegation's current state.", inputSchema: { id: z.string() }, annotations: { readOnlyHint: true } }, async ({ id }) => result({ delegation: gateway.get(id) ?? null }, "Read delegation.")); -server.registerTool("reject_delegation", { description: "Use this from the embedded inbox when the recipient rejects a request.", inputSchema: { id: z.string() }, annotations: { destructiveHint: true }, _meta: { ui: { visibility: ["app"] } } }, async ({ id }) => { gateway.reject(id); return result({ ok: true }, "Rejected delegation."); }); +server.registerTool("delegate_to_teammate", { description: "Use this when the user explicitly asks another teammate's Codex to perform bounded work.", inputSchema: { recipient: z.string(), objective: z.string(), workspace: z.string(), requestedScope: ScopeSchema }, annotations: { readOnlyHint: false, openWorldHint: true } }, async args => { const d = await gateway.delegate(args.recipient, args.objective, args.workspace, args.requestedScope); return result({ delegation: d }, `Delegation ${d.id} is waiting for ${d.recipient}.`); }); +server.registerTool("get_delegation", { description: "Use this when checking a delegation's current state.", inputSchema: { id: z.string() }, annotations: { readOnlyHint: true } }, async ({ id }) => result({ delegation: await gateway.get(id) ?? null }, "Read delegation.")); +server.registerTool("reject_delegation", { description: "Use this from the embedded inbox when the recipient rejects a request.", inputSchema: { id: z.string() }, annotations: { destructiveHint: true }, _meta: { ui: { visibility: ["app"] } } }, async ({ id }) => { await gateway.reject(id); return result({ ok: true }, "Rejected delegation."); }); server.registerTool("prepare_approval", { description: "Use this only from the Pigeon inbox to narrow scope and request native confirmation.", inputSchema: { id: z.string(), effectiveScope: ScopeSchema }, annotations: { readOnlyHint: false, openWorldHint: true }, _meta: { ui: { visibility: ["app"] } } }, async ({ id, effectiveScope }) => { - const prepared = gateway.prepareApproval(id, effectiveScope); + const prepared = await gateway.prepareApproval(id, effectiveScope); const c = prepared.confirmation; const answer = await server.server.elicitInput({ mode: "form", message: `Approve ${c.sender}'s Codex delegation? Objective: ${c.objective} | Workspace: ${c.workspace} | Authority: ${c.effectiveScope}`, requestedSchema: { type: "object", properties: { confirm: { type: "boolean", title: "Grant this authority" } }, required: ["confirm"] } }); if (answer.action !== "accept" || answer.content?.confirm !== true) return result({ confirmed: false }, "Approval cancelled."); - const run = await gateway.confirmApproval(id, prepared.capability); - return result({ confirmed: true, delegation: gateway.get(id), result: run }, "Delegation approved and completed."); + const delegation = await gateway.confirmApproval(id, effectiveScope); + return result({ confirmed: true, delegation }, "Delegation approved. Agent Kit will execute it under the confirmed authority."); }); if (process.argv[1] && readFileSync(process.argv[1], "utf8").includes("new McpServer")) await server.connect(new StdioServerTransport()); From 92880eff2b6a1123a40b630f432f60ca720174df Mon Sep 17 00:00:00 2001 From: Jonathan Haas Date: Sat, 11 Jul 2026 09:57:15 -0700 Subject: [PATCH 5/5] build: vendor immutable agent kit sdk --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- pnpm-workspace.yaml | 1 - vendor/evalops-agent-kit-0.1.0.tgz | Bin 0 -> 1881 bytes 4 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 vendor/evalops-agent-kit-0.1.0.tgz diff --git a/package.json b/package.json index c2d21a2..e7c355b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "validate:plugin": "python3 scripts/validate-plugin.py" }, "dependencies": { - "@evalops/agent-kit": "github:evalops/agent-kit#7b96bd0effb8e491fccffab1071975f8818253fa", + "@evalops/agent-kit": "file:vendor/evalops-agent-kit-0.1.0.tgz", "@modelcontextprotocol/ext-apps": "^1.0.1", "@modelcontextprotocol/sdk": "^1.25.3", "express": "^5.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 189ac63..df99228 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@evalops/agent-kit': - specifier: github:evalops/agent-kit#7b96bd0effb8e491fccffab1071975f8818253fa - version: git+ssh://git@github.com/evalops/agent-kit.git#7b96bd0effb8e491fccffab1071975f8818253fa + specifier: file:vendor/evalops-agent-kit-0.1.0.tgz + version: file:vendor/evalops-agent-kit-0.1.0.tgz '@modelcontextprotocol/ext-apps': specifier: ^1.0.1 version: 1.7.4(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) @@ -407,8 +407,8 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@evalops/agent-kit@git+ssh://git@github.com/evalops/agent-kit.git#7b96bd0effb8e491fccffab1071975f8818253fa': - resolution: {commit: 7b96bd0effb8e491fccffab1071975f8818253fa, repo: git@github.com:evalops/agent-kit.git, type: git} + '@evalops/agent-kit@file:vendor/evalops-agent-kit-0.1.0.tgz': + resolution: {integrity: sha512-SnSYA0LPnXiLC3/oCw4FLN/UMFYamcAEwtRb8j2HVodDzJfp62JOTGMRT7ElmDUpTJajQh4uXM4f1dnd3kjjig==, tarball: file:vendor/evalops-agent-kit-0.1.0.tgz} version: 0.1.0 engines: {node: '>=22'} @@ -2078,7 +2078,7 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@evalops/agent-kit@git+ssh://git@github.com/evalops/agent-kit.git#7b96bd0effb8e491fccffab1071975f8818253fa': {} + '@evalops/agent-kit@file:vendor/evalops-agent-kit-0.1.0.tgz': {} '@exodus/bytes@1.15.1': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 842ad20..4dd398c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,4 +3,3 @@ packages: - apps/* allowBuilds: esbuild: true - '@evalops/agent-kit@git+ssh://git@github.com/evalops/agent-kit.git#7b96bd0effb8e491fccffab1071975f8818253fa': true diff --git a/vendor/evalops-agent-kit-0.1.0.tgz b/vendor/evalops-agent-kit-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..2bca2016bc07d4f312146fc24cf04aba038f74a4 GIT binary patch literal 1881 zcmV-f2d4NRiwFP!000006YW{ubK5o&&$IrDJ+=?jRHiA_QEueOX(s1~UZ)q2eQ&vp z0s|`vF$u5$C|h;)zu(}46sZr}V<+j|)OZmE?8k2xzyjDU2)!aRJk;#!&@2noRPn+b zGOo=b7YuKb&-%p^TbicBqa(1tX_{{Sro;Z=6{J7D9H#yLaF`xJ+8+)EFX2V{RK*Wz zr8Y#pNS~~0)42bW#`8id1GhjG5`*ug5J(M|0#-0pGKWY=h9iL{I_}_&Bc>VA8s5%O zm|wU#RZ1$r8-s#r@c(W*002}nJPuz%8&y&xRcw2EkfYXQhTTAFn=T7faW!yU_nSG_ ziRFTEL*%Nn+KuO3#!;9cS4(<@=8Ty6HV|1@J9QUsy^1w0j3Ep*I*(!HL=e3c6tp3x z)NvEPlMM=WA%(VJ$u;37z$aN2-$IUNF4+ho&mO_5VJDwLTyZCMFT!{{u1UK!ooxzD zsYI|IXAJw-%8Zx(Iz=5Q4b2DNUlc^MAEiq2vPnD(HcxvS@{$pv*Y|ZwjPt&D`OcAaMVR z2uhs!I{+#CjMP+k6cR1PlK^>vcj56$_5q?qP|Us{ATRJSfY##c8EpXh)_cp@wIe(R z@dB^mOv#*UjI98fEwC4+w+rKw2FH5$`GdtCX&VP`ym1&cc8+Zs8kj1Q+ie>~?J>`k zXf&sSO2)-3j!HBAd(_=YCq*1luQ%#J?4X9BYsC%5|GaqruS6Tg#f(pvYY+SY(SJnL z-7nSp*@~p|Qe5qDfoaRaSI2T?X|MYgtYVokNUvL3a1* zbiuXvuM8bZP9Rk`g0JJElumITd8hT5-*eN(;hRL zP?;AQ8uW{CYX>wPx2hw^sxvKxzf>q8s|9|YK z!{_(^w`extMX(#7(}&5&+Eumoz2C-AcYioz(8@!wH9`6vvh#9o$q zz;MASI%O^(fx;QrMulOI@;t!EkZ9Ds0M%Z}OGOcKQWRXwbPohEe1R6=l&d_*&_u9H zd4mk_25IR#6;ioCwd^_7jFXv=+Hk6qPUqiqgeh0rK%pcn{k6{8I`gos$Ikc+r8s;i zZ5)DkL`cCcsJYbk>O`eA+4{(dFO|I+yM!Ww!klmc@>G#7l`n| zPD=v!rsJOYKNt?T>OV)r=lkC`X`zJTUYl>L_JnloaHWXWT5` zY-;i6)0Sv?X%iKEft&Qsc4QNmQXXy0l_%m1^FkUFbUC@g<()1)FU2`hsaQ2N<+c}t z(ox(0!H@R18bS11i6t_>HcBD5n7QvNQ4&U6_9gt85}t)mpC7jWyilA@z46}4H@nTm z1q!1_@K!0Ze7#N$U2(BtfCF0QIZ?~@!0M#o6oxb-3Ljk7w5!-|0auJ{%iXMVR-<*j zv+C&HtG`votW~bl!uu76(&eaGTHV7|C8?@v=BUbJzxMm~Bn-C-gd6Lj!Bj7*z}Kw1 z)t2uk-(S4Ci}}OHz}_nGtUbST!`y+QS(aH#@r8;97||@EJc2zus@DaOcDJoWJm=S* T+jDzvU!?sPQ|FEr044wc;Vq