-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add RFC process and REST API RFC #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,212 @@ | ||||||||||||||
| # REST API for Links and Posts | ||||||||||||||
|
|
||||||||||||||
| **Date:** 2026-05-26 | ||||||||||||||
| **Status:** Proposed | ||||||||||||||
|
|
||||||||||||||
| ## Goal | ||||||||||||||
|
|
||||||||||||||
| Add a JSON REST API to Abbey for creating, editing, and managing draft/published state of links and posts. This enables programmatic content management (e.g., from agents, scripts, or external tools) without going through the HTML form interface. | ||||||||||||||
|
|
||||||||||||||
| ## Motivation | ||||||||||||||
|
|
||||||||||||||
| Currently all content creation and editing happens through the browser UI. Adding a REST API allows: | ||||||||||||||
|
|
||||||||||||||
| - Automated posting from agents and scripts | ||||||||||||||
| - Quick link saving via API calls (e.g., from a bookmarklet or mobile device) | ||||||||||||||
| - Programmatic draft management (create drafts, edit, publish when ready) | ||||||||||||||
| - Future integrations with other tools and services | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Claw integration (Hermclaw, HomuncuCLAW / agent use case) Worth a short subsection (Motivation or “Claw integration”) that maps today’s patterns to this API:
HomuncuCLAW currently uses Draft visibility on list Please specify explicitly:
That’s the main thing agents need for “show my draft queue” from a phone. |
||||||||||||||
|
|
||||||||||||||
| ## Proposed Endpoints | ||||||||||||||
|
|
||||||||||||||
| ### Authentication | ||||||||||||||
|
|
||||||||||||||
| All API endpoints require an API key passed as a Bearer token in the `Authorization` header: | ||||||||||||||
|
|
||||||||||||||
| ```text | ||||||||||||||
| Authorization: Bearer <api_key> | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| API keys are generated per-user and stored in the database. The key is not the session cookie — it's a separate token specifically for API access. | ||||||||||||||
|
|
||||||||||||||
| ### Posts | ||||||||||||||
|
|
||||||||||||||
| | Method | Endpoint | Description | Auth | | ||||||||||||||
| |--------|----------|-------------|------| | ||||||||||||||
| | `GET` | `/api/posts` | List posts (respecting draft status for unauthenticated) | Optional | | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GET responses need full markdown for edit workflows List/show JSON includes Suggestion:
Without this, agents still need |
||||||||||||||
| | `GET` | `/api/posts/:slug` | Get a single post by slug | Optional | | ||||||||||||||
| | `POST` | `/api/posts` | Create a new post | Required | | ||||||||||||||
| | `PATCH` | `/api/posts/:slug` | Update an existing post | Required | | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Slug behavior — example vs current model The PATCH example changes Agents use slug as a stable identifier (
If (a), fix the example response so Also document optional |
||||||||||||||
| | `DELETE` | `/api/posts/:slug` | Delete a post | Required | | ||||||||||||||
|
|
||||||||||||||
| ### Links | ||||||||||||||
|
|
||||||||||||||
| | Method | Endpoint | Description | Auth | | ||||||||||||||
| |--------|----------|-------------|------| | ||||||||||||||
| | `GET` | `/api/links` | List links | Optional | | ||||||||||||||
| | `POST` | `/api/links` | Create a new link (auto-fetches title/description) | Required | | ||||||||||||||
| | `PATCH` | `/api/links/:id` | Update a link's title, description, or URL | Required | | ||||||||||||||
| | `DELETE` | `/api/links/:id` | Delete a link | Required | | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PDF URLs → papers (parity with LinksController) HTML Please document
MetaInspector latency and failures Link create runs MetaInspector synchronously in Consider documenting timeout behavior, optional GET /api/links/:id The endpoint table has list/create/update/destroy but no show. Agents often need fetch-by-id after create. Add |
||||||||||||||
|
|
||||||||||||||
| ### Request/Response Shapes | ||||||||||||||
|
|
||||||||||||||
| #### Create Post | ||||||||||||||
|
|
||||||||||||||
| ```json | ||||||||||||||
| // POST /api/posts | ||||||||||||||
| { | ||||||||||||||
| "title": "My New Post", | ||||||||||||||
| "markdown_body": "Hello world...", | ||||||||||||||
| "markdown_excerpt": "A short excerpt", | ||||||||||||||
| "post_tags": "ruby,rails,api", | ||||||||||||||
| "draft": true | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Response: 201 Created | ||||||||||||||
| { | ||||||||||||||
| "slug": "my-new-post", | ||||||||||||||
| "title": "My New Post", | ||||||||||||||
| "excerpt": "A short excerpt", | ||||||||||||||
| "tags": ["ruby", "rails", "api"], | ||||||||||||||
| "draft": true, | ||||||||||||||
| "created_at": "2026-05-26T00:00:00Z", | ||||||||||||||
| "updated_at": "2026-05-26T00:00:00Z", | ||||||||||||||
| "url": "/blog/2026/05/26/my-new-post/" | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| #### Update Post | ||||||||||||||
|
|
||||||||||||||
| ```json | ||||||||||||||
| // PATCH /api/posts/my-new-post | ||||||||||||||
| { | ||||||||||||||
| "title": "Updated Title", | ||||||||||||||
| "draft": false | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Response: 200 OK | ||||||||||||||
| { | ||||||||||||||
| "slug": "updated-title", | ||||||||||||||
| "title": "Updated Title", | ||||||||||||||
| "excerpt": "A short excerpt", | ||||||||||||||
| "tags": ["ruby", "rails", "api"], | ||||||||||||||
| "draft": false, | ||||||||||||||
| "created_at": "2026-05-26T00:00:00Z", | ||||||||||||||
| "updated_at": "2026-05-26T00:01:00Z", | ||||||||||||||
| "url": "/blog/2026/05/26/updated-title/" | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| #### Create Link | ||||||||||||||
|
|
||||||||||||||
| ```json | ||||||||||||||
| // POST /api/links | ||||||||||||||
| { | ||||||||||||||
| "url": "https://example.com/article" | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Response: 201 Created | ||||||||||||||
| { | ||||||||||||||
| "id": 42, | ||||||||||||||
| "title": "Auto-fetched from page", | ||||||||||||||
| "description": "Auto-fetched meta description", | ||||||||||||||
| "url": "https://example.com/article", | ||||||||||||||
| "created_at": "2026-05-26T00:00:00Z" | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| #### Update Link | ||||||||||||||
|
|
||||||||||||||
| ```json | ||||||||||||||
| // PATCH /api/links/42 | ||||||||||||||
| { | ||||||||||||||
| "title": "Custom Title", | ||||||||||||||
| "description": "Custom description" | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Response: 200 OK | ||||||||||||||
| { | ||||||||||||||
| "id": 42, | ||||||||||||||
| "title": "Custom Title", | ||||||||||||||
| "description": "Custom description", | ||||||||||||||
| "url": "https://example.com/article", | ||||||||||||||
| "created_at": "2026-05-26T00:00:00Z" | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| ### Draft State | ||||||||||||||
|
|
||||||||||||||
| Draft state is controlled via the `draft` boolean field on posts: | ||||||||||||||
|
|
||||||||||||||
| - `"draft": true` — post is not visible in public feeds or listings | ||||||||||||||
| - `"draft": false` (or omitted) — post is published and publicly visible | ||||||||||||||
| - Updating `draft` from `true` to `false` publishes the post | ||||||||||||||
| - The `published_at` timestamp should be set when a post is first published (draft → non-draft transition) | ||||||||||||||
|
|
||||||||||||||
| Links do not have a draft state — they are always publicly visible once created. | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The RFC says:
The get "/blog/:year/:month/:day/:id/", to: "blog#show", as: "dated_post"Please either:
As written, the checklist item “Set published_at on draft → published transitions” implies schema work that isn’t specified in the RFC body. |
||||||||||||||
|
|
||||||||||||||
| ### Error Responses | ||||||||||||||
|
|
||||||||||||||
| All errors return a JSON body with a descriptive message: | ||||||||||||||
|
|
||||||||||||||
| ```json | ||||||||||||||
| // 401 Unauthorized | ||||||||||||||
| { "error": "Invalid or missing API key" } | ||||||||||||||
|
|
||||||||||||||
| // 404 Not Found | ||||||||||||||
| { "error": "Post not found" } | ||||||||||||||
|
|
||||||||||||||
| // 422 Unprocessable Entity | ||||||||||||||
| { "error": "Validation failed", "details": { "title": ["can't be blank"] } } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| ## Technical Details | ||||||||||||||
|
|
||||||||||||||
| ### API Key Model | ||||||||||||||
|
|
||||||||||||||
| New `ApiKey` model: | ||||||||||||||
|
|
||||||||||||||
| - `id` — primary key | ||||||||||||||
| - `user_id` — belongs to user | ||||||||||||||
| - `name` — human-readable label (e.g., "Hermes Agent") | ||||||||||||||
| - `token_digest` — SHA256 hash of the raw token (never stored plaintext) | ||||||||||||||
| - `last_used_at` — timestamp of last API request | ||||||||||||||
| - `created_at`, `expires_at` — lifecycle timestamps | ||||||||||||||
| - Raw token is shown only once at creation time | ||||||||||||||
|
|
||||||||||||||
| ### Routing | ||||||||||||||
|
|
||||||||||||||
| API routes live under `/api` namespace in `config/routes.rb`: | ||||||||||||||
|
|
||||||||||||||
| ```ruby | ||||||||||||||
| namespace :api do | ||||||||||||||
| resources :posts, only: %i[index show create update destroy], param: :slug | ||||||||||||||
| resources :links, only: %i[index create update destroy] | ||||||||||||||
| end | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| ### Controllers | ||||||||||||||
|
|
||||||||||||||
| New `Api::PostsController` and `Api::LinksController` in `app/controllers/api/`. These are separate from the existing `BlogController` and `LinksController` to keep concerns separated. They share model logic but have their own rendering (JSON instead of HTML). | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. API design details for v1
|
||||||||||||||
|
|
||||||||||||||
| ### Authentication Middleware | ||||||||||||||
|
|
||||||||||||||
| API authentication uses a concern similar to the existing `Authentication` concern, but checking for Bearer token instead of session cookies. The `ApiController` base class skips the `request_authentication` redirect and returns `401 JSON` instead. | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. API key model — tighten the spec Per-user keys with
Optional auth on GET Clarify equivalence to
Same rules for |
||||||||||||||
|
|
||||||||||||||
| ### Existing Behavior Unchanged | ||||||||||||||
|
|
||||||||||||||
| - The HTML UI controllers (`BlogController`, `LinksController`) are untouched | ||||||||||||||
| - Public routes (`/blog/*`, `/links`, feeds) continue to work as before | ||||||||||||||
| - Session-based authentication for the admin UI is unchanged | ||||||||||||||
| - The `Post.published` scope still gates public visibility | ||||||||||||||
|
|
||||||||||||||
| ## Implementation Checklist | ||||||||||||||
|
|
||||||||||||||
| - [ ] Create `ApiKey` model and migration | ||||||||||||||
| - [ ] Create `app/controllers/api/application_controller.rb` with Bearer token auth | ||||||||||||||
| - [ ] Create `app/controllers/api/posts_controller.rb` with CRUD + draft state | ||||||||||||||
| - [ ] Create `app/controllers/api/links_controller.rb` with CRUD | ||||||||||||||
| - [ ] Add API routes to `config/routes.rb` | ||||||||||||||
| - [ ] Add view/UI for generating and managing API keys in the admin area | ||||||||||||||
| - [ ] Set `published_at` on draft → published transitions | ||||||||||||||
| - [ ] Add tests for all API endpoints (creation, editing, draft state, auth, errors) | ||||||||||||||
| - [ ] Update `AGENTS.md` with new commands and API documentation | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RFC status lifecycle
When an RFC is merged or implemented, please document that authors should update
**Status:**in the file (Proposed→Accepted→Implemented/Rejected). Otherwiserfcs/will accumulate stale “Proposed” docs.Agent discoverability
The checklist already says “Update AGENTS.md with API documentation.” When the API ships, consider a short
## APIsubsection with an examplecurland env var name (e.g.ABBEY_API_KEY) — agents and harness-style tooling readAGENTS.mdfirst.