Skip to content

Commit 214c095

Browse files
committed
feat: add AGENTS.md.
1 parent ba671fe commit 214c095

1 file changed

Lines changed: 373 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
# AGENTS.md — Operating JustWiki from an AI agent
2+
3+
This guide teaches an AI agent how to read and write JustWiki content via its
4+
REST API. Everything below is drawn from the live handlers under
5+
`backend/app/routers/` — if behaviour ever diverges from this doc, trust the
6+
code and update the doc.
7+
8+
## Base URL and transport
9+
10+
- Backend: `http://localhost:8000` in development; whatever host you deploy
11+
`uvicorn` on in production.
12+
- All endpoints are JSON and live under `/api/`. The Vite dev server proxies
13+
`/api``:8000`, so `http://localhost:5173/api/...` works too.
14+
- The server is a single-worker FastAPI app backed by SQLite in WAL mode.
15+
Assume low concurrency: you will not be fighting many writers.
16+
17+
## Authentication
18+
19+
JustWiki accepts two kinds of credentials on every authenticated endpoint:
20+
21+
1. **Personal API token** (recommended for agents) — looks like
22+
`jwk_<random>`. Long-lived, revocable, created in the Profile page.
23+
2. **Session JWT** — issued by `/api/auth/login`, lives 24 hours, delivered
24+
as an httpOnly cookie or Bearer header. Good for short interactive jobs
25+
and the web UI.
26+
27+
Both are sent the same way: `Authorization: Bearer <token>`. The server
28+
tells them apart by the `jwk_` prefix.
29+
30+
### Create an API token (one-time human step)
31+
32+
1. Log in to JustWiki with a password.
33+
2. Open **Profile → API Tokens**.
34+
3. Click **New Token**, name it (e.g. `ci-bot`), pick an expiry (default 30
35+
days, max 365, or "Never").
36+
4. Copy the token string that starts with `jwk_…`. **This is the only time
37+
it will be shown.** If you lose it, revoke and mint a new one.
38+
39+
Prefer creating a dedicated `editor`-role account for agents so revocation
40+
is clean and audit logs are easy to read.
41+
42+
Policy recap:
43+
44+
- Only `editor`/`admin` roles can create tokens; `viewer` cannot.
45+
- A token **cannot** mint another token — stopping an attacker from using a
46+
stolen token to survive your revocation.
47+
- Revoking leaves the row in place so `last_used` / activity entries
48+
remain auditable.
49+
- No rate limit on token usage (only login is rate-limited).
50+
51+
### Use the token
52+
53+
```bash
54+
export JW_TOKEN="jwk_xxxxxxxxxxxx..."
55+
56+
curl -sS -H "Authorization: Bearer $JW_TOKEN" \
57+
http://localhost:8000/api/auth/me
58+
```
59+
60+
### Password login (alternative)
61+
62+
```bash
63+
curl -sS -c cookies.txt -X POST http://localhost:8000/api/auth/login \
64+
-H 'Content-Type: application/json' \
65+
-d '{"username":"admin","password":"admin"}'
66+
curl -sS -b cookies.txt http://localhost:8000/api/auth/me
67+
```
68+
69+
Login is rate-limited to **5 attempts/IP/60s**. Back off on HTTP 429.
70+
71+
### Manage your own tokens from the API
72+
73+
| Method | Path | Purpose |
74+
| ------ | ---------------------------- | ------------------------------------- |
75+
| GET | `/api/auth/tokens` | List your tokens (plaintext not sent) |
76+
| POST | `/api/auth/tokens` | Create one (body: `{name, expires_in_days}`) — only over session login |
77+
| DELETE | `/api/auth/tokens/{id}` | Revoke |
78+
79+
### Roles
80+
81+
| role | can read | can write | can delete own | admin things |
82+
| ------ | -------- | --------- | -------------- | ------------ |
83+
| admin ||| any page | ✅ all |
84+
| editor ||| own pages ||
85+
| viewer |||||
86+
87+
`viewer` is capped at read even if an ACL grants write. `admin` bypasses all
88+
ACL checks. The creator or an admin may delete a page. Only admins may purge
89+
(hard-delete) from the trash.
90+
91+
## Resource reference
92+
93+
All routes are authenticated unless noted.
94+
95+
### Pages — `/api/pages`
96+
97+
| Method | Path | Purpose |
98+
| ------ | ---------------------------- | ----------------------------------------- |
99+
| GET | `/api/pages` | Paginated list (filter by `parent_id`) |
100+
| GET | `/api/pages/tree` | Hierarchical tree |
101+
| GET | `/api/pages/graph` | Node/link graph for visualisation |
102+
| POST | `/api/pages` | Create a page |
103+
| GET | `/api/pages/{slug}` | Read one page (also bumps view count) |
104+
| PUT | `/api/pages/{slug}` | Update (requires `base_version` on edits) |
105+
| PATCH | `/api/pages/{slug}/move` | Change `parent_id` / `sort_order` |
106+
| DELETE | `/api/pages/{slug}` | Soft-delete (moves to trash) |
107+
| GET | `/api/pages/{slug}/children` | Direct children |
108+
| GET | `/api/pages/{slug}/backlinks`| Incoming wikilinks |
109+
110+
### Versions — `/api/pages/{slug}/...`
111+
112+
| Method | Path | Purpose |
113+
| ------ | --------------------------------------- | ----------------------- |
114+
| GET | `/api/pages/{slug}/versions` | List prior versions |
115+
| GET | `/api/pages/{slug}/versions/{num}` | Read a specific version |
116+
| GET | `/api/pages/{slug}/diff?v1=A&v2=B` | Unified diff |
117+
| POST | `/api/pages/{slug}/revert/{num}` | Revert to version `num` |
118+
119+
### Search — `/api/search`
120+
121+
`GET /api/search?q=...&tag=...&page=1&per_page=20`. FTS5 with trigram; queries
122+
with any word shorter than 3 chars fall back to LIKE. Snippets have matched
123+
terms wrapped in `<mark>...</mark>`.
124+
125+
### Tags — `/api/tags`, `/api/pages/{slug}/tags`
126+
127+
List all tags with page counts, add/remove tag on a page.
128+
129+
### Media — `/api/media`
130+
131+
| Method | Path | Purpose |
132+
| ------ | -------------------------- | --------------------------------- |
133+
| POST | `/api/media/upload` | Multipart upload (20 MB limit) |
134+
| GET | `/api/media` | List visible media |
135+
| GET | `/api/media/{filename}` | Fetch file (ACL-checked) |
136+
| DELETE | `/api/media/{media_id}` | Delete (admin, no live refs only) |
137+
138+
Allowed types: PNG, JPEG, GIF, WebP, SVG, PDF, text/plain, text/markdown.
139+
140+
### ACL — `/api/pages/{slug}/acl`
141+
142+
| Method | Path | Purpose |
143+
| ------ | ------------------------------------- | --------------------------------- |
144+
| GET | `/api/pages/{slug}/acl` | Explicit + inherited rows |
145+
| PUT | `/api/pages/{slug}/acl` | Replace the explicit ACL set |
146+
| DELETE | `/api/pages/{slug}/acl` | Clear explicit rows (re-inherit) |
147+
| GET | `/api/pages/{slug}/my-permission` | Your resolved permission |
148+
149+
ACL row shape: `{"principal_type":"user"|"group","principal_id":<int>,"permission":"read"|"write"}`.
150+
151+
### Groups — `/api/groups` (admin-only for writes)
152+
153+
`GET /api/groups`, `POST /api/groups`, `DELETE /api/groups/{id}`,
154+
`GET /api/groups/{id}/members`, `POST /api/groups/{id}/members`,
155+
`DELETE /api/groups/{id}/members/{user_id}`.
156+
157+
### Comments — `/api/pages/{slug}/comments`
158+
159+
Read requires page-read; writes require page-write. `POST`, `PUT /{id}`,
160+
`DELETE /{id}`. Non-admin authors can only edit or delete their own comments.
161+
162+
### Trash — `/api/trash`
163+
164+
`GET /api/trash` (your own items; admin sees all), `POST /api/trash/{slug}/restore`,
165+
`DELETE /api/trash/{slug}` (admin-only hard delete).
166+
167+
### Templates — `/api/templates`
168+
169+
`GET`, `POST`, `PUT /{id}`, `DELETE /{id}`. You can pass `template_id` on
170+
page creation and the template's `content_md` will seed the new page.
171+
172+
### Users — `/api/users` (mostly admin)
173+
174+
`GET /api/users/search?q=...&limit=...` is open to any authenticated user
175+
(for ACL pickers). All other user CRUD is admin-only.
176+
177+
## Core write workflows
178+
179+
### 1. Create a page
180+
181+
```bash
182+
curl -sS -b cookies.txt -X POST http://localhost:8000/api/pages \
183+
-H 'Content-Type: application/json' \
184+
-d '{
185+
"title": "Release checklist",
186+
"content_md": "# Release checklist\n\n- [ ] Bump version\n- [ ] Run tests\n",
187+
"parent_id": null,
188+
"sort_order": 0
189+
}'
190+
```
191+
192+
Returns the created page, including `slug` (auto-generated from title; CJK
193+
preserved) and `version: 1`. To nest under a parent, include `parent_id`; you
194+
need write permission on the parent.
195+
196+
Templates: pass `"template_id": <int>` instead of `content_md` to seed from a
197+
template.
198+
199+
### 2. Edit a page (optimistic locking)
200+
201+
`content_md` / `title` edits **require** `base_version` equal to the current
202+
`version` on disk; otherwise you get HTTP 400 (`base_version_required`) or
203+
409 (`conflict`). Metadata-only edits (`is_public`, `parent_id`, `sort_order`)
204+
do not need it.
205+
206+
```bash
207+
# 1) Read the page to learn its current version
208+
curl -sS -b cookies.txt http://localhost:8000/api/pages/release-checklist \
209+
| jq '{version, slug}'
210+
# → {"version": 3, "slug": "release-checklist"}
211+
212+
# 2) Send the edit with base_version
213+
curl -sS -b cookies.txt -X PUT http://localhost:8000/api/pages/release-checklist \
214+
-H 'Content-Type: application/json' \
215+
-d '{
216+
"content_md": "# Release checklist\n\n- [x] Bump version\n- [ ] Run tests\n",
217+
"base_version": 3
218+
}'
219+
```
220+
221+
On conflict (409) the server returns the latest `current_version`; re-read,
222+
re-apply your change on top, and retry.
223+
224+
### 3. Upload media and reference it
225+
226+
```bash
227+
curl -sS -b cookies.txt -X POST http://localhost:8000/api/media/upload \
228+
-F "file=@diagram.png"
229+
# → {"id": 42, "filename":"ab12...png", "url":"/api/media/ab12...png", ...}
230+
```
231+
232+
Then embed it in a page's `content_md`:
233+
234+
```markdown
235+
![Architecture](/api/media/ab12...png)
236+
```
237+
238+
The server scans page content on write and records each media reference in
239+
`media_references`. Referenced media survives media-list filtering and cannot
240+
be deleted until all referencing pages are purged.
241+
242+
### 4. Tag a page
243+
244+
```bash
245+
curl -sS -b cookies.txt -X POST \
246+
http://localhost:8000/api/pages/release-checklist/tags \
247+
-H 'Content-Type: application/json' \
248+
-d '{"name":"ops"}'
249+
```
250+
251+
`POST` creates the tag if it doesn't exist. `DELETE /api/pages/{slug}/tags/{tag_name}`
252+
removes it; orphan tags are cleaned up automatically.
253+
254+
### 5. Restrict a page with ACL
255+
256+
```bash
257+
# Grant a group read-only, a user write. Any existing rows are replaced.
258+
curl -sS -b cookies.txt -X PUT \
259+
http://localhost:8000/api/pages/release-checklist/acl \
260+
-H 'Content-Type: application/json' \
261+
-d '{
262+
"rows": [
263+
{"principal_type":"group","principal_id": 3, "permission":"read"},
264+
{"principal_type":"user", "principal_id": 7, "permission":"write"}
265+
]
266+
}'
267+
```
268+
269+
Resolution rule: walk the `parent_id` chain, find the shallowest ancestor
270+
with any ACL rows (the "anchor"), and take the most-permissive matching row
271+
for the caller. No anchor ⇒ default open (write for editor, read for viewer).
272+
273+
Clear with `DELETE /api/pages/{slug}/acl` — the page then re-inherits.
274+
275+
### 6. Move a page
276+
277+
```bash
278+
curl -sS -b cookies.txt -X PATCH \
279+
http://localhost:8000/api/pages/release-checklist/move \
280+
-H 'Content-Type: application/json' \
281+
-d '{"parent_id": 12, "sort_order": 5}'
282+
```
283+
284+
Refuses with 400 if it would create a cycle. Requires write on both source
285+
and destination parent.
286+
287+
### 7. Soft-delete and restore
288+
289+
```bash
290+
# Soft delete (moves to trash; slug stays reserved)
291+
curl -sS -b cookies.txt -X DELETE http://localhost:8000/api/pages/release-checklist
292+
293+
# Restore
294+
curl -sS -b cookies.txt -X POST http://localhost:8000/api/trash/release-checklist/restore
295+
```
296+
297+
Admin-only hard delete: `DELETE /api/trash/{slug}`.
298+
299+
### 8. Revert to an old version
300+
301+
```bash
302+
curl -sS -b cookies.txt http://localhost:8000/api/pages/release-checklist/versions
303+
curl -sS -b cookies.txt -X POST \
304+
http://localhost:8000/api/pages/release-checklist/revert/2
305+
```
306+
307+
Reverting snapshots the current content into a new version first, then writes
308+
the old content back and bumps `version` — a subsequent concurrent editor
309+
will get a 409 as expected.
310+
311+
## Markdown conventions
312+
313+
Use stock GFM plus these JustWiki extensions. The backend parses them on
314+
write to keep backlinks and media refs in sync; the viewer renders them.
315+
316+
- **Wikilinks**`[[slug]]` or `[[slug|display text]]`. Creates a
317+
`backlinks` row and renders as a link to `/wiki/<slug>`. Auto-slug keeps
318+
CJK characters (e.g. `[[專案規劃]]`).
319+
- **Transclusion**`![[slug]]` inlines another page's rendered content.
320+
- **Callouts** — fenced blocks: `:::info` / `:::tip` / `:::warning` / `:::danger`
321+
ending with `:::`.
322+
- **Math**`$inline$` and `$$display$$` (KaTeX).
323+
- **Mermaid**```` ```mermaid ... ``` ```` fences.
324+
- **Draw.io**`::drawio[diagram_id]` embeds.
325+
326+
`content_md` is stored verbatim; the viewer sanitises on render with
327+
DOMPurify. You can assume raw HTML in markdown will be stripped.
328+
329+
## Pitfalls an agent should know about
330+
331+
1. **Always fetch before editing.** Skipping the read to get `version` is the
332+
fastest way to hit `base_version_required` or `conflict`.
333+
2. **Slugs are case-sensitive URL keys.** They're derived from the title;
334+
if you need a specific slug, pass it explicitly on `POST /api/pages`.
335+
3. **404 may mean "no permission".** The server returns 404 instead of 403
336+
for pages the caller can't read, so treat 404 on known slugs as "maybe
337+
ACL-blocked."
338+
4. **Soft-delete keeps the slug reserved.** To reuse a slug, either restore
339+
the trashed page or admin-purge it first.
340+
5. **View counts bump on GET** — reading `/api/pages/{slug}` is a write in
341+
disguise. Dedup'd per (user, page) over `VIEW_DEDUP_MINUTES`, but still
342+
something to know if you're scraping.
343+
6. **Editor and Viewer are separate render paths.** The dual pipeline lives
344+
on the frontend; content written via the API goes through the viewer path
345+
on display. If you're testing a markdown feature end-to-end, also click
346+
through the Editor in the UI.
347+
7. **Rate limits.** Login: 5/min/IP. AI chat: `AI_RATE_LIMIT_PER_HOUR` per
348+
user. Public reads: 60/min/IP. No global write limit, but respect 429.
349+
350+
## Quick session template
351+
352+
```bash
353+
# 1. Authenticate once (Profile → API Tokens → Copy)
354+
export JW_TOKEN="jwk_xxxxxxxxxxxx..."
355+
AUTH=(-H "Authorization: Bearer $JW_TOKEN")
356+
357+
# 2. Read current state
358+
curl -sS "${AUTH[@]}" http://localhost:8000/api/pages/my-page | jq '{slug,version}'
359+
360+
# 3. Mutate with base_version
361+
curl -sS "${AUTH[@]}" -X PUT http://localhost:8000/api/pages/my-page \
362+
-H 'Content-Type: application/json' \
363+
-d '{"content_md":"…new content…","base_version":7}'
364+
```
365+
366+
## Further reading in this repo
367+
368+
- `backend/app/routers/` — one file per domain, each handler is short; read
369+
the one matching your endpoint for the exact contract.
370+
- `backend/app/schemas.py` — Pydantic request/response models.
371+
- `backend/app/services/acl.py` — the single source of truth for permission
372+
resolution; every router calls these helpers.
373+
- `CLAUDE.md` — project-wide conventions and the dev workflow.

0 commit comments

Comments
 (0)