- frontend-api.md (#986): the list, search, and recent routes return bare
JSON arrays, not `{ "workspaces": … }`-style wrappers (the route tests
assert `as_array()`); a page read returns `body_markdown`, not `body`; a
search hit carries workspace/project/kind and no `id`.
- windows.md (#758): native `ai-memory upgrade` is done (#801/#802), not
in-progress.
- managed-workstreams.md + support matrix (#987): document the Codex shared
daemon handing hooks a stale AI_MEMORY_RUN_ID and the `--no-daemon`
workaround until the server-side fix lands.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
Add a server-rendered page under /web that lists the pending
auto-improvement proposals of all projects. The page has a project
filter and a sort. It shows the rationale, the proposed body, and a
warning for proposals that write under _rules/ or replace a page.
The approve and reject buttons post from the browser to the existing
/admin/pending-writes/{id}/approve|reject handlers with the session
cookie and the CSRF header. Admission, audit, attribution, and the
single writer stay the same. ai-memory-web adds no write route.
The page uses the same Capability::Admin decision as /admin, and
serve passes the trusted-proxy setting to it. A non-root session gets
a 403 page. The HTML auth redirect does not send that session to the
change-password form.
Two store reads feed the page. One counts pending proposals per
project, so the total and the project filter include every project.
The other lists proposals, optionally for one project, with a cap of
500 rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwU4vj4tZLYVLR7vEKeh2W
This update introduces public HTML forms for user authentication at `/login` and `/change-password`. Unauthenticated browser GET requests to the wiki now redirect to these pages instead of returning JSON 401/403 responses. The existing API endpoints remain unchanged, ensuring JSON clients continue to receive appropriate responses. This enhancement improves user experience by providing a seamless transition to authentication for web users.
* feat(auth): add browser sessions and API credentials
* fix(auth): preserve 1.x compatibility paths
* fix(changelog): preserve released sections
* docs(auth): tie mirror triggers to 2.0 cutover
* docs(store): correct mirror migration version
* docs(changelog): link admin console PR
* fix(migrations): renumber human_auth/api_credentials to V54/V55
Main gained V52__purged_sessions_tombstones and V53__page_embed_failures
after this branch was opened, so the merge produced four migration files
across two version numbers. Git saw no conflict - the filenames differ -
and the collision only surfaces at runtime:
UNIQUE constraint failed: refinery_schema_history.version
on a fresh database, so the merged tree could not open a store at all.
Renumbered V52__human_auth -> V54 and V53__api_credentials -> V55, and
shifted the version numbers the tests pin: `run_to`/`open_to` targets, the
`schema_version` assertions, the rollback assertion, and the test names.
The pre-migration fixture point moves 51 -> 53 because main's V52/V53
create unrelated tables (purged_sessions, page_embed_failures) and touch
neither `users` nor any table these migrations rewrite.
Migration content is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
---------
Co-authored-by: AkitaOnRails <fabioakita@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Both docs described a browser edit surface as "a deliberate v2
conversation", which reads as planned-but-later. It is not planned: the
wiki is a record of what a project produced, authored by automated
summarisation over captured observations, and retrieval, provenance and
the audit trail all assume nobody went back and adjusted it. A
hand-edited page stops answering "what did this project produce" and
starts answering "what did someone want it to say", with no way to
distinguish the two afterwards.
That wording is what prompted #482 to ask for scoping on work that was
never going to be accepted, so the cost of leaving it was real.
States the boundary rather than only the refusal: human input is
first-class through `memory_write_page`, which lands annotations in the
same lineage as everything else. The line is between adding to the
record through the normal path and editing it from outside.
Retitles the frontend-api section from "Known gaps (planned iterations,
not blockers)" to "Known gaps and deliberate non-goals", since it now
holds an entry that is deliberately neither, and updates the one link
pointing at the old anchor.
Expose the store's scoped session readers to third-party frontends:
- `GET /workspaces/{ws}/projects/{project}/sessions?limit&offset&include_open`
lists the sessions that touched the project (row anchored there or at least
one observation there), newest first, owner-filtered like handoffs.
- `GET .../sessions/{session_id}/observations?limit&offset&order&kinds&q&body_max_chars`
pages one session's raw hook observations in that scope, with `total`,
`elided_other_scope`, and per-body capping with a visible marker; a session
not visible in the scope for the caller is a 404, bad ids/kinds/order a 400.
Both responses are `no-store` because they carry prompt-derived text. Document
the routes in `docs/frontend-api.md` and record the whole #401 read path in the
CHANGELOG.
Post-merge audit of #396 found two documentation gaps:
- docs/frontend-api.md still described the 500 body as the source error
chain, which #396 replaced with a fixed generic body on every route.
- CHANGELOG had no entry for the /api/v1 500-body redaction or for the
/web cookie moving from SameSite=Lax to SameSite=Strict, both of which
are user-visible public-surface changes under the changelog merge gate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On a shared server the open-handoff lookup was scoped by (workspace,
project, state) alone, so the next session to start — whoever it belonged
to — consumed the pending baton, and delivery is destructive: the author
simply lost it. cwd did not help, because memory_handoff_begin rows are
manual (from_session_id = NULL), and manual handoffs bypass the cwd rule
and outrank automatic ones — the deliberate artefact was exactly the one
that crossed operators. Sessions had the sibling hazard: finalize-session
picks "the newest open session in the scope" and acts destructively on it
(ends it, synthesises a page from its observations, mints a handoff from
its raw prompts), across everyone.
Design: handoffs and sessions record their operator (migrations V39/V40)
as the qualified IdentityKey::storage_key() TEXT the identity contract
defines — never a raw name, so a username can never alias an equal OIDC
subject. The read side is OwnerFilter (own rows + shared), the write side
is owner_stamp, which stamps only where the deployment actually
distinguishes operators: a single-operator server that names its operator
via [auth].root_username keeps writing the pre-ownership NULL, or its
HTTP writes would become invisible to the same person's stdio transport.
Ownership is checked BEFORE the manual/cwd delivery rules; the claim
predicate rides inside the accept UPDATE's WHERE so an unadmitted caller
changes 0 rows and is told so (no double delivery on a lost race); and
both supersession sweeps bind the triggering row's owner NULL-safely
(owner_user IS ?), so one operator starting or ending a session can no
longer expire another operator's pending baton.
Invariants throughout: absent owner = shared = pre-feature behaviour, so
every stored row, every unauthenticated server, and every caller without
an actor behaves exactly as before; cross-operator escape hatches
(any_owner on accept/cancel, --all-owners on finalize-session) require
admin authority; the SessionEnd path attributes the page, the checkpoint
and the baton to the operator recorded on the session, not to whoever
delivered the event; briefing counts and the read-only overviews apply
the same filter as the fetch so a count never advertises a baton its
caller cannot retrieve. A new owner-scoped listing endpoint (V41 index)
makes a mis-delivered baton inspectable, redacting prompt-derived fields
from callers an authenticating server can neither name nor place as
root. Handoff lifecycle events raise admission ops (handoff_begin /
handoff_accept / handoff_cancel) in one fixed order — deciders before
the operation, observers only after it happened — and a refusal on the
automatic paths degrades the event (baton skipped, claim left open)
rather than failing it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-merge audit Phase 7 live test caught this — `pr-audit` of #79
in isolation couldn't see it. PR #79 added the `/favicon.ico` route
to `crates/ai-memory-web/src/routes/mod.rs::build(...)`, which the
host then nests under `/web` via `mount_web_router`. So in the
shipped 0.12.0 deployment the route lived at `/web/favicon.ico` —
unreachable by the browser's automatic root fetch. The icon still
appeared in tabs because base.html's `<link rel="icon" href="static/
logo.png">` works via the injected `<base href>`, but the
dedicated route was dead code in practice.
Fix:
- Move the favicon route out of the web router and into a separate
`favicon_router()` exposed publicly from `ai-memory-web`.
- The serve handler merges it at the OUTER router — after the
base-path nest, OUTSIDE both `apply_http_layers` and the
`--base-path` prefix.
Two side effects, both intentional:
1. The favicon is now exempt from bearer auth and the host
allowlist. Browsers fetch `<host>/favicon.ico` without the
user's HTTP Basic credentials; gating it 401 would just be
theatre — the embedded PNG is the same one any /web visitor
already sees, info-leak surface is nil, and the alternative is
"broken icon in fresh tabs".
2. Under `--base-path /wiki`, the favicon STAYS at root rather
than moving to `/wiki/favicon.ico`. Browsers fetch the URL at
the host origin regardless of where the app is mounted; the
route must not move with the prefix or it becomes invisible
again under a subpath deploy.
Tests:
- New `favicon_lives_at_host_root_regardless_of_base_path`
exercises both `base_path = ""` and `base_path = "/wiki"`,
asserting 200 + `image/png` at `/favicon.ico` in both cases.
- `based_web_router` test helper mirrors the production favicon
mount so every downstream test sees the route at the same path
the browser hits.
docs(audit-P2): correct the /favicon.ico section in frontend-api.md.
The earlier doc claimed the favicon "rides the same HTTP Basic
credentials" — that was the broken (pre-fix) mount path's behavior,
not the desired one. Now documented as exempt from auth/host
allowlist with the rationale spelled out.
CHANGELOG entry added under [Unreleased] / Fixed citing #79.
946 → 947 workspace tests; fmt + clippy clean.
Post-merge audit Phase 4 surfaced two doc-staleness items the
per-PR audits couldn't see in isolation:
- docs/windows.md mentioned only `AI_MEMORY_HOOK_PLATFORM=windows-bash`
(the escape-hatch value). The default `windows-native` value PR #84
introduced was never named — an operator reading the doc to opt
back into bash had no list of valid platform values to choose from.
- docs/frontend-api.md endpoint reference omitted the new
`GET /favicon.ico` route PR #79 added. Added §4.10 describing it.
The favicon section also nails down the bearer-auth answer (it rides
the `/web/*` HTTP Basic credentials, not exempt) so a future
"why isn't my favicon showing" issue points at the auth answer
directly.
No code touched.
Stale-docs audit across the operator-facing surface. None of the
underlying behaviour changed — these are doc fixes for behaviour that
already shipped over the last week of PRs (#60 move-project,
#65 base-path, #68 wikilinks, #70 openai-compat strict, plus the
audit-cleanup follow-ups in the 65682dc…0d32be1 range).
Operator docs
- `README.md`: status badge bumped from v0.2 to v0.8, project
derivation paragraph rewritten to describe the CLI's main-repo-root
resolution (worktrees share one project) vs the hook router's
basename default, write-page example switched to the H1-in-body
convention (passing `--title` still works but invites issue #67's
JSON-escape footgun).
- `docs/install.md`: bootstrap `--project` default is now described as
"derived from cwd" instead of the stale `"scratch"`. Added an
"Optional serve flags" table covering `--base-path`, `--web-slug`,
`--web-ui-dir`, `--cors-allow-origin`.
- `docs/deploy.md`: cross-link to the "Hosting under a subpath"
section in `https-via-proxy.md`.
Admin / lifecycle docs
- `docs/admission-webhooks.md`: payload sample includes the new
`partial_failure` field (purge-project only, skipped on the wire
when false). Spelled out that admission now fires BEFORE the SQL
destruction in both `/admin/purge-project` and the
`/admin/move-project` copy-purge path, so a `Reject` webhook on
`purge_project` leaves the source intact. Documented that
copy-purge fires two distinct webhook events from one request.
- `docs/lifecycle-ops.md`: failure-mode table covers
`WikiError::DestinationExists` (409) and the block-policy 409 with
`conflicts` body. `--on-conflict` documented alongside the JSON
`on_conflict` field for direct `/admin/move-project` callers.
Dedup naming cites the `DEDUP_FROM_TOKEN` const so the doc
round-trips with the source. Safety-matrix `move-project` row
notes the reject-policy escape hatch.
Frontend / web docs
- `docs/frontend-api.md`: removed two stale "Known gaps" claims —
Cache-Control + ETag and CORS both ship today; §5 documents the
cache headers and §9 is a new CORS section. Added `/api/v1/graph`
to the endpoint reference (§4.9). §6 covers the base-path
normaliser's safety rules (dot-segment rejection, fall-back-to-root
on unsafe chars, query-preserving trailing-slash redirect).
- `docs/usage.md`: wikilink description spells out the "literal in
code" guarantee (fence-glyph tracking, indented code, inline code)
and the external-scheme allowlist; cross-links to the base-path
flag docs.
- `docs/https-via-proxy.md`: subpath section now mentions the
dot-segment rejection and the silent root-fallback warning.
LLM / MCP docs
- `docs/llm-provider-comparison.md`: new section on
`AI_MEMORY_LLM_COMPAT_STRICT`, including the narrowed parse-shape
fallback (5xx / auth / transport errors propagate now), the
two-call cost of fallback, and a per-engine recommendation table.
"When to revisit" entry on strict-JSON-schema availability rewritten
to reflect that the feature exists today.
- `docs/mcp-install.md`: verify-it-works tool list now includes
`memory_read_page` and `memory_delete_page` (their omission would
have made users think their install was broken).
863 workspace tests still pass; fmt + clippy clean. No code touched.
The README lists endpoints but a 3rd-party frontend dev needs more: auth
flow, response schemas (PageHit, BriefingSnapshot, HealthDetail, …),
error model, limits/pagination, custom-UI hosting, a fetch+curl example,
and pointers to the canonical source-of-truth files. This doc fills that
gap and the README points to it.
Doc-only — no behaviour change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>