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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/agent-auth-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@epilot/agent-auth": minor
---

New package `@epilot/agent-auth`: Agent Auth Protocol (AAP) client with Ed25519 key generation, RFC 7638 thumbprints, host/agent JWT signing, discovery caching, agent registration, capability requests, status polling (`waitForApproval`), capability execution, revoke/reactivate/rotate, plus epilot helpers (`listEpilotOrganizations`, `issueEpilotAccessToken`, `organizationAccessCapability`, `organizationGrants`, `epilotAgentAuthIssuer`).
5 changes: 5 additions & 0 deletions .changeset/cli-agent-auth-login.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@epilot/cli": minor
---

`epilot auth login` now uses the Agent Auth Protocol: the CLI registers an agent for this machine, the user approves it once in the browser, and tokens are issued (and silently refreshed) per organization. New `epilot org list|use|request|current` commands switch organizations and request access; `auth status` shows the agent and its grants; `auth logout` revokes the agent. `--legacy` keeps the previous browser callback flow, `--token` stays as manual mode.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
- name: Build and test CLI
if: steps.check.outputs.cli == 'true'
run: |
pnpm --filter @epilot/cli build
pnpm --filter @epilot/cli... build
pnpm --filter @epilot/cli test

- name: Bump versions, commit, tag and push
Expand Down Expand Up @@ -230,7 +230,7 @@ jobs:
registry-url: https://registry.npmjs.org/

- run: pnpm install --frozen-lockfile
- run: pnpm --filter @epilot/cli build
- run: pnpm --filter @epilot/cli... build

- name: Publish @epilot/cli
run: pnpm publish --ignore-scripts --no-git-checks || true
Expand Down
21 changes: 21 additions & 0 deletions packages/agent-auth/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2026 epilot GmbH

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
177 changes: 177 additions & 0 deletions packages/agent-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# @epilot/agent-auth

[![npm version](https://img.shields.io/npm/v/@epilot/agent-auth.svg)](https://www.npmjs.com/package/@epilot/agent-auth)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

[Agent Auth Protocol](https://agentauthprotocol.com/specification/v1.0-draft) (AAP) client for epilot.
Zero runtime dependencies, Node >= 18, ESM + CJS, TypeScript types included.

## What is the Agent Auth Protocol?

AAP makes agents first-class principals instead of anonymous holders of a user's token:

- A **host** (a machine, an application, a runtime) owns an Ed25519 key pair.
- An **agent** is a scoped actor registered under a host with its own Ed25519 key pair. It is `pending` until a
user approves it, then `active` until it expires or is revoked.
- A **capability** is an action the server offers. A **grant** ties a capability to an agent, optionally with
**constraints** on the arguments (for epilot: which organization, read-only, anonymized).
- **Host JWTs** (`typ: host+jwt`) authenticate host operations (register, status, revoke, rotate).
**Agent JWTs** (`typ: agent+jwt`, <= 60 s) authenticate `request-capability` and `execute`.
- **Approval** is RFC 8628 style device authorization: the server returns a `verification_uri_complete` and a
`user_code`; the user approves in the browser; the client polls `/agent/status`.

epilot's AAP server lives under `https://access-token.sls.epilot.io/v1/access-tokens/agent-auth` and offers two capabilities:

| Capability | Arguments | Result |
| --- | --- | --- |
| `epilot.organizations.list` | — | the linked user's organizations annotated with this agent's grants |
| `epilot.access_token.issue` | `{organization_id, read_only?, anonymize?, expires_in?}` | a short-lived epilot API token for that organization |

Full context: the epilot RFC "Agent Auth Protocol for epilot — agents as first-class principals".

## Install

```bash
npm install @epilot/agent-auth
```

## Keys

```ts
import { generateKeyPair, keyPairFromPrivateJwk, jwkThumbprint } from '@epilot/agent-auth';

const hostKey = generateKeyPair(); // { publicKey, privateKey, thumbprint }
// Persist hostKey.privateKey (a private Ed25519 JWK) with mode 0600, then later:
const restored = keyPairFromPrivateJwk(storedPrivateJwk);
jwkThumbprint(hostKey.publicKey); // RFC 7638 SHA-256 thumbprint, the `iss` of your JWTs
```

## JWTs

```ts
import { createHostJwt, createAgentJwt, decodeJwt } from '@epilot/agent-auth';

// Host JWT: iss = host thumbprint, carries host_public_key (and agent_public_key when registering)
const hostJwt = createHostJwt({ hostKey, audience: issuer, agentPublicKey: agentKey.publicKey });

// Agent JWT: iss = host thumbprint, sub = agent id, aud = capability location, exp <= 60 s
const agentJwt = createAgentJwt({
agentKey,
hostThumbprint: hostKey.thumbprint,
agentId,
audience: executeUrl,
capabilities: ['epilot.organizations.list'], // optional narrowing
});

decodeJwt(agentJwt); // { header, payload } — no verification, for debugging
```

You normally do not build JWTs yourself; `AgentAuthClient` does it for every call.

## AgentAuthClient

```ts
import { AgentAuthClient, epilotAgentAuthIssuer } from '@epilot/agent-auth';

const client = new AgentAuthClient({
baseUrl: epilotAgentAuthIssuer('production'), // or 'staging' | 'dev'
// fetch?: custom fetch, discoveryTtlMs?: 3_600_000, timeoutMs?: 15_000
});
```

| Method | Auth | Description |
| --- | --- | --- |
| `discover(force?)` | none | `GET /.well-known/agent-configuration`, cached for `discoveryTtlMs` |
| `endpoint(name)` | — | resolve an endpoint URL from discovery |
| `registerAgent(hostKey, agentKey, body)` | host JWT | register an agent; returns grants and an `approval` when pending |
| `requestCapability(identity, body)` | agent JWT | ask for more grants; returns the new grants and an `approval` |
| `getAgentStatus(hostKey, agentId)` | host JWT | full agent state incl. grants |
| `waitForApproval(hostKey, agentId, approval, options?)` | host JWT | poll status at `approval.interval` until active (or until `options.pendingGrantIds` are decided); throws `approval_expired` |
| `execute(identity, { capability, arguments }, location?)` | agent JWT | run a capability; unwraps `{data}` |
| `listCapabilities(auth?, query?)` / `describeCapability(name, auth?)` | optional | capability catalogue, with grant status when authenticated |
| `revokeAgent` / `reactivateAgent` / `rotateAgentKey` | host JWT | agent lifecycle |
| `rotateHostKey` / `revokeHost` | host JWT | host lifecycle (revoking a host revokes its agents) |
| `introspect(token, bearer?)` | server bearer | server-to-server validation of an agent JWT |

`identity` is `{ hostKey, agentKey, agentId }`. Every error is an `AgentAuthError` with `status`, `code`
(the server's `error`, e.g. `constraint_violated`, `agent_revoked`, or `network_error`), `message` and `details`.

## epilot helpers

```ts
import {
EPILOT_CAPABILITIES, // { organizationsList: 'epilot.organizations.list', accessTokenIssue: 'epilot.access_token.issue' }
organizationAccessCapability, // build an `epilot.access_token.issue` request with constraints
listEpilotOrganizations, // execute epilot.organizations.list
issueEpilotAccessToken, // execute epilot.access_token.issue
organizationGrants, // map grants to { organizationId, readOnly, anonymized }
epilotAgentAuthIssuer, // issuer URL per stage
} from '@epilot/agent-auth';

organizationAccessCapability({ organizationId: '739224', readOnly: true, anonymize: true });
// → { name: 'epilot.access_token.issue', constraints: { organization_id: '739224', read_only: true, anonymize: true } }
organizationAccessCapability(); // no constraints: the approval page grants the user's login organization
```

## Full example flow

```ts
import {
AgentAuthClient,
EPILOT_CAPABILITIES,
epilotAgentAuthIssuer,
generateKeyPair,
issueEpilotAccessToken,
listEpilotOrganizations,
organizationAccessCapability,
} from '@epilot/agent-auth';
import { hostname } from 'node:os';

const client = new AgentAuthClient({ baseUrl: epilotAgentAuthIssuer() });
const hostKey = generateKeyPair(); // persist this once per machine
const agentKey = generateKeyPair(); // one per agent

// 1. Register the agent. The user has not approved anything yet → status "pending".
const registration = await client.registerAgent(hostKey, agentKey, {
name: `my-agent @ ${hostname()}`,
host_name: hostname(),
mode: 'delegated',
reason: 'Sync contacts nightly',
capabilities: [
EPILOT_CAPABILITIES.organizationsList,
organizationAccessCapability({ readOnly: true, anonymize: true }),
],
});
const identity = { hostKey, agentKey, agentId: registration.agent_id };

// 2. Send the user to the approval page and wait.
if (registration.approval?.method === 'device_authorization') {
console.log(`Open ${registration.approval.verification_uri_complete}`);
console.log(`Code: ${registration.approval.user_code}`);
await client.waitForApproval(hostKey, identity.agentId, registration.approval);
}

// 3. Which organizations may this agent access?
const { organizations } = await listEpilotOrganizations(client, identity);
const org = organizations.find((o) => o.access.granted)!;

// 4. Mint an epilot API token (short-lived; call again whenever it expires).
const issued = await issueEpilotAccessToken(client, identity, { organization_id: org.organization_id });
console.log(issued.token, issued.expires_at);

// 5. Later: ask for more (write access to another organization). The user approves again in the browser.
const request = await client.requestCapability(identity, {
capabilities: [organizationAccessCapability({ organizationId: '911210', readOnly: false })],
reason: 'Import meter readings',
});
await client.waitForApproval(hostKey, identity.agentId, request.approval as never, {
pendingGrantIds: request.agent_capability_grants.map((g) => g.id!).filter(Boolean),
});

// 6. Done with this agent.
await client.revokeAgent(hostKey, identity.agentId);
```

## License

MIT
58 changes: 58 additions & 0 deletions packages/agent-auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "@epilot/agent-auth",
"version": "0.1.0",
"description": "Agent Auth Protocol (AAP) client for epilot: Ed25519 keys, host/agent JWTs, capability requests and epilot access token issuance",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"test": "vitest run",
"test:watch": "vitest",
"lint": "biome check src test",
"lint:fix": "biome check --write src test",
"typecheck": "tsc --noEmit",
"prepublishOnly": "pnpm build && pnpm test"
},
"keywords": [
"epilot",
"agent-auth-protocol",
"aap",
"agent",
"authentication",
"ed25519"
],
"author": "epilot GmbH",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/epilot-dev/sdk-js.git",
"directory": "packages/agent-auth"
},
"homepage": "https://github.com/epilot-dev/sdk-js/tree/main/packages/agent-auth#readme",
"bugs": {
"url": "https://github.com/epilot-dev/sdk-js/issues"
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"engines": {
"node": ">=18"
},
"devDependencies": {
"tsup": "^8.0.0",
"typescript": "^5.3.0",
"vitest": "^1.0.0"
}
}
Loading
Loading