Skip to content

feat: add RFC process and REST API RFC#159

Open
hermclaw wants to merge 1 commit into
capotej:mainfrom
hermclaw:feat/rfcs-and-rest-api-rfc
Open

feat: add RFC process and REST API RFC#159
hermclaw wants to merge 1 commit into
capotej:mainfrom
hermclaw:feat/rfcs-and-rest-api-rfc

Conversation

@hermclaw

Copy link
Copy Markdown
Contributor

Summary

  • Add RFC process to AGENTS.md — mirrors the harness convention: RFCs live in rfcs/ with YYYY-MM-DD_short_title.md naming, structured with Date/Status/Goal and free-form details
  • Add RFC: REST API for Links and Posts (rfcs/2026-05-26_rest_api_for_links_and_posts.md) — proposes a JSON API under /api for programmatic content management:
    • POST/PATCH /api/posts — create and edit posts, set draft state
    • POST/PATCH /api/links — create and edit links (auto-fetches title/description via MetaInspector)
    • Auth via per-user API keys (Bearer token)
    • Separate ApiController base class (no changes to existing HTML controllers)
    • Full endpoint specs, request/response shapes, error handling, and implementation checklist

- 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 EnriqueCanals left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. published_at — not on posts today; align with created_at URLs or add migration.
  2. Slug policy — PATCH example implies auto slug from title; model only sets slug on create.
  3. Authenticated GET should return markdown_body and markdown_excerpt.
  4. Draft visibility — Bearer = all posts; no Bearer = Post.published only.
  5. PDF URL → paper, MetaInspector timeouts, pagination, rate limits on auth failures.
  6. 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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:

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 execrails 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_scope when authenticated? in BlogController).
  • Without Bearer: list matches Post.published only (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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (normalized tags array and/or post_tags string — 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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 slug is 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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  • 201 with a paper resource (needs /api/papers or a polymorphic response),
  • 422 with 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

published_at on posts

The RFC says:

The published_at timestamp should be set when a post is first published

The posts table today has draft, created_at, updated_atno 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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API design details for v1

  • Pagination: HTML uses Kaminari (paginates_per 5 on posts, 15 on links). Specify page / per_page (or cursor) on GET /api/posts and GET /api/links.
  • Content-Type: require application/json on mutating requests; return 415 if missing.
  • Errors: controllers use :unprocessable_content — align RFC examples with Rails 8 in this app.
  • Tags: document that post_tags is comma-separated and normalized like the HTML form.
  • DELETE: specify 204 No Content vs 200 + JSON for shell scripts.
  • Idempotency: agents retry POST; document duplicate-link behavior or an Idempotency-Key header 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_compare on the digest.
  • Format: prefix tokens (e.g. abbey_…) so they are identifiable in logs and rotation.
  • Rate limiting: mirror session login (rate_limit on SessionsController) — per-IP and/or per-key on repeated 401s.
  • Revocation: revoked_at or soft-delete; admin UI in the checklist is the right place.
  • Deployment: Abbey + HomuncuCLAW use Tailscale — note whether /api is 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.published only.

Same rules for GET /api/posts/:slug. Decide whether unauthenticated access to a draft slug returns 404 without leaking existence.

Comment thread AGENTS.md
- 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.

Copy link
Copy Markdown
Contributor

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 (ProposedAcceptedImplemented / 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.

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.

2 participants