From 2e373c446183355b036dbdf761d343809596bd0a Mon Sep 17 00:00:00 2001 From: rohanharikr Date: Mon, 11 May 2026 18:46:35 +0100 Subject: [PATCH] Bootstrap: drop /bootstrap POST, defer person binding to first request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PS-side /bootstrap endpoint was removed in hellocoop/Wallet#3937 (bootstrap_endpoint was also dropped from issuer's PS metadata). The CLI was unconditionally fetching bootstrap_endpoint from metadata and POSTing to /bootstrap, so npx @aauth/bootstrap --ps threw immediately with "PS metadata missing bootstrap_endpoint" against any current PS. Align the package with draft-hardt-aauth-bootstrap §Self-Hosted Enrollment: publication of the JWKS is the enrollment, there is no separate enrollment step. Person binding to a user happens lazily on the agent's first /aauth/token call, per §Agent-Person Binding in the protocol spec. Code changes: - bootstrap/src/bootstrap-ps.ts: collapse from ~204 to ~70 lines. New behavior: fetch PS metadata, validate required fields (issuer, token_endpoint, jwks_uri) and that issuer matches the URL, write agentId + personServerUrl to ~/.aauth/config.json. No network POST, no ephemeral keypair, no agent token mint, no 202 polling, no browser open. - bootstrap/src/cli.ts: drop onInteraction callback and login-hint / domain-hint / provider-hint / tenant flags from runBootstrapPS (those parameterized the now-removed POST; equivalent flags already exist on @aauth/fetch for the real /aauth/token call). Drop `open` import. Update success message and help text — no default PS URL shown anywhere. - bootstrap/package.json: drop @aauth/mcp-agent, @hellocoop/httpsig, open (no longer used). - local-keys/src/create-agent-token.ts: tweak error message ("register" → "configure one"). - local-keys/src/index.ts: export writeConfig so other packages / tests can reset config state. Docs: - README.md, bootstrap/README.md, bootstrap/skills/setup.md, fetch/README.md, fetch/skills/fetch.md, local-keys/README.md: remove the "default PS is person.hello-beta.net" claim and the beta-data warning; reframe `--ps` from "register" to "configure"; use placeholder in examples. Tests: - bootstrap/src/bootstrap-ps.test.ts: new file, 12 cases covering happy path, validation errors, issuer mismatch, trailing-slash normalization, config preservation, and a regression guard that asserts no registration POST is made. End-to-end verified locally against person.hello-beta.net with a real fetch through whoami.aauth.dev — lazy binding works: resource → 401 + resource_token → fetch hits PS /aauth/token → PS detects new (user, agent_id) → 202 + interaction URL → browser consent → binding written → auth_token issued → fetch retries resource and gets a real identity response. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 6 +- bootstrap/README.md | 8 +- bootstrap/package.json | 5 +- bootstrap/skills/setup.md | 7 +- bootstrap/src/bootstrap-ps.test.ts | 169 ++++++++++++++++++++++ bootstrap/src/bootstrap-ps.ts | 206 +++++---------------------- bootstrap/src/cli.ts | 26 +--- fetch/README.md | 4 +- fetch/skills/fetch.md | 6 +- local-keys/README.md | 6 +- local-keys/src/create-agent-token.ts | 2 +- local-keys/src/index.ts | 1 + package-lock.json | 5 +- 13 files changed, 230 insertions(+), 221 deletions(-) create mode 100644 bootstrap/src/bootstrap-ps.test.ts diff --git a/README.md b/README.md index b61acfd..1ea4bb7 100644 --- a/README.md +++ b/README.md @@ -83,12 +83,10 @@ const token = await createResourceToken({ resource, authServer, agent, agentJkt ### Local development ```bash -# Generate keys, register with a person server, and publish them -npx @aauth/bootstrap --ps https://person.hello-beta.net +# Generate keys, configure a person server, and publish them +npx @aauth/bootstrap --ps ``` -> **Note:** `https://person.hello-beta.net` is the Hellō Beta Person Server. Data is reset regularly, so don't store anything you need to keep. To run against a different PS (including your own), pass its URL via `--ps`. - See [`@aauth/bootstrap`](./bootstrap) for the full setup flow. ## Protocol Support diff --git a/bootstrap/README.md b/bootstrap/README.md index 3d8622a..74576d7 100644 --- a/bootstrap/README.md +++ b/bootstrap/README.md @@ -7,13 +7,13 @@ Part of [aauth-dev/packages-js](https://github.com/aauth-dev/packages-js). Proto ## Quick Start ```bash -# Generate keys, register with a person server, and walk through hosting setup -npx @aauth/bootstrap --ps https://person.hello-beta.net +# Generate keys, configure a person server, and walk through hosting setup +npx @aauth/bootstrap --ps ``` -> **Note:** `https://person.hello-beta.net` is the Hellō Beta Person Server. Data is reset regularly, so don't store anything you need to keep. To run against a different PS (including your own), pass its URL via `--ps`. +The bootstrap flow detects available key backends (YubiKey PIV, macOS Secure Enclave, software), generates keys on the strongest available backend, configures a person server for your agent, and bundles agent skills that walk you through publishing keys on platforms like GitHub Pages, GitLab Pages, Cloudflare Pages, and Netlify. -The bootstrap flow detects available key backends (YubiKey PIV, macOS Secure Enclave, software), generates keys on the strongest available backend, registers your agent with a person server, and bundles agent skills that walk you through publishing keys on platforms like GitHub Pages, GitLab Pages, Cloudflare Pages, and Netlify. +Per [draft-hardt-aauth-bootstrap §Self-Hosted Enrollment](https://github.com/dickhardt/AAuth), publication of the JWKS is the enrollment — there is no separate enrollment step. Person binding to a user happens lazily on the agent's first authorized request, per [§Agent-Person Binding](https://github.com/dickhardt/AAuth) in the protocol spec. ## Commands diff --git a/bootstrap/package.json b/bootstrap/package.json index 248e754..f249e0a 100644 --- a/bootstrap/package.json +++ b/bootstrap/package.json @@ -30,9 +30,6 @@ "directory": "bootstrap" }, "dependencies": { - "@aauth/local-keys": "^0.8.0", - "@aauth/mcp-agent": "^0.8.0", - "@hellocoop/httpsig": "^1.1.3", - "open": "^11.0.0" + "@aauth/local-keys": "^0.8.0" } } diff --git a/bootstrap/skills/setup.md b/bootstrap/skills/setup.md index de40bac..871d986 100644 --- a/bootstrap/skills/setup.md +++ b/bootstrap/skills/setup.md @@ -93,12 +93,7 @@ The person server URL is included as the `ps` claim in agent tokens. Set it duri npx @aauth/bootstrap add-agent --person-server ``` -The default person server is `https://person.hello-beta.net` — the Hellō Beta Person Server. If the user doesn't specify one, use the default: -``` -npx @aauth/bootstrap add-agent --person-server https://person.hello-beta.net -``` - -**Note:** The Hellō Beta Person Server resets its data regularly, so don't store anything that needs to persist. To run against a different PS (including your own), pass its URL via `--person-server` (alias `--ps`). +The agent MUST be configured with a person server URL. **Do not assume a default** — if the user hasn't specified one, ask them which PS to use before proceeding. ### 4. Choose a hosting platform diff --git a/bootstrap/src/bootstrap-ps.test.ts b/bootstrap/src/bootstrap-ps.test.ts new file mode 100644 index 0000000..18138bd --- /dev/null +++ b/bootstrap/src/bootstrap-ps.test.ts @@ -0,0 +1,169 @@ +import { describe, it, expect, vi, beforeAll, beforeEach, afterEach, afterAll } from 'vitest' +import { readConfig, writeConfig, getAgentConfig } from '@aauth/local-keys' +import type { AAuthConfig } from '@aauth/local-keys' +import { bootstrapWithPS } from './bootstrap-ps.js' + +const PS_URL = 'https://ps.example' +const AGENT_URL = 'https://agent.example' + +const validMetadata = { + issuer: PS_URL, + token_endpoint: `${PS_URL}/aauth/token`, + jwks_uri: `${PS_URL}/.well-known/jwks.json`, + interaction_endpoint: `${PS_URL}/aauth/interact`, +} + +function mockMetadataResponse(body: unknown, status = 200): Response { + return new Response(typeof body === 'string' ? body : JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }) +} + +describe('bootstrapWithPS', () => { + let originalConfig: AAuthConfig + let mockFetch: ReturnType + + beforeAll(() => { + originalConfig = readConfig() + }) + + afterAll(() => { + writeConfig(originalConfig) + }) + + beforeEach(() => { + writeConfig({ agents: {} }) + mockFetch = vi.fn() + vi.stubGlobal('fetch', mockFetch) + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('fetches metadata from the correct well-known URL', async () => { + mockFetch.mockResolvedValueOnce(mockMetadataResponse(validMetadata)) + + await bootstrapWithPS({ agentUrl: AGENT_URL, personServerUrl: PS_URL }) + + expect(mockFetch).toHaveBeenCalledWith(`${PS_URL}/.well-known/aauth-person.json`) + }) + + it('writes agentId and personServerUrl to config on success', async () => { + mockFetch.mockResolvedValueOnce(mockMetadataResponse(validMetadata)) + + await bootstrapWithPS({ agentUrl: AGENT_URL, personServerUrl: PS_URL }) + + const agentConfig = getAgentConfig(AGENT_URL) + expect(agentConfig?.agentId).toBe('aauth:local@agent.example') + expect(agentConfig?.personServerUrl).toBe(PS_URL) + }) + + it('uses the provided `local` value in agentId', async () => { + mockFetch.mockResolvedValueOnce(mockMetadataResponse(validMetadata)) + + await bootstrapWithPS({ agentUrl: AGENT_URL, personServerUrl: PS_URL, local: 'work' }) + + expect(getAgentConfig(AGENT_URL)?.agentId).toBe('aauth:work@agent.example') + }) + + it('preserves existing key entries when writing the agent config', async () => { + writeConfig({ + agents: { + [AGENT_URL]: { + keys: { + 'kid-123': { + backend: 'yubikey-piv', + algorithm: 'ES256', + keyId: '9e', + deviceLabel: 'yubikey-5c-0775', + }, + }, + }, + }, + }) + mockFetch.mockResolvedValueOnce(mockMetadataResponse(validMetadata)) + + await bootstrapWithPS({ agentUrl: AGENT_URL, personServerUrl: PS_URL }) + + const agentConfig = getAgentConfig(AGENT_URL) + expect(agentConfig?.keys['kid-123']).toBeDefined() + expect(agentConfig?.personServerUrl).toBe(PS_URL) + }) + + it('throws when metadata endpoint returns non-OK', async () => { + mockFetch.mockResolvedValueOnce(new Response('not found', { status: 404 })) + + await expect( + bootstrapWithPS({ agentUrl: AGENT_URL, personServerUrl: PS_URL }), + ).rejects.toThrow(/Failed to fetch PS metadata.*404/) + }) + + it('throws when metadata is missing issuer', async () => { + const { issuer, ...rest } = validMetadata + void issuer + mockFetch.mockResolvedValueOnce(mockMetadataResponse(rest)) + + await expect( + bootstrapWithPS({ agentUrl: AGENT_URL, personServerUrl: PS_URL }), + ).rejects.toThrow(/missing required field: issuer/) + }) + + it('throws when metadata is missing token_endpoint', async () => { + const { token_endpoint, ...rest } = validMetadata + void token_endpoint + mockFetch.mockResolvedValueOnce(mockMetadataResponse(rest)) + + await expect( + bootstrapWithPS({ agentUrl: AGENT_URL, personServerUrl: PS_URL }), + ).rejects.toThrow(/missing required field: token_endpoint/) + }) + + it('throws when metadata is missing jwks_uri', async () => { + const { jwks_uri, ...rest } = validMetadata + void jwks_uri + mockFetch.mockResolvedValueOnce(mockMetadataResponse(rest)) + + await expect( + bootstrapWithPS({ agentUrl: AGENT_URL, personServerUrl: PS_URL }), + ).rejects.toThrow(/missing required field: jwks_uri/) + }) + + it('throws when issuer does not match the PS URL', async () => { + mockFetch.mockResolvedValueOnce( + mockMetadataResponse({ ...validMetadata, issuer: 'https://imposter.example' }), + ) + + await expect( + bootstrapWithPS({ agentUrl: AGENT_URL, personServerUrl: PS_URL }), + ).rejects.toThrow(/issuer.*does not match URL/) + }) + + it('accepts trailing-slash differences between issuer and URL', async () => { + mockFetch.mockResolvedValueOnce( + mockMetadataResponse({ ...validMetadata, issuer: `${PS_URL}/` }), + ) + + await expect( + bootstrapWithPS({ agentUrl: AGENT_URL, personServerUrl: PS_URL }), + ).resolves.not.toThrow() + }) + + it('strips trailing slash from PS URL when constructing metadata URL', async () => { + mockFetch.mockResolvedValueOnce(mockMetadataResponse(validMetadata)) + + await bootstrapWithPS({ agentUrl: AGENT_URL, personServerUrl: `${PS_URL}/` }) + + expect(mockFetch).toHaveBeenCalledWith(`${PS_URL}/.well-known/aauth-person.json`) + }) + + it('does NOT make a registration POST to the PS', async () => { + mockFetch.mockResolvedValueOnce(mockMetadataResponse(validMetadata)) + + await bootstrapWithPS({ agentUrl: AGENT_URL, personServerUrl: PS_URL }) + + // Only the metadata GET should have been issued. + expect(mockFetch).toHaveBeenCalledTimes(1) + }) +}) diff --git a/bootstrap/src/bootstrap-ps.ts b/bootstrap/src/bootstrap-ps.ts index 49077c2..e1729c6 100644 --- a/bootstrap/src/bootstrap-ps.ts +++ b/bootstrap/src/bootstrap-ps.ts @@ -1,190 +1,63 @@ -import { createHash, randomUUID } from 'node:crypto' -import { generateKeyPair, exportJWK } from 'jose' -import { fetch as httpSigFetch } from '@hellocoop/httpsig' -import { pollDeferred } from '@aauth/mcp-agent' -import { setPersonServer, getAgentConfig, setAgentConfig, resolveKey, getBackend, readKeychain } from '@aauth/local-keys' -import type { FetchLike } from '@aauth/mcp-agent' +import { setAgentConfig, getAgentConfig } from '@aauth/local-keys' export interface BootstrapPSOptions { agentUrl: string personServerUrl: string - local?: string // Local part of agent identifier (default: "local") - loginHint?: string - domainHint?: string - providerHint?: string - tenant?: string - onInteraction: (url: string, code: string) => void + local?: string } interface PSMetadata { - bootstrap_endpoint: string + issuer: string token_endpoint: string - interaction_endpoint?: string jwks_uri: string + interaction_endpoint?: string } /** - * Bootstrap an agent with a person server. + * Configure an agent with a person server. + * + * Per draft-hardt-aauth-bootstrap §Self-Hosted Enrollment, publication of the + * JWKS is the enrollment — there is no separate enrollment step. The PS + * binding to a person happens lazily on the agent's first /aauth/token call, + * per draft-hardt-oauth-aauth-protocol §Agent-Person Binding. * - * 1. Fetch PS metadata → bootstrap_endpoint - * 2. Generate ephemeral keypair (bound into agent token via cnf.jwk) - * 3. Mint agent token locally, signed by root key - * 4. POST to PS /bootstrap, signed with ephemeral, carrying agent token - * - 200/204 → done - * - 202 → poll until complete (user interaction case) - * 5. Store PS URL in config + * This function: + * 1. Fetches and validates PS metadata + * 2. Persists agentId + personServerUrl to ~/.aauth/config.json + * + * No network registration call is made; signAgentToken reads personServerUrl + * from config and includes it in the `ps` claim of every minted agent_token. */ export async function bootstrapWithPS(options: BootstrapPSOptions): Promise { - const { agentUrl, personServerUrl, local = 'local', loginHint, domainHint, providerHint, tenant, onInteraction } = options + const { agentUrl, personServerUrl, local = 'local' } = options - // 1. Fetch PS metadata const metadata = await fetchPSMetadata(personServerUrl) - if (!metadata.bootstrap_endpoint) { - throw new Error('Person server metadata missing bootstrap_endpoint') - } - - // 2. Generate ephemeral keypair — bound into the agent token via cnf.jwk and - // used to sign polling requests if the PS returns 202. - const { publicKey: ephPub, privateKey: ephPriv } = await generateKeyPair('ES256', { crv: 'P-256' }) - const ephPrivJwk = await exportJWK(ephPriv) - const ephPubJwk = await exportJWK(ephPub) - - // 3. Mint agent token with ephemeral key as cnf, signed by the agent's root key - const domain = new URL(agentUrl).hostname - const agentId = `aauth:${local}@${domain}` - const agentTokenJwt = await buildAgentToken({ - agentUrl, - sub: agentId, - personServerUrl, - ephPubJwk, - }) - // 4. POST to PS /bootstrap — single round-trip, carrying agent token in Signature-Key - const body: Record = { - agent_server: agentUrl, + if (!metadata.issuer) { + throw new Error('PS metadata missing required field: issuer') } - if (loginHint) body.login_hint = loginHint - if (domainHint) body.domain_hint = domainHint - if (providerHint) body.provider_hint = providerHint - if (tenant) body.tenant = tenant - - const bootstrapResponse = await httpSigFetch(metadata.bootstrap_endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'AAuth-Capabilities': 'interaction', - }, - body: JSON.stringify(body), - signingKey: ephPrivJwk, - signatureKey: { type: 'jwt', jwt: agentTokenJwt }, - }) as Response - - if (bootstrapResponse.status === 202) { - // User interaction required — poll until PS completes the binding - const locationUrl = bootstrapResponse.headers.get('location') - if (!locationUrl) { - throw new Error('PS bootstrap 202 response missing Location header') - } - const resolvedLocation = resolveUrl(personServerUrl, locationUrl) - - let interactionUrl: string | undefined - let interactionCode: string | undefined - const aauthHeader = bootstrapResponse.headers.get('aauth-requirement') - if (aauthHeader) { - const match = aauthHeader.match(/url="([^"]+)"/) - const codeMatch = aauthHeader.match(/code="([^"]+)"/) - if (match) interactionUrl = match[1] - if (codeMatch) interactionCode = codeMatch[1] - } - - const signedFetch: FetchLike = async (url, init) => { - const response = await httpSigFetch(url, { - ...init, - signingKey: ephPrivJwk, - signatureKey: { type: 'jwt', jwt: agentTokenJwt }, - }) - return response as Response - } - - const result = await pollDeferred({ - signedFetch, - locationUrl: resolvedLocation, - interactionUrl, - interactionCode, - onInteraction, - }) - - if (result.response.status !== 200) { - const errorMsg = result.error?.error_description || result.error?.error || `status ${result.response.status}` - throw new Error(`PS bootstrap polling failed: ${errorMsg}`) - } - } else if (bootstrapResponse.status !== 200 && bootstrapResponse.status !== 204) { - const text = await bootstrapResponse.text() - throw new Error(`PS bootstrap failed with status ${bootstrapResponse.status}: ${text}`) - } - - // 5. Store agent identifier and PS URL in config - const existing = getAgentConfig(agentUrl) - setAgentConfig(agentUrl, { ...existing || { keys: {} }, agentId, personServerUrl }) -} - -/** - * Build an agent token signed by the root key, with the bootstrap ephemeral key as cnf. - * This ensures the announcement is tied to the same ephemeral key used in the bootstrap request. - */ -async function buildAgentToken(opts: { - agentUrl: string - sub: string - personServerUrl: string - ephPubJwk: unknown -}): Promise { - const { agentUrl, sub, personServerUrl, ephPubJwk } = opts - - const resolved = await resolveKey(agentUrl) - const now = Math.floor(Date.now() / 1000) - - const header: Record = { - alg: resolved.algorithm === 'RS256' ? 'RS256' : resolved.algorithm, - typ: 'aa-agent+jwt', - kid: resolved.kid, + if (!metadata.token_endpoint) { + throw new Error('PS metadata missing required field: token_endpoint') } - - const payload: Record = { - iss: agentUrl, - dwk: 'aauth-agent.json', - sub, - jti: randomUUID(), - cnf: { jwk: ephPubJwk }, - ps: personServerUrl, - iat: now, - exp: now + 300, + if (!metadata.jwks_uri) { + throw new Error('PS metadata missing required field: jwks_uri') } - const headerB64 = Buffer.from(JSON.stringify(header)).toString('base64url') - const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url') - const signingInput = `${headerB64}.${payloadB64}` - - if (resolved.backend === 'software') { - // Software keys: import private key from keychain and sign with jose - const { importJWK, SignJWT } = await import('jose') - const data = readKeychain(agentUrl) - if (!data) throw new Error(`No software keys found in keychain for ${agentUrl}`) - const rootJwk = data.keys[resolved.kid] || data.keys[data.current] - if (!rootJwk) throw new Error(`Key ${resolved.kid} not found in keychain`) - const alg = rootJwk.alg || (rootJwk.crv === 'P-256' ? 'ES256' : 'EdDSA') - const rootKey = await importJWK(rootJwk, alg) - const jwt = await new SignJWT(payload) - .setProtectedHeader({ alg, typ: 'aa-agent+jwt', kid: rootJwk.kid || resolved.kid }) - .sign(rootKey) - return jwt + const normalisedIssuer = metadata.issuer.replace(/\/$/, '') + const normalisedUrl = personServerUrl.replace(/\/$/, '') + if (normalisedIssuer !== normalisedUrl) { + throw new Error( + `PS issuer (${metadata.issuer}) does not match URL (${personServerUrl})`, + ) } - // Hardware keys: sign hash via backend driver - const driver = getBackend(resolved.backend) - const hash = createHash('sha256').update(signingInput).digest() - const { signature } = await driver.signHash(resolved.keyId, hash) - const sigB64 = Buffer.from(signature).toString('base64url') - return `${signingInput}.${sigB64}` + const agentId = `aauth:${local}@${new URL(agentUrl).hostname}` + const existing = getAgentConfig(agentUrl) + setAgentConfig(agentUrl, { + ...(existing ?? { keys: {} }), + agentId, + personServerUrl, + }) } async function fetchPSMetadata(personServerUrl: string): Promise { @@ -195,10 +68,3 @@ async function fetchPSMetadata(personServerUrl: string): Promise { } return await response.json() as PSMetadata } - -function resolveUrl(base: string, url: string): string { - if (url.startsWith('http://') || url.startsWith('https://')) { - return url - } - return new URL(url, base).href -} diff --git a/bootstrap/src/cli.ts b/bootstrap/src/cli.ts index 89dfd60..1f9f724 100644 --- a/bootstrap/src/cli.ts +++ b/bootstrap/src/cli.ts @@ -24,7 +24,6 @@ import { import type { KeyAlgorithm, KeyBackend, AAuthPublicJwk } from '@aauth/local-keys' import { listSkills, getSkill } from './skills.js' import { bootstrapWithPS } from './bootstrap-ps.js' -import open from 'open' function parseArgs(args: string[]) { const flags: Record = {} @@ -291,13 +290,9 @@ Add-agent options: --key-id Backend-specific key ID (slot, label, etc.) --algorithm Key algorithm -Person server bootstrap (can be combined with any command): - --person-server Bootstrap with person server (alias: --ps) +Person server configuration (can be combined with any command): + --person-server Person server URL (alias: --ps) --local Local part of agent identifier (default: "local") - --login-hint Hint about who to authorize - --domain-hint Domain/org routing hint - --provider-hint Upstream identity provider hint - --tenant Tenant identifier Examples: npx @aauth/bootstrap discover @@ -305,8 +300,8 @@ Examples: npx @aauth/bootstrap generate --backend secure-enclave --agent https://me.github.io npx @aauth/bootstrap sign-token --agent https://me.github.io npx @aauth/bootstrap add-agent https://me.github.io - npx @aauth/bootstrap --ps https://hello.coop - npx @aauth/bootstrap generate --agent https://me.github.io --ps https://hello.coop + npx @aauth/bootstrap --ps + npx @aauth/bootstrap generate --agent https://me.github.io --ps npx @aauth/bootstrap public-key --agent https://me.github.io`) } @@ -338,24 +333,15 @@ async function runBootstrapPS(flags: Record) { } } - console.error(`Bootstrapping ${agentUrl} with person server ${personServerUrl}...`) + console.error(`Configuring ${agentUrl} with person server ${personServerUrl}...`) await bootstrapWithPS({ agentUrl, personServerUrl, local: flags.local, - loginHint: flags['login-hint'], - domainHint: flags['domain-hint'], - providerHint: flags['provider-hint'], - tenant: flags.tenant, - onInteraction: (interactionEndpoint, code) => { - const url = `${interactionEndpoint}?code=${code}` - console.error(`Opening browser for consent: ${url}`) - open(url) - }, }) - console.error('Bootstrap complete. Person server registered.') + console.error('Person server configured. Person binding will happen on the agent\'s first authorized request.') } async function run() { diff --git a/fetch/README.md b/fetch/README.md index 92c78c2..a550b16 100644 --- a/fetch/README.md +++ b/fetch/README.md @@ -9,7 +9,7 @@ Part of [aauth-dev/packages-js](https://github.com/aauth-dev/packages-js). Proto The agent must be bootstrapped with a person server before making authorized requests. Use [`@aauth/bootstrap`](../bootstrap): ```bash -npx @aauth/bootstrap --ps https://person.hello-beta.net +npx @aauth/bootstrap --ps ``` ## Quick Start @@ -84,7 +84,7 @@ Run `npx @aauth/fetch --skill` to print a structured LLM-readable usage guide co ## Related Packages -- [`@aauth/bootstrap`](../bootstrap) — set up agent keys and register with a person server (run this first) +- [`@aauth/bootstrap`](../bootstrap) — set up agent keys and configure a person server (run this first) - [`@aauth/mcp-agent`](../mcp-agent) — programmatic agent-side AAuth for use inside applications ## License diff --git a/fetch/skills/fetch.md b/fetch/skills/fetch.md index b0f5336..7b8d75f 100644 --- a/fetch/skills/fetch.md +++ b/fetch/skills/fetch.md @@ -10,13 +10,13 @@ Make HTTP requests to AAuth-protected APIs. Handles HTTP message signatures, age ## Prerequisites -The agent must be bootstrapped with a person server before making authorized requests: +The agent must be configured with a person server before making authorized requests: ```bash -npx @aauth/bootstrap --ps https://person.hello-beta.net +npx @aauth/bootstrap --ps ``` -This registers the agent with the person server and stores the agent identifier (e.g., `aauth:local@yourdomain.com`) in `~/.aauth/config.json`. +This validates the PS metadata and stores the PS URL plus agent identifier (e.g., `aauth:local@yourdomain.com`) in `~/.aauth/config.json`. Person binding then happens lazily on the first authorized request. ## Discovery diff --git a/local-keys/README.md b/local-keys/README.md index 0ec20c8..b4bd9d8 100644 --- a/local-keys/README.md +++ b/local-keys/README.md @@ -4,7 +4,7 @@ Library for managing AAuth agent signing keys across hardware and software backe Part of [aauth-dev/packages-js](https://github.com/aauth-dev/packages-js). Protocol spec: [dickhardt/AAuth](https://github.com/dickhardt/AAuth). -> **Looking for a CLI?** This package is a library. The CLI for setting up agent keys, registering with a person server, and publishing keys is [`@aauth/bootstrap`](../bootstrap). Run `npx @aauth/bootstrap --ps https://person.hello-beta.net` to get started. +> **Looking for a CLI?** This package is a library. The CLI for setting up agent keys, configuring a person server, and publishing keys is [`@aauth/bootstrap`](../bootstrap). Run `npx @aauth/bootstrap --ps ` to get started. ## Install @@ -82,7 +82,7 @@ addKeyToAgent('https://you.github.io', 'kid-123', { deviceLabel: 'yubikey-5c-0775', }) -setPersonServer('https://you.github.io', 'https://person.hello-beta.net') +setPersonServer('https://you.github.io', 'https://your-ps.example') setHosting('https://you.github.io', { platform: 'github-pages', @@ -98,7 +98,7 @@ setHosting('https://you.github.io', { { "agents": { "https://you.github.io": { - "personServerUrl": "https://person.hello-beta.net", + "personServerUrl": "https://your-ps.example", "hosting": { "platform": "github-pages", "repo": "you/you.github.io" diff --git a/local-keys/src/create-agent-token.ts b/local-keys/src/create-agent-token.ts index f7d58cf..94c243c 100644 --- a/local-keys/src/create-agent-token.ts +++ b/local-keys/src/create-agent-token.ts @@ -53,7 +53,7 @@ export async function createAgentToken( if (!agentId) { throw new Error( `No agent identifier configured for ${agentUrl}. ` + - "Run 'npx @aauth/bootstrap --ps ' to register.", + "Run 'npx @aauth/bootstrap --ps ' to configure one.", ) } } diff --git a/local-keys/src/index.ts b/local-keys/src/index.ts index 86ba18a..addd254 100644 --- a/local-keys/src/index.ts +++ b/local-keys/src/index.ts @@ -5,6 +5,7 @@ export { createAgentToken } from './create-agent-token.js' export { discoverBackends, getBackend } from './backends/index.js' export { readConfig, + writeConfig, getAgentConfig, setAgentConfig, addKeyToAgent, diff --git a/package-lock.json b/package-lock.json index 38c5b0f..3f24cd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,10 +27,7 @@ "version": "0.8.2", "license": "MIT", "dependencies": { - "@aauth/local-keys": "^0.8.0", - "@aauth/mcp-agent": "^0.8.0", - "@hellocoop/httpsig": "^1.1.3", - "open": "^11.0.0" + "@aauth/local-keys": "^0.8.0" }, "bin": { "aauth-bootstrap": "dist/cli.js"