Metadata (GET /api/artifacts/{id}) stays metadata-only; the full content now
has its own endpoint so an agent can verify exactly what it posted.
- New ArtifactRawView at /api/artifacts/{id}/raw: bearer-or-session auth,
owner-scoped. Serves exact source text (markdown/html_full/json/csv), file
bytes (image/pdf/binary), or 404 for render-only kinds (html_fragment/deck).
Reuses raw_artifact_path/safe_artifact_path.
- serialize_artifact exposes api_raw_url; /api/capabilities advertises the
route. (The old /a/{id}?raw= viewer stays session-only for browsers.)
- Docs + tests (exact content, owner-scoping, 403 unauth, 404 render-only).
Suite: 136 passing; verified live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes an inconsistency an agent hit: it could POST/PATCH/DELETE with its token
but GET (readback) required a session and 302-redirected to the browser login.
- GET /api/artifacts and GET /api/artifacts/{id} now accept a bearer token OR
a session, owner-scoped (a token reads only its owner's artifacts). Agents
can read back what they posted with the same token.
- Unauthenticated API requests return a JSON 403, never a browser-login
redirect (resolve_request_user helper; dropped @login_required on the JSON
read views).
- GET /api/skill and GET /api/capabilities are now public (no user data), so
an agent can fetch the contract + posting instructions before it has a key.
- Docs (api.md, CLAUDE.md) updated; tests for bearer readback, owner-scoping,
and the JSON-403-not-redirect behavior. Suite: 132 passing; verified live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes the "Daybook" name from everything user- and agent-facing, since no
agents are configured yet and this is the cheap moment to do it.
- UI brand, docs, README, CLAUDE.md -> AgentFeed.
- Ingest env vars DAYBOOK_URL/TOKEN/SOURCE/DATA_DIR/BIND_ADDR/HOST_DATA_DIR/
KEY_PROVISIONING_API -> AGENTFEED_* (token *values* unchanged).
- Skill: skills/daybook-posting -> skills/agentfeed-posting, daybook_post.py
-> agentfeed_post.py (+ scripts/ wrapper and its test).
- static/daybook.css -> static/agentfeed.css; localStorage daybook.* ->
agentfeed.*; templatetag daybook_extras -> agentfeed_extras; multipart
boundaries; json/csv viewer element ids; daybook_exception_handler ->
agentfeed_exception_handler; compose service/container -> agentfeed.
- Key-provisioning API now OFF by default (independent of DEBUG); enable with
AGENTFEED_KEY_PROVISIONING_API=1 on test installs only.
- Posting helper now points at Settings -> API Keys.
Deferred on purpose (a later, separate step): the Django package
`daybook_project`, the `/opt/daybook` install path, and the Forgejo repo name.
The sibling AGENT_DAYBOOK__LLM_TEST name is intentionally untouched.
Full suite: 129 passing; live server verified serving under the new name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the single per-user DRF token with named, individually-revocable
API keys, so you can run one key per agent and cut off a leaked one without
disrupting the others.
- New ApiKey model (user, name, dbk_-prefixed key, created_at, last_used_at).
Data migration copies every existing authtoken 1:1 into a "default" named
key with the same value, so no client (incl. the sibling test project's
hardcoded test-user key) needs reconfiguring.
- Auth (resolve_token_user) resolves against ApiKey and stamps last_used_at,
throttled to one write/minute.
- Settings -> API Keys: table with name/key/created/last-used + per-row
revoke and a create form; keys viewable on demand. Settings -> Account now
links here; Settings -> Users issues a named initial key.
- Gated provisioning API for test installations: GET/POST /api/keys and
DELETE /api/keys/{id}, controlled by DAYBOOK_KEY_PROVISIONING_API (unset ->
follows DEBUG, so off in deployment). Advertised via key_provisioning_api
in /api/capabilities; 403 when disabled.
- Docs (api/deployment/quickstart/posting-helper), SKILL.md, and env.example
updated from "token / Settings -> Account" to "key / Settings -> API Keys".
- Tests: new API + UI suites for keys; existing token-based tests moved to
ApiKey. Full suite 129 passing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Serves skills/daybook-posting/SKILL.md so users/agents can fetch the
posting instructions from the running instance instead of hunting for the
file. The server rewrites the DAYBOOK_URL config line to the request's own
origin, so the text handed to an agent already points at the right server;
the recipient only supplies their token.
- Same auth as /api/capabilities (session or bearer token); no user data.
- Raw text/markdown by default, ?format=json returns {version, url, skill}.
- Advertised via skill_url + a /api/skill methods entry in capabilities.
- Ships in the Docker image (COPY . /app, skills/ not dockerignored).
- Tests for auth, origin rewrite, JSON mode, session auth, capabilities ad.
- docs/api.md + CHANGELOG updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Records the decision to keep html_full fully flexible: no sanitization, no
CSP, and no HTML/CSS validation — malformed markup is kept as-is. The
themed kinds (markdown/html_fragment/deck) remain sanitized, which also
normalizes them to well-formed HTML.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
markdown/deck/html_fragment rendered author-influenced content verbatim,
so <script>/<img onerror>/etc. executed on the Daybook origin — self-XSS
against the owner's session, and (post-sharing) unauthenticated via /s/.
Two layers, both in artifacts/views.py:
- Create-time allowlist sanitization via nh3 (maintained ammonia/Rust
sanitizer, not deprecated bleach). Keeps formatting/tables/links/images
(incl. relative /assets URLs) and codehilite markup; drops script, on*
handlers, style, unknown tags, and javascript:/data: URLs. html_full is
deliberately NOT sanitized (documented verbatim escape hatch).
- Serve-time CSP (script-src 'none') for the sanitized kinds, on both
/a/{id} and the unauthenticated /s/{token} route. Being a response
header, it also neutralizes scripts in artifacts stored before this fix.
Not applied to html_full or json/csv (json/csv data is already escaped;
their viewers need their own JS).
Verified live across all kinds; 7 regression tests added. 102 tests pass.
Not addressed (noted in the issue): CSV formula injection,
CSRF_COOKIE_HTTPONLY (moot with XSS closed), nonce CSP for json/csv/full.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same pass as the SKILL.md review. Fixed factually-wrong content and filled
gaps against the current code.
docs/api.md:
- Capabilities example response was stale: artifact_kinds missing json/csv,
raw_supported_for missing json/csv. Corrected to match live output, with
a note that the endpoint is the authoritative source.
- "Office document conversion is not implemented" is now false - replaced
with the accurate scope (.xlsx -> csv only; .docx/.pptx/.xls not).
- Source Fields overview table omitted source_prompt entirely; added it,
plus a note that source_name/source_id/source_prompt must be strings
(400, not coerced) and metadata must be an object.
- Kinds table: noted tables now render styled in markdown/html_fragment,
and made explicit that html_full is served verbatim with no styling.
- MVP constraints: noted the public-share-link exception to "feeds private
per user" and clarified PATCH edits metadata only.
docs/llm_posting_helper.md:
- Added .xlsx upload, --upload-image image embedding, a Source & Theme
Flags table (all six source-*/theme flags were undocumented here), and a
note that --meta/--meta-json merge and are distinct from tags.
- Added a scope note that the helper only creates - update/delete need the
API, sharing is feed-UI-only.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Uploading a .xlsx file no longer becomes a generic downloadable binary -
it's read server-side with openpyxl and converted to a csv artifact,
rendering in the same interactive grid as a native CSV post. Detected by
content type or .xlsx extension (some clients send generic/incorrect
content types for multipart uploads). Only the active sheet converts;
formula cells use whatever cached result the spreadsheet software last
saved (openpyxl doesn't evaluate formulas itself). Corrupt/unreadable
files return 400.
Since the existing posting script already forwards any non-text file via
multipart with no special-casing, `daybook_post.py balance_sheet.xlsx`
already works with zero script changes - verified live.
Two real bugs found and fixed while building this, both now covered by
regression tests:
- {**a_querydict, ...} silently wrapped every value (including title) in
a list - Python's ** spread on a dict subclass bypasses QueryDict's
__getitem__ override and exposes its internal one-list-per-key storage
directly. Fixed by building the delegated dict explicitly via .get().
- Pre-setting None for absent optional fields (e.g. source_prompt) defeated
create_text_artifact's own data.get(field, default) calls, since .get()
only falls through to its default when the key is *missing*, not when
the value is None - caused a NOT NULL constraint failure. Fixed by only
setting keys that were actually provided.
- csv.writer's default "\r\n" terminator was getting doubled by
Path.write_text()'s own newline translation on Windows, producing a
blank line between every row. Fixed with lineterminator="\n".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Any artifact can now be made publicly viewable via a share link, no login
required - opt-in per artifact, toggled from the feed UI next to Keep.
- Artifact.share_token: nullable, unique, high-entropy (secrets.token_urlsafe(32)).
NULL = not shared; SQL treats each NULL as distinct so uniqueness still
holds for the many not-shared rows.
- New public route GET /s/<token> (shared_artifact_view), completely
unauthenticated, serving the same rendered/raw content as the owner
route via a shared serve_artifact_file() helper.
- toggle_artifact_share: generates/clears the token: mirrors
toggle_artifact_keep's pattern.
- Fixed a real gap this surfaced: embedded images in a shared artifact's
content would otherwise break for an anonymous visitor, since asset_view
was owner-only. It now also allows access when the asset's parent
artifact is currently shared - unattached (orphaned) assets stay private
regardless.
- serialize_artifact now reports is_shared/share_url so the API reflects
sharing status (read-only for now; toggling stays UI-only).
- Feed UI: Share button (mirrors Keep's toggle interaction), a copyable
share-link box that appears when shared, and a "shared" badge on the
artifact row - distinct blue accent from Keep's green so both can be
visible on the same row without confusion.
Verified live end-to-end (toggle on/off, public access, fake-token 404,
old-link-dies-on-unshare, embedded-asset access before/after sharing)
before writing the 12 automated tests in tests/views/test_sharing.py.
Also fixed two unrelated stale spots noticed in docs/api.md while adding
the Sharing section: the example response was missing is_kept/size_bytes,
and the raw-content table didn't list json/csv (added earlier this
session but never documented there).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
#009: html_fragment's content-shape check is substring-based, so it can
false-positive on inert text inside <script>/<pre> (e.g. a JS string
literal that happens to contain "<html>"). Documented as a known
limitation in docs/api.md rather than implementing real HTML parsing - a
proper fix is a much bigger change than the narrow trigger justifies, and
substring matching is the same approach the client helper already uses.
#011: added a curated CDN Snippets section to SKILL.md for html_full
(Tailwind, Chart.js, with an offline/air-gapped caveat), scoped explicitly
to that kind only. Fixed daybook_post.py's --theme help text, which named
a non-existent "bootstrap" theme (written ahead of a vendored-Bootstrap
feature that was planned but never built, not a typo).
Also updated SKILL.md's kind cheat sheet, which never mentioned the
json/csv kinds and still pointed at the old --as-markdown workaround for
them - and filed issue #012 for a real gap that surfaced doing so: the
posting script itself doesn't support --kind json/csv yet, only the raw API
does.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Gap 1 from planning/2026-09-02-artifact_kind_gaps_for_frequent_agent_output.md.
Chose new first-class kinds over riding on html_fragment, resolving that
doc's open design question in favor of filtering support, raw-source
preservation (like markdown's .md sidecar), and server-owned validation.
- json: validated with json.loads at post time (400 on invalid JSON),
rendered with renderjson (collapsible tree), vendored locally.
- csv: parsed server-side with the stdlib csv module into columns/rows and
handed to Grid.js as plain data - no client-side CSV-parsing plugin
needed, keeping normalization server-owned. Vendored locally (MIT).
- Both preserve their original text as a sidecar file, generalizing what
was previously markdown-only handling (storage.py's SIDECAR_SUFFIXES) so
disk-size accounting, eviction cleanup, and ?raw=1 all cover json/csv too.
- Raw JSON is embedded in a <script type="application/json"> block with
`</script` occurrences escaped (safe because JSON's \/ escape round-trips
correctly) - verified with a live </script>-injection payload against the
actual running server, not just unit tests.
New migration (0012) for the KIND_CHOICES change - SQLite doesn't enforce
choices at the schema level, but Django's migration state tracks it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Enables codehilite on the markdown renderer (Pygments, 'friendly' style,
vendored as static/themes/pygments.css) so fenced code blocks get real
syntax highlighting with zero client-side JS - matching the "themes are
CSS-only" stance in framework_artifact_rendering.md. Verified live against
the running dev instance: a fenced python block renders with highlighted
keyword/string/function spans.
This was Gap 2 from planning/2026-09-02-artifact_kind_gaps_for_frequent_agent_output.md,
picked as the first fix since it needed no new kind or design decision -
just enabling an existing markdown extension.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- #006 (high, data loss): make_room_for() no longer deletes unkept
artifacts on a doomed request - checks whether eviction can possibly
succeed before deleting anything. Also fixed a related latent bug found
while testing: the function returned evicted Artifact objects after
calling .delete() on them, so their .id was always None (Django clears
pk on delete); now returns ids captured beforehand.
- #003: added server-side field-length validation (source/title/
source_name/source_id/theme) on POST and PATCH, matching the model's
own max_length limits, which were previously unenforced on the ingest
path entirely.
- #004: ported daybook_post.py's content-shape validation
(html_fragment/html_full/deck/markdown) to the server, closing the gap
for any client that doesn't go through the helper script.
- #005: PATCH now rejects wrong-typed `tags` (previously silently wiped
existing tags) and rejects immutable fields (content/source/kind/day)
with 400 instead of silently no-op'ing them.
- #002: deleted the misleading skills/daybook-posting/.env.example (a
zero-dependency script that never reads .env files) and clarified
SKILL.md/docs/llm_posting_helper.md that these must be real
process/shell env vars.
Adds tests/unit/ for storage-layer tests. 46 tests passing (was 25).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds docs/deployment.md, a platform-agnostic guide (any Linux host with
Docker) built around a git-pull based fast-redeploy workflow, replacing the
Proxmox-specific deploy/proxmox_llm_deploy.md which was also stale (old
`persona` field, single-shared-token model predating multi-user auth).
Also: drops the unused DAYBOOK_API_TOKEN from deploy/env.example, documents
the new DELETE endpoint and capabilities `methods` field in docs/api.md and
docs/agent_quickstart.md, and fixes CLAUDE.md's OpenCode note now that
opencode.json is untracked (machine-specific MCP config).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Daybook: a self-hosted, multi-user Django + DRF app where agents, scripts,
or people publish artifacts and each logged-in user browses their own
time-ordered, label-filterable feed.
This squashes prior local development history (kept privately on a separate
branch) into a single clean starting point for the public repository.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>