feat: add RFC process and REST API RFC#159
Conversation
- Add RFCs section to AGENTS.md matching harness convention - Add RFC for REST API: create/edit links and posts, manage draft state - Includes endpoint design, auth via API keys, request/response shapes, and implementation checklist
EnriqueCanals
left a comment
There was a problem hiding this comment.
Thanks for this. The RFC process matches what we do in harness, and the REST API RFC fits how HomuncuCLAW (and similar claws) manage Abbey today via rails runner over SSH/Tailscale.
RFC process: LGTM with small doc tweaks (status lifecycle, agent-facing curl example when the API ships).
REST API RFC: Strong direction for programmatic posting. Before implementation, clarify a few items so the API does not drift from HTML behavior or break agent workflows: stable slugs, draft listing, full body on GET, published_at vs created_at, PDF links, and MetaInspector latency.
Threaded comments below on specific sections.
Review summary
Approve: RFC process in AGENTS.md (add status lifecycle; add curl example in AGENTS.md when API ships).
Approve in principle: REST API RFC — good fit for agents/claws replacing rails runner over SSH.
Clarify before implementation:
published_at— not on posts today; align withcreated_atURLs or add migration.- Slug policy — PATCH example implies auto slug from title; model only sets slug on create.
- Authenticated GET should return
markdown_bodyandmarkdown_excerpt. - Draft visibility — Bearer = all posts; no Bearer =
Post.publishedonly. - PDF URL → paper, MetaInspector timeouts, pagination, rate limits on auth failures.
- Shared param/serialization layer with HTML controllers.
Follow-ups (v2 is fine): Pages API, attachments, redirect_from, HermClaw/HomuncuCLAW bin/blog → HTTP note in RFC.
| - 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 |
There was a problem hiding this comment.
Claw integration (Hermclaw, HomuncuCLAW / agent use case)
Worth a short subsection (Motivation or “Claw integration”) that maps today’s patterns to this API:
| Claw need today | RFC coverage |
|---|---|
Post.create! / find_by(slug:).update! |
POST / PATCH /api/posts |
Promote draft (update!(draft: false)) |
draft on PATCH |
| List drafts / “what’s in the queue” | GET /api/posts with Bearer |
| Save link from URL | POST /api/links |
Page CRUD (/p/...) |
Not in v1 — call out as intentional omission or follow-up |
HomuncuCLAW currently uses bin/blog → SSH → docker exec → rails runner. This RFC is the right replacement for that runner path; agent confirmation rails (YES, YES, DESTROY) stay in SOUL.md, not in the API.
Draft visibility on list
Please specify explicitly:
- With Bearer: list includes drafts (mirror
post_scopewhenauthenticated?inBlogController). - Without Bearer: list matches
Post.publishedonly (same as public site).
That’s the main thing agents need for “show my draft queue” from a phone.
|
|
||
| | Method | Endpoint | Description | Auth | | ||
| |--------|----------|-------------|------| | ||
| | `GET` | `/api/posts` | List posts (respecting draft status for unauthenticated) | Optional | |
There was a problem hiding this comment.
GET responses need full markdown for edit workflows
List/show JSON includes excerpt but not markdown_body or markdown_excerpt. Claws and scripts editing drafts need the full body on GET /api/posts/:slug (and likely authenticated list/detail).
Suggestion:
- Authenticated GET: include
markdown_body,markdown_excerpt, and tags (normalizedtagsarray and/orpost_tagsstring — pick one). - Unauthenticated GET: omit body or return truncated excerpt only, matching what the public site exposes.
Without this, agents still need rails runner for reads, which undercuts much of the motivation.
| | `GET` | `/api/posts` | List posts (respecting draft status for unauthenticated) | Optional | | ||
| | `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 | |
There was a problem hiding this comment.
Slug behavior — example vs current model
The PATCH example changes slug from my-new-post to updated-title when only title is sent. In the app today, slug is assigned only on create (before_create :assign_slug). The HTML form allows manual slug edits but does not auto-regenerate from title on update.
Agents use slug as a stable identifier (PATCH /api/posts/:slug). Please pick and document one policy:
- (a) Slug immutable after create unless
slugis explicitly in the PATCH body (recommended; matches HTML). - (b) Slug auto-updates from
title(breaking vs current UI; breaks bookmarks unless you add redirects). - (c) Another rule, documented explicitly.
If (a), fix the example response so slug stays my-new-post when only title changes.
Also document optional slug on POST (HTML supports it; model overwrites on create via assign_slug — clarify whether the API allows override at create time).
| | `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 | |
There was a problem hiding this comment.
PDF URLs → papers (parity with LinksController)
HTML LinksController#create does not always create a Link. It calls PdfPaperCreator.create_from_url(url) and redirects to papers when that returns a paper.
Please document POST /api/links when the URL is a PDF, for example:
201with a paper resource (needs/api/papersor a polymorphic response),422with a clear error (“use papers endpoint”), or- explicitly defer papers to v2.
MetaInspector latency and failures
Link create runs MetaInspector synchronously in before_create. API clients may see multi-second hangs or hard failures on bad hosts.
Consider documenting timeout behavior, optional skip_fetch: true with required title / description, or async create + 202 + poll (v2 is fine if called out).
GET /api/links/:id
The endpoint table has list/create/update/destroy but no show. Agents often need fetch-by-id after create. Add GET /api/links/:id or document list-only access.
| - 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. |
There was a problem hiding this comment.
published_at on posts
The RFC says:
The
published_attimestamp should be set when a post is first published
The posts table today has draft, created_at, updated_at — no published_at (only feed_posts have that column). Public URLs are date-based from created_at:
get "/blog/:year/:month/:day/:id/", to: "blog#show", as: "dated_post"Please either:
- v1: Drop published_at from the RFC and use created_at / updated_at for ordering (current behavior), and remove the checklist item as written, or
- v2: Add a migration + document whether API clients can set created_at (backdating) and how that interacts with canonical URLs.
As written, the checklist item “Set published_at on draft → published transitions” implies schema work that isn’t specified in the RFC body.
|
|
||
| ### 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). |
There was a problem hiding this comment.
API design details for v1
- Pagination: HTML uses Kaminari (
paginates_per5 on posts, 15 on links). Specifypage/per_page(or cursor) onGET /api/postsandGET /api/links. - Content-Type: require
application/jsonon mutating requests; return415if missing. - Errors: controllers use
:unprocessable_content— align RFC examples with Rails 8 in this app. - Tags: document that
post_tagsis comma-separated and normalized like the HTML form. - DELETE: specify
204 No Contentvs200+ JSON for shell scripts. - Idempotency: agents retry POST; document duplicate-link behavior or an
Idempotency-Keyheader for creates.
|
|
||
| ### 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. |
There was a problem hiding this comment.
API key model — tighten the spec
Per-user keys with token_digest and show-once raw token look good. Suggest adding to Technical Details:
- Lookup: store a short public prefix (e.g. first 8 chars) for indexed lookup; verify with
ActiveSupport::SecurityUtils.secure_compareon the digest. - Format: prefix tokens (e.g.
abbey_…) so they are identifiable in logs and rotation. - Rate limiting: mirror session login (
rate_limitonSessionsController) — per-IP and/or per-key on repeated 401s. - Revocation:
revoked_ator soft-delete; admin UI in the checklist is the right place. - Deployment: Abbey + HomuncuCLAW use Tailscale — note whether
/apiis Tailscale-only, localhost + reverse proxy, or public HTTPS.
Optional auth on GET
Clarify equivalence to post_scope:
- Bearer present → all posts (including drafts).
- No Bearer →
Post.publishedonly.
Same rules for GET /api/posts/:slug. Decide whether unauthenticated access to a draft slug returns 404 without leaking existence.
| - Remaining sections are free-form but typically include motivation, technical details, and an implementation checklist | ||
|
|
||
| See `rfcs/2026-05-26_rest_api_for_links_and_posts.md` for a complete example. | ||
|
|
There was a problem hiding this comment.
RFC status lifecycle
When an RFC is merged or implemented, please document that authors should update **Status:** in the file (Proposed → Accepted → Implemented / Rejected). Otherwise rfcs/ will accumulate stale “Proposed” docs.
Agent discoverability
The checklist already says “Update AGENTS.md with API documentation.” When the API ships, consider a short ## API subsection with an example curl and env var name (e.g. ABBEY_API_KEY) — agents and harness-style tooling read AGENTS.md first.
Summary
rfcs/withYYYY-MM-DD_short_title.mdnaming, structured with Date/Status/Goal and free-form detailsrfcs/2026-05-26_rest_api_for_links_and_posts.md) — proposes a JSON API under/apifor programmatic content management:POST/PATCH /api/posts— create and edit posts, set draft statePOST/PATCH /api/links— create and edit links (auto-fetches title/description via MetaInspector)ApiControllerbase class (no changes to existing HTML controllers)