Skip to content

feat(auth): link SkillX accounts with AgentKit and accept its access tokens - #161

Merged
mrgoonie merged 2 commits into
mainfrom
feat/agentkit-account-link
Oct 29, 2026
Merged

mrgoonie merged 2 commits into
mainfrom
feat/agentkit-account-link

Conversation

@mrgoonie

Copy link
Copy Markdown
Contributor

Part of #81 (account linking + resource server; evidence events and webhooks stay open there).

Implements the SkillX side of the AgentKit identity contract (identity-contract.md, v3 ecosystem linking).

What changes

  • Link with AgentKit (Settings): SkillX is a public OAuth client identified by its CIMD https://skillx.sh/oauth/client.json. It uses auth code + S256 PKCE with resource=https://skillx.sh/api and scopes skillx-kits:read skillx-kits:write. The state is bound to the session user in KV (10 min), so a callback in another account's session is refused.
    • One AgentKit sub links to at most one SkillX account; a duplicate link is refused and its new grant revoked.
    • Unlink revokes SkillX's refresh token at the AS (RFC 7009) and deletes the row.
    • GitHub login is untouched.
  • Resource server: wherever scoped keys are accepted (resolveRequestCredential), an agentkit.best at+jwt is verified locally against the JWKS (ES256, typ, iss, aud ⊇ resource, exp/iat, sub) and mapped skillx-kits:read → library:read, skillx-kits:write → kits:write:@<active handle>. Errors: 401 invalid_token, 403 account_not_linked (with the link URL), 503 agentkit_unavailable.
  • Revocation ≤ 60 s: AS tokens live 60 s. A link last checked more than 60 s ago is re-validated by refreshing SkillX's own rotating grant. The one-time refresh is claimed in D1, so concurrent requests don't burn it. invalid_grant deletes the link; during an AS outage there is a 15 min grace, then 503. The refresh token is stored AES-GCM encrypted (HKDF from BETTER_AUTH_SECRET, user id as AAD).
  • Discovery: RFC 9728 /.well-known/oauth-protected-resource[/api]. Every 401 on these routes carries WWW-Authenticate: Bearer resource_metadata=....
  • No token-exchange endpoint: contract §4.2 says ak/Desktop send the AS-minted token for https://skillx.sh/api directly as Bearer.

Safety

  • Flag SKILLX_AGENTKIT_IDENTITY is "0" in production until the agentkit.best AS (ak-web #726) is deployed. With it off, bearer JWTs are ignored, the link and discovery routes answer 404, and unlinked accounts behave exactly as before.
  • Migration 0021_agentkit_links is additive only (a new table) and has rollback SQL in its header.

Tests

  • tests/auth/agentkit-bearer.test.ts and agentkit-link.test.ts (32 tests) run against a fake AS with a real ES256 key, PKCE checks, rotating refresh tokens and revocation.
  • Full suite: 144 files, 2723 passed. pnpm typecheck is clean.

🤖 Generated with Claude Code

mrgoonie and others added 2 commits October 30, 2026 00:31
…tokens

- "Link with AgentKit" in Settings: public OAuth client (CIMD), auth code +
  S256 PKCE, resource https://skillx.sh/api; consent, unlink with revoke.
- Resource server: agentkit.best at+jwt (ES256) verified via JWKS and
  mapped to library:read / kits:write:@handle on scoped-credential routes.
- Link liveness re-checked by refresh every 60s; invalid_grant unlinks.
- RFC 9728 metadata and WWW-Authenticate resource_metadata on 401s.
- Additive migration 0021 (agentkit_links); flag SKILLX_AGENTKIT_IDENTITY
  stays off until the agentkit.best AS is in production.

Part of #81

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reshes

- Claim a link refresh only if no other request refreshed since the row was
  read, so a stale read never replays a spent refresh token.
- Store the rotated refresh token before verifying the new access token,
  and revoke it when the subject no longer matches.
- Requests that lose the claim ride on the refresh in flight instead of
  answering 503 after a long idle period.
- Refuse a link the AS issued no refresh token for.
- Treat an undecodable signature as invalid_token, keep serving cached JWKS
  keys while a refresh fails (throttled, single-flight).
- Advertise RFC 9728 metadata in 401 challenges only while the feature is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mrgoonie
mrgoonie merged commit f702a64 into main Oct 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant