v0.3.0: rename app Daybook -> AgentFeed
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>
This commit is contained in:
parent
4cacd8f64e
commit
d30e2436ab
39 changed files with 303 additions and 284 deletions
|
|
@ -8,6 +8,6 @@ Run the demo seeding script to populate today with one tagged sample artifact pe
|
|||
python scripts/seed_demo_artifacts.py
|
||||
```
|
||||
|
||||
Requires the Daybook dev server to be running on the project's fixed local port: `python manage.py runserver 127.0.0.1:8001`. The script defaults to `DAYBOOK_URL=http://127.0.0.1:8001` / `DAYBOOK_TOKEN=dev-token`; no overrides needed unless the user has changed these.
|
||||
Requires the AgentFeed dev server to be running on the project's fixed local port: `python manage.py runserver 127.0.0.1:8001`. The script defaults to `AGENTFEED_URL=http://127.0.0.1:8001` / `AGENTFEED_TOKEN=dev-token`; no overrides needed unless the user has changed these.
|
||||
|
||||
After running, report each posted artifact's kind and URL, and remind the user to reload the feed for today.
|
||||
|
|
|
|||
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -1,13 +1,32 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to Daybook are recorded here. Format loosely follows
|
||||
All notable changes to AgentFeed are recorded here. Format loosely follows
|
||||
[Keep a Changelog](https://keepachangelog.com/); versions are SemVer and match
|
||||
the git tags (`vX.Y.Z`) and `daybook_project.__version__`.
|
||||
the git tags (`vX.Y.Z`) and `__version__`. (Entries before 0.3.0 use the app's
|
||||
former name, "Daybook".)
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
_Nothing yet._
|
||||
|
||||
## [0.3.0] — 2026-09-04
|
||||
|
||||
### Changed
|
||||
- **Renamed the app to AgentFeed.** The former name "Daybook" is removed
|
||||
from everything user- and agent-facing: the UI brand, docs/README, the
|
||||
posting skill (`agentfeed-posting` / `agentfeed_post.py`), CSS
|
||||
(`agentfeed.css`), localStorage keys, and the ingest **environment
|
||||
variables** — `DAYBOOK_URL/TOKEN/SOURCE/DATA_DIR/...` are now
|
||||
`AGENTFEED_*`. Agents must update their env-var names (the token *values*
|
||||
are unchanged). The Django package `daybook_project`, the `/opt/…` install
|
||||
path, and the git repo keep their names for now (a later, separate step).
|
||||
- Key-provisioning API (`POST /api/keys`) is now **disabled by default**,
|
||||
independent of `DEBUG`; enable it explicitly with
|
||||
`AGENTFEED_KEY_PROVISIONING_API=1` on a test install only.
|
||||
|
||||
### Fixed
|
||||
- Posting helper now points at Settings → API Keys (was Settings → Account).
|
||||
|
||||
## [0.2.1] — 2026-09-04
|
||||
|
||||
### Added
|
||||
|
|
@ -98,7 +117,8 @@ First tagged release.
|
|||
- Dockerised (Gunicorn + whitenoise), SQLite, `git`-based deploy with a
|
||||
runbook (`deploy/cloud_agent_deploy.md`) and health check.
|
||||
|
||||
[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.2.1...HEAD
|
||||
[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.3.0...HEAD
|
||||
[0.3.0]: https://git.cygnus.systems/peter/daybook/compare/v0.2.1...v0.3.0
|
||||
[0.2.1]: https://git.cygnus.systems/peter/daybook/compare/v0.2.0...v0.2.1
|
||||
[0.2.0]: https://git.cygnus.systems/peter/daybook/compare/v0.1.1...v0.2.0
|
||||
[0.1.1]: https://git.cygnus.systems/peter/daybook/compare/v0.1.0...v0.1.1
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Project
|
||||
|
||||
Daybook is a self-hosted, multi-user Django + Django REST Framework web app where AI agents, scripts, or people publish artifacts and each logged-in user browses their own time-ordered, label-filterable feed.
|
||||
AgentFeed is a self-hosted, multi-user Django + Django REST Framework web app where AI agents, scripts, or people publish artifacts and each logged-in user browses their own time-ordered, label-filterable feed.
|
||||
|
||||
The core product principle is: agents publish content, not files. The server owns IDs, styling, storage layout, dates, and normalization into browser-native HTML, image, or PDF outputs.
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ The core product principle is: agents publish content, not files. The server own
|
|||
- Use the local Django commands for normal iteration: `python manage.py check`, `python manage.py migrate`, and `python manage.py runserver`.
|
||||
- Run the local development server on `127.0.0.1:8001`. Port `8000` may be reserved or blocked on this workstation.
|
||||
- Always use this same fixed port for the dev server. Never start a second instance on a different port; if `8001` is already serving, reuse it rather than starting another.
|
||||
- All artifact-posting tools (`skills/daybook-posting/scripts/daybook_post.py`, `scripts/seed_demo_artifacts.py`, and any future posting skill) must default `DAYBOOK_URL` to `http://127.0.0.1:8001` to match.
|
||||
- All artifact-posting tools (`skills/agentfeed-posting/scripts/agentfeed_post.py`, `scripts/seed_demo_artifacts.py`, and any future posting skill) must default `AGENTFEED_URL` to `http://127.0.0.1:8001` to match.
|
||||
- A regular (non-superuser) user `test` / password `testpwd` must always exist in the local dev database — it's the permanent account a sibling project (`AGENT_DAYBOOK__LLM_TEST`) uses for API testing. If the local `db.sqlite3` is ever reset, recreate this exact user (e.g. via Settings → Users) before assuming that project is broken.
|
||||
- Do not deploy to the Proxmox NUC during routine code iteration.
|
||||
- Treat the NUC container as the local live/staging target. Deploy there only at explicit stable checkpoints, such as an end-of-day build.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Daybook
|
||||
# AgentFeed
|
||||
|
||||
Daybook is a self-hosted, multi-user **artifact journal**. AI agents, scripts,
|
||||
AgentFeed is a self-hosted, multi-user **artifact journal**. AI agents, scripts,
|
||||
or people publish finished outputs — reports, HTML, images, PDFs, decks, JSON,
|
||||
CSV — and each logged-in user browses their own time-ordered, label-filterable
|
||||
feed. The core principle: **agents publish content, not files.** The server
|
||||
|
|
@ -35,9 +35,9 @@ Then open <http://127.0.0.1:8001/> and log in.
|
|||
|---|---|
|
||||
| [`docs/agent_quickstart.md`](docs/agent_quickstart.md) | Shortest path to posting from an agent |
|
||||
| [`docs/api.md`](docs/api.md) | Full REST API reference (kinds, endpoints, keys, sharing) |
|
||||
| [`docs/llm_posting_helper.md`](docs/llm_posting_helper.md) | Using the bundled `daybook_post.py` helper |
|
||||
| [`docs/llm_posting_helper.md`](docs/llm_posting_helper.md) | Using the bundled `agentfeed_post.py` helper |
|
||||
| [`docs/deployment.md`](docs/deployment.md) | Docker deployment, reverse proxy, `/admin/`, backup |
|
||||
| [`skills/daybook-posting/SKILL.md`](skills/daybook-posting/SKILL.md) | The agent-posting skill (also served at `/api/skill`) |
|
||||
| [`skills/agentfeed-posting/SKILL.md`](skills/agentfeed-posting/SKILL.md) | The agent-posting skill (also served at `/api/skill`) |
|
||||
| [`CHANGELOG.md`](CHANGELOG.md) | Release history |
|
||||
|
||||
## Admin
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from rest_framework.response import Response
|
|||
from rest_framework.views import exception_handler
|
||||
|
||||
|
||||
def daybook_exception_handler(exc, context):
|
||||
def agentfeed_exception_handler(exc, context):
|
||||
"""DRF exception handler that additionally maps pathological-input crashes
|
||||
to a clean 400 instead of an unhandled 500.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ from django import forms
|
|||
from django.contrib.auth.forms import AuthenticationForm
|
||||
|
||||
|
||||
class DaybookLoginForm(AuthenticationForm):
|
||||
class AgentFeedLoginForm(AuthenticationForm):
|
||||
remember_me = forms.BooleanField(required=False, initial=True)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from django.db import models
|
|||
|
||||
|
||||
def generate_api_key():
|
||||
"""`dbk_` prefix (recognizable as a Daybook key) + 40 hex chars."""
|
||||
"""`dbk_` prefix (recognizable as a AgentFeed key) + 40 hex chars."""
|
||||
return "dbk_" + secrets.token_hex(20)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ from rest_framework.response import Response
|
|||
from rest_framework.views import APIView
|
||||
|
||||
from .demo_data import create_demo_artifacts
|
||||
from .forms import DaybookLoginForm
|
||||
from .forms import AgentFeedLoginForm
|
||||
from .models import ApiKey, Artifact, Asset, TagColor
|
||||
from .storage import (
|
||||
QuotaExceeded,
|
||||
|
|
@ -66,7 +66,7 @@ REMEMBER_ME_SESSION_AGE = 60 * 60 * 24 * 30 # 30 days
|
|||
|
||||
MAX_ASSET_BYTES = 50 * 1024 * 1024 # 50 MB per uploaded image
|
||||
|
||||
# Matches the asset id in a Daybook asset URL, e.g. /assets/ab12cd34ef56
|
||||
# Matches the asset id in a AgentFeed asset URL, e.g. /assets/ab12cd34ef56
|
||||
ASSET_URL_RE = re.compile(r"/assets/([0-9a-f]{12})")
|
||||
|
||||
|
||||
|
|
@ -315,7 +315,7 @@ class CapabilitiesView(APIView):
|
|||
"/api/capabilities": ["GET"],
|
||||
"/api/skill": ["GET"],
|
||||
}
|
||||
if settings.DAYBOOK_KEY_PROVISIONING_API:
|
||||
if settings.AGENTFEED_KEY_PROVISIONING_API:
|
||||
methods["/api/keys"] = ["GET", "POST"]
|
||||
methods["/api/keys/{id}"] = ["DELETE"]
|
||||
|
||||
|
|
@ -330,30 +330,30 @@ class CapabilitiesView(APIView):
|
|||
"auth": "bearer_token",
|
||||
"methods": methods,
|
||||
"skill_url": request.build_absolute_uri(reverse("skill-api")),
|
||||
"key_provisioning_api": settings.DAYBOOK_KEY_PROVISIONING_API,
|
||||
"key_provisioning_api": settings.AGENTFEED_KEY_PROVISIONING_API,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
SKILL_MD_PATH = settings.BASE_DIR / "skills" / "daybook-posting" / "SKILL.md"
|
||||
SKILL_MD_PATH = settings.BASE_DIR / "skills" / "agentfeed-posting" / "SKILL.md"
|
||||
|
||||
|
||||
def render_posting_skill(origin):
|
||||
"""Return the bundled posting SKILL.md with its DAYBOOK_URL placeholder
|
||||
"""Return the bundled posting SKILL.md with its AGENTFEED_URL placeholder
|
||||
rewritten to this instance's origin, so text handed to an agent already
|
||||
points at the right server. Only the config-block assignment is touched;
|
||||
the surrounding prose (which explains DAYBOOK_URL) is left alone."""
|
||||
the surrounding prose (which explains AGENTFEED_URL) is left alone."""
|
||||
text = SKILL_MD_PATH.read_text(encoding="utf-8")
|
||||
return re.sub(
|
||||
r"(?m)^(DAYBOOK_URL=)\S+",
|
||||
r"(?m)^(AGENTFEED_URL=)\S+",
|
||||
lambda m: m.group(1) + origin,
|
||||
text,
|
||||
)
|
||||
|
||||
|
||||
class SkillView(APIView):
|
||||
"""Serve the bundled agent-posting skill (skills/daybook-posting/SKILL.md),
|
||||
with DAYBOOK_URL pre-filled to this instance so a recipient posts to the
|
||||
"""Serve the bundled agent-posting skill (skills/agentfeed-posting/SKILL.md),
|
||||
with AGENTFEED_URL pre-filled to this instance so a recipient posts to the
|
||||
right server. Same auth as capabilities: a logged-in user can copy it, or
|
||||
an agent can fetch it with its API token.
|
||||
|
||||
|
|
@ -390,7 +390,7 @@ def serialize_api_key(api_key):
|
|||
|
||||
class ApiKeyListCreateView(APIView):
|
||||
"""Programmatic API-key management, for automated test installations that
|
||||
provision their own agents. Gated by settings.DAYBOOK_KEY_PROVISIONING_API,
|
||||
provision their own agents. Gated by settings.AGENTFEED_KEY_PROVISIONING_API,
|
||||
which is OFF in deployment (it hands out key material over the API). When
|
||||
off, every method returns 403. Authenticated by session or an existing
|
||||
bearer token, and only ever operates on that user's own keys.
|
||||
|
|
@ -405,7 +405,7 @@ class ApiKeyListCreateView(APIView):
|
|||
return resolve_token_user(request)
|
||||
|
||||
def get(self, request):
|
||||
if not settings.DAYBOOK_KEY_PROVISIONING_API:
|
||||
if not settings.AGENTFEED_KEY_PROVISIONING_API:
|
||||
return Response({"detail": "Key provisioning API is disabled on this instance."}, status=403)
|
||||
user = self._resolve_user(request)
|
||||
if user is None:
|
||||
|
|
@ -413,7 +413,7 @@ class ApiKeyListCreateView(APIView):
|
|||
return Response([serialize_api_key(k) for k in user.api_keys.all()])
|
||||
|
||||
def post(self, request):
|
||||
if not settings.DAYBOOK_KEY_PROVISIONING_API:
|
||||
if not settings.AGENTFEED_KEY_PROVISIONING_API:
|
||||
return Response({"detail": "Key provisioning API is disabled on this instance."}, status=403)
|
||||
user = self._resolve_user(request)
|
||||
if user is None:
|
||||
|
|
@ -432,7 +432,7 @@ class ApiKeyDetailView(APIView):
|
|||
"""
|
||||
|
||||
def delete(self, request, key_id):
|
||||
if not settings.DAYBOOK_KEY_PROVISIONING_API:
|
||||
if not settings.AGENTFEED_KEY_PROVISIONING_API:
|
||||
return Response({"detail": "Key provisioning API is disabled on this instance."}, status=403)
|
||||
user = request.user if request.user.is_authenticated else resolve_token_user(request)
|
||||
if user is None:
|
||||
|
|
@ -858,7 +858,7 @@ def create_binary_artifact(data, request, user):
|
|||
|
||||
|
||||
# Allowlist for sanitizing the "themed" text kinds (markdown, deck,
|
||||
# html_fragment) — kinds where Daybook, not the poster, owns presentation, so
|
||||
# html_fragment) — kinds where AgentFeed, not the poster, owns presentation, so
|
||||
# they must never be a script-execution vector (issue #014). html_full is the
|
||||
# deliberate exception (verbatim, poster owns everything) and is NOT run
|
||||
# through this. The list keeps ordinary formatting + tables + code-highlight
|
||||
|
|
@ -963,14 +963,14 @@ def wrap_json_viewer(content, title, request):
|
|||
</style>
|
||||
</head>
|
||||
<body class="artifact">
|
||||
<script type="application/json" id="daybook-data">{safe_json}</script>
|
||||
<div id="daybook-json-view"></div>
|
||||
<script type="application/json" id="agentfeed-data">{safe_json}</script>
|
||||
<div id="agentfeed-json-view"></div>
|
||||
<script>
|
||||
(function () {{
|
||||
var raw = document.getElementById("daybook-data").textContent;
|
||||
var raw = document.getElementById("agentfeed-data").textContent;
|
||||
var data = JSON.parse(raw);
|
||||
renderjson.set_show_to_level(2);
|
||||
document.getElementById("daybook-json-view").appendChild(renderjson(data));
|
||||
document.getElementById("agentfeed-json-view").appendChild(renderjson(data));
|
||||
}})();
|
||||
</script>
|
||||
</body>
|
||||
|
|
@ -1007,18 +1007,18 @@ def wrap_csv_viewer(content, title, request):
|
|||
</style>
|
||||
</head>
|
||||
<body class="artifact">
|
||||
<script type="application/json" id="daybook-data">{table_json}</script>
|
||||
<div id="daybook-csv-view"></div>
|
||||
<script type="application/json" id="agentfeed-data">{table_json}</script>
|
||||
<div id="agentfeed-csv-view"></div>
|
||||
<script>
|
||||
(function () {{
|
||||
var table = JSON.parse(document.getElementById("daybook-data").textContent);
|
||||
var table = JSON.parse(document.getElementById("agentfeed-data").textContent);
|
||||
new gridjs.Grid({{
|
||||
columns: table.columns,
|
||||
data: table.data,
|
||||
search: true,
|
||||
sort: true,
|
||||
pagination: {{ limit: 20 }},
|
||||
}}).render(document.getElementById("daybook-csv-view"));
|
||||
}}).render(document.getElementById("agentfeed-csv-view"));
|
||||
}})();
|
||||
</script>
|
||||
</body>
|
||||
|
|
@ -1306,9 +1306,9 @@ def raw_artifact_path(artifact):
|
|||
return None
|
||||
|
||||
|
||||
class DaybookLoginView(auth_views.LoginView):
|
||||
class AgentFeedLoginView(auth_views.LoginView):
|
||||
template_name = "artifacts/login.html"
|
||||
authentication_form = DaybookLoginForm
|
||||
authentication_form = AgentFeedLoginForm
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
|
|
@ -1459,7 +1459,7 @@ def raw_unavailable_html(artifact):
|
|||
<body>
|
||||
<main>
|
||||
<h1>Raw source unavailable</h1>
|
||||
<p>Daybook does not have preserved raw source for <strong>{html.escape(artifact.title)}</strong>.</p>
|
||||
<p>AgentFeed does not have preserved raw source for <strong>{html.escape(artifact.title)}</strong>.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Single source of truth for the app version. Bump this, commit, then tag the
|
||||
# commit `vX.Y.Z` and deploy. Surfaced on Settings -> About and in
|
||||
# GET /api/capabilities ("version").
|
||||
__version__ = "0.2.1"
|
||||
__version__ = "0.3.0"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from pathlib import Path
|
|||
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
DATA_DIR = Path(os.environ.get("DAYBOOK_DATA_DIR", BASE_DIR)).resolve()
|
||||
DATA_DIR = Path(os.environ.get("AGENTFEED_DATA_DIR", BASE_DIR)).resolve()
|
||||
|
||||
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-only-change-me")
|
||||
DEBUG = os.environ.get("DJANGO_DEBUG", "1").lower() in {"1", "true", "yes", "on"}
|
||||
|
|
@ -11,14 +11,13 @@ ALLOWED_HOSTS = [host.strip() for host in os.environ.get("DJANGO_ALLOWED_HOSTS",
|
|||
|
||||
# Allow creating/listing/revoking API keys over the ingest API (POST /api/keys).
|
||||
# Meant for automated test installations that provision their own agents. It
|
||||
# exposes key material over the API, so it is OFF in deployment. Unset, it
|
||||
# follows DEBUG (on locally, off in production); set DAYBOOK_KEY_PROVISIONING_API
|
||||
# explicitly (0/1) to override either way.
|
||||
_key_api_env = os.environ.get("DAYBOOK_KEY_PROVISIONING_API")
|
||||
if _key_api_env is None:
|
||||
DAYBOOK_KEY_PROVISIONING_API = DEBUG
|
||||
else:
|
||||
DAYBOOK_KEY_PROVISIONING_API = _key_api_env.lower() in {"1", "true", "yes", "on"}
|
||||
# exposes key material over the API, so it is DISABLED BY DEFAULT and must be
|
||||
# turned on explicitly — set AGENTFEED_KEY_PROVISIONING_API=1 only on a throwaway
|
||||
# test install. Never enable it in a real deployment. (Independent of DEBUG, so
|
||||
# a misconfigured DEBUG=1 production box still won't expose it.)
|
||||
AGENTFEED_KEY_PROVISIONING_API = os.environ.get(
|
||||
"AGENTFEED_KEY_PROVISIONING_API", ""
|
||||
).lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
# Needed when running behind a TLS-terminating reverse proxy (Apache/Nginx/etc.):
|
||||
# Django otherwise thinks every request is plain HTTP, which breaks CSRF checks
|
||||
|
|
@ -71,7 +70,7 @@ WSGI_APPLICATION = "daybook_project.wsgi.application"
|
|||
REST_FRAMEWORK = {
|
||||
# Maps deeply-nested-JSON RecursionErrors (raised in DRF's parser, before
|
||||
# view validation) to a 400 instead of an unhandled 500. See issue #015.
|
||||
"EXCEPTION_HANDLER": "artifacts.exception_handlers.daybook_exception_handler",
|
||||
"EXCEPTION_HANDLER": "artifacts.exception_handlers.agentfeed_exception_handler",
|
||||
}
|
||||
|
||||
DATABASES = {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ from django.contrib import admin
|
|||
from django.contrib.auth import views as auth_views
|
||||
from django.urls import include, path
|
||||
|
||||
from artifacts.views import DaybookLoginView
|
||||
from artifacts.views import AgentFeedLoginView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("accounts/login/", DaybookLoginView.as_view(), name="login"),
|
||||
path("accounts/login/", AgentFeedLoginView.as_view(), name="login"),
|
||||
path("accounts/logout/", auth_views.LogoutView.as_view(next_page="login"), name="logout"),
|
||||
path("", include("artifacts.urls")),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Cloud Deployment Runbook For A Dedicated Deploy Agent
|
||||
|
||||
This is written for a capable (Opus-class) agent deploying and operating
|
||||
Daybook on a cloud VM. Treat it as a guide and a source of Daybook-specific
|
||||
AgentFeed on a cloud VM. Treat it as a guide and a source of AgentFeed-specific
|
||||
facts, **not a rigid script** — you're expected to use judgment, adapt to
|
||||
the actual VM, and read the authoritative sources rather than follow steps
|
||||
blindly. The steps below are a sensible default path; deviate where the host
|
||||
|
|
@ -25,7 +25,7 @@ disagree):
|
|||
Where a decision needs info you don't have (VM IP, domain, whether it's
|
||||
internet-facing, whether a reverse proxy/secondary volume exists), ask the
|
||||
operator once, then proceed autonomously for routine redeploys. The rest of
|
||||
this file is the recommended path plus the non-obvious, Daybook-specific
|
||||
this file is the recommended path plus the non-obvious, AgentFeed-specific
|
||||
gotchas (SSH port, CSRF-behind-proxy, bind address, data volume, healthcheck)
|
||||
that are easy to get wrong.
|
||||
|
||||
|
|
@ -37,9 +37,9 @@ a token tied to a full user account.
|
|||
|
||||
1. Generate an SSH key pair on the VM (or ask the operator for one to install):
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "daybook-deploy-agent" -f ~/.ssh/daybook_deploy -N ""
|
||||
ssh-keygen -t ed25519 -C "agentfeed-deploy-agent" -f ~/.ssh/agentfeed_deploy -N ""
|
||||
```
|
||||
2. Ask the operator to add the **public** key (`~/.ssh/daybook_deploy.pub`) as
|
||||
2. Ask the operator to add the **public** key (`~/.ssh/agentfeed_deploy.pub`) as
|
||||
a Deploy Key on the repo in Forgejo (repo → Settings → Deploy Keys →
|
||||
read-only is sufficient), or hand it to them directly — never ask them to
|
||||
give you their personal account credentials.
|
||||
|
|
@ -52,7 +52,7 @@ a token tied to a full user account.
|
|||
cat >> ~/.ssh/config <<'EOF'
|
||||
Host git.cygnus.systems
|
||||
Port 2222
|
||||
IdentityFile ~/.ssh/daybook_deploy
|
||||
IdentityFile ~/.ssh/agentfeed_deploy
|
||||
IdentitiesOnly yes
|
||||
EOF
|
||||
```
|
||||
|
|
@ -112,12 +112,12 @@ Edit `deploy/.env`:
|
|||
DJANGO_DEBUG=0
|
||||
DJANGO_SECRET_KEY=<generated-secret>
|
||||
DJANGO_ALLOWED_HOSTS=<vm-public-ip>,<domain-if-any>,localhost,127.0.0.1
|
||||
DAYBOOK_DATA_DIR=/data
|
||||
AGENTFEED_DATA_DIR=/data
|
||||
DATABASE_PATH=/data/db.sqlite3
|
||||
ARTIFACTS_ROOT=/data/artifacts_store
|
||||
```
|
||||
|
||||
There is no shared API token to set — Daybook issues a personal token per
|
||||
There is no shared API token to set — AgentFeed issues a personal token per
|
||||
user from Settings → Account once a user exists (step 5). Never commit
|
||||
`deploy/.env`; it is already gitignored.
|
||||
|
||||
|
|
@ -131,17 +131,17 @@ if unsure which applies here.
|
|||
## 4. Network Access
|
||||
|
||||
**If this VM is reachable from the public internet, a reverse proxy with TLS
|
||||
in front of Daybook is not optional — it's the only path.** Bearer tokens are
|
||||
in front of AgentFeed is not optional — it's the only path.** Bearer tokens are
|
||||
this app's entire write-auth model, and they'd otherwise go over plaintext
|
||||
HTTP straight to a port on the public IP. Direct port exposure (below) is
|
||||
only for private/VPN-only networks with no public exposure.
|
||||
|
||||
- **Internet-facing (the common case):** set `DAYBOOK_BIND_ADDR=127.0.0.1` in
|
||||
- **Internet-facing (the common case):** set `AGENTFEED_BIND_ADDR=127.0.0.1` in
|
||||
`deploy/.env` so the container port is not reachable from outside the VM at
|
||||
all, then open `80`/`443` in the VM's firewall/security group for the
|
||||
reverse proxy (see §8). Do not open `8000` externally.
|
||||
- **Private/VPN-only network:** open TCP `8000` to the addresses that should
|
||||
reach it (never `0.0.0.0/0`), and leave `DAYBOOK_BIND_ADDR` unset.
|
||||
reach it (never `0.0.0.0/0`), and leave `AGENTFEED_BIND_ADDR` unset.
|
||||
|
||||
Before opening any port, check whether one is already open and in use —
|
||||
`ss -tlnp | grep -E ':80|:443|:8000'` — rather than assuming a clean slate.
|
||||
|
|
@ -153,8 +153,8 @@ don't have API access to it yourself.
|
|||
## 5. Build, Start, Create A User
|
||||
|
||||
If the target has a separate, larger data volume than its main disk (common
|
||||
on space-constrained VMs), point Daybook's persistent data at it before
|
||||
starting: set `DAYBOOK_HOST_DATA_DIR=/path/on/that/volume` in `deploy/.env`.
|
||||
on space-constrained VMs), point AgentFeed's persistent data at it before
|
||||
starting: set `AGENTFEED_HOST_DATA_DIR=/path/on/that/volume` in `deploy/.env`.
|
||||
Ask the operator whether such a volume exists and where before defaulting to
|
||||
the main disk — filling the main disk on a host running other services is a
|
||||
bigger problem than it would be on a dedicated box. Create that same path
|
||||
|
|
@ -162,9 +162,9 @@ bigger problem than it would be on a dedicated box. Create that same path
|
|||
unset) before starting the stack:
|
||||
|
||||
```bash
|
||||
mkdir -p deploy/data # or the DAYBOOK_HOST_DATA_DIR path if you set one above
|
||||
mkdir -p deploy/data # or the AGENTFEED_HOST_DATA_DIR path if you set one above
|
||||
docker compose -f deploy/docker-compose.yml up -d --build
|
||||
docker compose -f deploy/docker-compose.yml exec daybook python manage.py createsuperuser
|
||||
docker compose -f deploy/docker-compose.yml exec agentfeed python manage.py createsuperuser
|
||||
```
|
||||
|
||||
The entrypoint runs migrations and collects static files automatically on
|
||||
|
|
@ -183,7 +183,7 @@ response when unauthenticated — it confirms the app is up. (Don't use
|
|||
container's own healthcheck hits the login page instead, which is 200
|
||||
without auth.) This `curl` runs
|
||||
against `127.0.0.1` from on the VM itself, so it works regardless of whether
|
||||
`DAYBOOK_BIND_ADDR` is set. Log in from a browser at whichever address is
|
||||
`AGENTFEED_BIND_ADDR` is set. Log in from a browser at whichever address is
|
||||
actually externally reachable at this point in the runbook — `http://<vm-ip>:8000/`
|
||||
if you're on the private/VPN-only path from §4, or `https://<domain>/` through
|
||||
the reverse proxy once §8 is done if you're on the internet-facing path — with
|
||||
|
|
@ -230,13 +230,13 @@ For a routine forward release that's fine; if a rollback is needed after a
|
|||
migration, restore the pre-deploy `deploy/data` backup (see §9) alongside
|
||||
the code checkout.
|
||||
|
||||
`deploy/.env` and the data directory (wherever `DAYBOOK_HOST_DATA_DIR` points,
|
||||
`deploy/.env` and the data directory (wherever `AGENTFEED_HOST_DATA_DIR` points,
|
||||
or `deploy/data` by default) are untracked, so `git pull` never touches them —
|
||||
no need to back them up before a routine redeploy.
|
||||
|
||||
**If the container crash-loops right after a build** (`docker compose ps`
|
||||
shows `restarting`, health never goes green), the most likely cause is the
|
||||
entrypoint's `collectstatic` step failing — check `docker logs daybook`
|
||||
entrypoint's `collectstatic` step failing — check `docker logs agentfeed`
|
||||
before assuming an env/network problem. A `whitenoise ... MissingFileError`
|
||||
there means a static asset references a file that isn't present (this is what
|
||||
bit v0.1.0). No data or schema impact; fix the asset in the repo, tag, and
|
||||
|
|
@ -277,14 +277,14 @@ instead. Apache example:
|
|||
|
||||
```apache
|
||||
<VirtualHost *:443>
|
||||
ServerName daybook.example.com
|
||||
ServerName agentfeed.example.com
|
||||
ProxyPreserveHost On
|
||||
RequestHeader set X-Forwarded-Proto "https"
|
||||
ProxyPass / http://127.0.0.1:8000/
|
||||
ProxyPassReverse / http://127.0.0.1:8000/
|
||||
SSLEngine on
|
||||
SSLCertificateFile /etc/letsencrypt/live/daybook.example.com/fullchain.pem
|
||||
SSLCertificateKeyFile /etc/letsencrypt/live/daybook.example.com/privkey.pem
|
||||
SSLCertificateFile /etc/letsencrypt/live/agentfeed.example.com/fullchain.pem
|
||||
SSLCertificateKeyFile /etc/letsencrypt/live/agentfeed.example.com/privkey.pem
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
|
|
@ -296,7 +296,7 @@ Only reach for a standalone proxy like Caddy if the target genuinely has
|
|||
nothing on 80/443 yet — it still needs the same header:
|
||||
|
||||
```text
|
||||
daybook.example.com {
|
||||
agentfeed.example.com {
|
||||
reverse_proxy 127.0.0.1:8000 {
|
||||
header_up X-Forwarded-Proto https
|
||||
}
|
||||
|
|
@ -305,15 +305,15 @@ daybook.example.com {
|
|||
|
||||
## 9. Backup
|
||||
|
||||
If you set `DAYBOOK_HOST_DATA_DIR` in step 5, the real data lives there, not
|
||||
If you set `AGENTFEED_HOST_DATA_DIR` in step 5, the real data lives there, not
|
||||
at `deploy/data` — back up whichever path is actually configured, read
|
||||
straight from `deploy/.env` rather than assumed, so this keeps working
|
||||
whether or not that override is set:
|
||||
|
||||
```bash
|
||||
cd /opt/daybook
|
||||
DATA_DIR="$(grep -oP '(?<=^DAYBOOK_HOST_DATA_DIR=).*' deploy/.env || echo deploy/data)"
|
||||
tar -czf "/tmp/daybook-data-$(date +%Y%m%d-%H%M%S).tar.gz" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")"
|
||||
DATA_DIR="$(grep -oP '(?<=^AGENTFEED_HOST_DATA_DIR=).*' deploy/.env || echo deploy/data)"
|
||||
tar -czf "/tmp/agentfeed-data-$(date +%Y%m%d-%H%M%S).tar.gz" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")"
|
||||
```
|
||||
|
||||
Copy the resulting archive off the VM (to object storage, the operator's
|
||||
|
|
@ -331,12 +331,12 @@ Before reporting the deployment complete, confirm all of these:
|
|||
public HTTPS origin, and the proxy forwards `X-Forwarded-Proto: https`.
|
||||
Verify by actually logging in through the proxy, not just curling
|
||||
`/api/capabilities` — CSRF only bites on POST (login, artifact writes).
|
||||
- [ ] `docker compose -f deploy/docker-compose.yml ps` shows `daybook` healthy.
|
||||
- [ ] `docker compose -f deploy/docker-compose.yml ps` shows `agentfeed` healthy.
|
||||
- [ ] `/api/capabilities` returns `403` unauthenticated, `200` with a token.
|
||||
- [ ] At least one superuser exists with a working personal API token.
|
||||
- [ ] A smoke-test artifact posts and renders successfully.
|
||||
- [ ] If internet-facing: TLS is in front of the app, not raw HTTP on `8000`.
|
||||
- [ ] A backup of the data directory (`deploy/data`, or `DAYBOOK_HOST_DATA_DIR`
|
||||
- [ ] A backup of the data directory (`deploy/data`, or `AGENTFEED_HOST_DATA_DIR`
|
||||
if you set it) has been taken and copied off the VM.
|
||||
- [ ] If the target host has its own service-inventory, monitoring, or
|
||||
changelog conventions for new services, they've been followed — ask
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# ${VAR} substitution below (DAYBOOK_BIND_ADDR, DAYBOOK_HOST_DATA_DIR) reads
|
||||
# ${VAR} substitution below (AGENTFEED_BIND_ADDR, AGENTFEED_HOST_DATA_DIR) reads
|
||||
# from deploy/.env because Compose resolves .env relative to *this file's*
|
||||
# directory, not the caller's cwd. That relies on being invoked as
|
||||
# `docker compose -f deploy/docker-compose.yml` from the repo root, per
|
||||
|
|
@ -7,20 +7,20 @@
|
|||
# the defaults (0.0.0.0 bind, ./data) would be a security regression, not
|
||||
# just a broken build.
|
||||
services:
|
||||
daybook:
|
||||
agentfeed:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile
|
||||
container_name: daybook
|
||||
container_name: agentfeed
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
mem_limit: 384m
|
||||
mem_reservation: 128m
|
||||
ports:
|
||||
- "${DAYBOOK_BIND_ADDR:-0.0.0.0}:8000:8000"
|
||||
- "${AGENTFEED_BIND_ADDR:-0.0.0.0}:8000:8000"
|
||||
volumes:
|
||||
- ${DAYBOOK_HOST_DATA_DIR:-./data}:/data
|
||||
- ${AGENTFEED_HOST_DATA_DIR:-./data}:/data
|
||||
healthcheck:
|
||||
# Hit the login page, not /api/capabilities: capabilities returns 403
|
||||
# when unauthenticated, and `curl --fail` treats 403 as an error, which
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
mkdir -p "${DAYBOOK_DATA_DIR:-/data}" "${ARTIFACTS_ROOT:-/data/artifacts_store}"
|
||||
mkdir -p "${AGENTFEED_DATA_DIR:-/data}" "${ARTIFACTS_ROOT:-/data/artifacts_store}"
|
||||
|
||||
python manage.py migrate --noinput
|
||||
python manage.py collectstatic --noinput
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
DJANGO_DEBUG=0
|
||||
DJANGO_SECRET_KEY=replace-with-a-long-random-secret
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,your-vm-ip,your-domain.example
|
||||
# Required if Daybook sits behind a TLS-terminating reverse proxy (Apache,
|
||||
# Required if AgentFeed sits behind a TLS-terminating reverse proxy (Apache,
|
||||
# Nginx, Traefik, a cloud load balancer). Without it, login/POST fails with
|
||||
# "CSRF verification failed" because Django can't tell the original request
|
||||
# was HTTPS. Set to the public HTTPS origin(s) users actually visit:
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=https://your-domain.example
|
||||
# Leave OFF in deployment. When on, POST /api/keys can create/list/revoke API
|
||||
# keys over the network (it returns key material), which is only appropriate
|
||||
# for automated test installations. Unset, it follows DJANGO_DEBUG — so with
|
||||
# DJANGO_DEBUG=0 above it is already off. Set to 1 only on a throwaway test box.
|
||||
# DAYBOOK_KEY_PROVISIONING_API=0
|
||||
DAYBOOK_DATA_DIR=/data
|
||||
# Disabled by default. When on, POST /api/keys can create/list/revoke API keys
|
||||
# over the network (it returns key material), which is only appropriate for
|
||||
# automated test installations. Leave unset in deployment; set to 1 only on a
|
||||
# throwaway test box.
|
||||
# AGENTFEED_KEY_PROVISIONING_API=0
|
||||
AGENTFEED_DATA_DIR=/data
|
||||
DATABASE_PATH=/data/db.sqlite3
|
||||
ARTIFACTS_ROOT=/data/artifacts_store
|
||||
|
||||
# Optional, read by deploy/docker-compose.yml directly (not by the app):
|
||||
# DAYBOOK_BIND_ADDR=127.0.0.1
|
||||
# AGENTFEED_BIND_ADDR=127.0.0.1
|
||||
# Uncomment and set to 127.0.0.1 when a reverse proxy on the same host will
|
||||
# front this service — this stops the container port from being reachable
|
||||
# directly from the network. Leave unset (binds all interfaces) only for
|
||||
# private/VPN-only deployments with no proxy in front.
|
||||
# DAYBOOK_HOST_DATA_DIR=/mnt/some-larger-volume/daybook-data
|
||||
# AGENTFEED_HOST_DATA_DIR=/mnt/some-larger-volume/agentfeed-data
|
||||
# Uncomment and point at a path on a separate/larger volume if the host's
|
||||
# main disk is space-constrained. Defaults to ./data next to this file.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Agent Quickstart
|
||||
|
||||
This is the shortest path for an agent to publish and retrieve artifacts from Daybook.
|
||||
This is the shortest path for an agent to publish and retrieve artifacts from AgentFeed.
|
||||
|
||||
## 0. Get Your API Key
|
||||
|
||||
|
|
|
|||
28
docs/api.md
28
docs/api.md
|
|
@ -1,6 +1,6 @@
|
|||
# Daybook API
|
||||
# AgentFeed API
|
||||
|
||||
Daybook is built around agents publishing artifacts. Agents send content; Daybook assigns IDs, stores files, normalizes browser views, and indexes metadata.
|
||||
AgentFeed is built around agents publishing artifacts. Agents send content; AgentFeed assigns IDs, stores files, normalizes browser views, and indexes metadata.
|
||||
|
||||
Base URL while running locally:
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ Currently supported kinds:
|
|||
|
||||
| Kind | Upload type | Rendered behavior | Raw support |
|
||||
|---|---|---|---|
|
||||
| `html_fragment` | JSON text | Wrapped with a Daybook theme; tables styled automatically; **HTML sanitized** | No |
|
||||
| `html_fragment` | JSON text | Wrapped with a AgentFeed theme; tables styled automatically; **HTML sanitized** | No |
|
||||
| `html_full` | JSON text | Stored as complete HTML, verbatim — no theme, styling, or sanitization | Yes |
|
||||
| `markdown` | JSON text | Rendered to HTML (code blocks syntax-highlighted, tables styled), **sanitized**, original `.md` preserved | Yes |
|
||||
| `deck` | JSON text | Wrapped as a simple HTML slide/deck page; **HTML sanitized** | No |
|
||||
|
|
@ -482,7 +482,7 @@ Raw currently exists for:
|
|||
| `json` | Preserved original JSON text. |
|
||||
| `csv` | Preserved original CSV text. |
|
||||
|
||||
If raw content is unavailable, Daybook returns a small HTML fallback page.
|
||||
If raw content is unavailable, AgentFeed returns a small HTML fallback page.
|
||||
|
||||
## Sharing
|
||||
|
||||
|
|
@ -503,7 +503,7 @@ GET /s/{share_token}
|
|||
with the link can view it.
|
||||
- Images embedded in a shared artifact's content are also served
|
||||
unauthenticated while that artifact is shared, so inline images don't
|
||||
break for a visitor without a Daybook account.
|
||||
break for a visitor without a AgentFeed account.
|
||||
- `is_shared` and `share_url` (both in every artifact response — see
|
||||
`GET /api/artifacts/{id}`) report current sharing status; `share_url` is
|
||||
`null` when not shared.
|
||||
|
|
@ -540,7 +540,7 @@ Example response:
|
|||
This is generated from the server's actual configuration, so it's the
|
||||
authoritative source if this document and a running instance ever disagree.
|
||||
|
||||
Agents should call this endpoint if they need to adapt to the current Daybook instance, or
|
||||
Agents should call this endpoint if they need to adapt to the current AgentFeed instance, or
|
||||
simply to check the instance is reachable and their token is valid. Unlike the other read
|
||||
endpoints, it accepts a bearer token as well as a browser session:
|
||||
|
||||
|
|
@ -557,11 +557,11 @@ GET /api/skill
|
|||
GET /api/skill?format=json
|
||||
```
|
||||
|
||||
Returns the bundled agent-posting skill (`skills/daybook-posting/SKILL.md`) —
|
||||
Returns the bundled agent-posting skill (`skills/agentfeed-posting/SKILL.md`) —
|
||||
the same instructions you'd give an AI so it knows how to publish to this
|
||||
instance. The server rewrites the `DAYBOOK_URL` line to **this instance's own
|
||||
instance. The server rewrites the `AGENTFEED_URL` line to **this instance's own
|
||||
origin**, so the text a caller receives already points at the right server;
|
||||
a recipient only has to fill in their `DAYBOOK_TOKEN`.
|
||||
a recipient only has to fill in their `AGENTFEED_TOKEN`.
|
||||
|
||||
Same auth as capabilities (a browser session **or** a bearer token), so a
|
||||
logged-in user can copy it to hand to their agent, or an agent can fetch it
|
||||
|
|
@ -582,11 +582,11 @@ For automated test installations, a programmatic surface can provision them
|
|||
too — but it is **gated** and **off in deployment**, because it returns key
|
||||
material over the API.
|
||||
|
||||
The gate is `DAYBOOK_KEY_PROVISIONING_API`. Unset, it follows `DJANGO_DEBUG`
|
||||
(on locally, off in production); set it explicitly (`0`/`1`) to override.
|
||||
`GET /api/capabilities` reports the current state as `key_provisioning_api`,
|
||||
and only lists the `/api/keys` methods when it's enabled. When disabled, every
|
||||
method below returns `403`.
|
||||
The gate is `AGENTFEED_KEY_PROVISIONING_API`. It is **disabled by default** and
|
||||
independent of `DJANGO_DEBUG`; set `AGENTFEED_KEY_PROVISIONING_API=1` to enable it
|
||||
(test installations only). `GET /api/capabilities` reports the current state as
|
||||
`key_provisioning_api`, and only lists the `/api/keys` methods when it's
|
||||
enabled. When disabled, every method below returns `403`.
|
||||
|
||||
```http
|
||||
GET /api/keys # list this user's keys (incl. key values)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Deployment
|
||||
|
||||
Daybook deploys the same way on any Linux host with Docker: a cloud VM, a home
|
||||
AgentFeed deploys the same way on any Linux host with Docker: a cloud VM, a home
|
||||
server, a Proxmox guest, whatever. It runs as a single Docker container
|
||||
(Gunicorn) with all persistent state — the SQLite database and artifact files —
|
||||
on a bind-mounted `./data` directory outside the image.
|
||||
|
|
@ -8,7 +8,7 @@ on a bind-mounted `./data` directory outside the image.
|
|||
```text
|
||||
Target host (cloud VM, home server, ...)
|
||||
Docker Engine
|
||||
daybook container
|
||||
agentfeed container
|
||||
/app application code baked into the image
|
||||
/data persistent volume: db.sqlite3, artifacts_store/
|
||||
```
|
||||
|
|
@ -17,8 +17,8 @@ Target host (cloud VM, home server, ...)
|
|||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `Dockerfile` | Builds the Daybook image. |
|
||||
| `docker-compose.yml` | Runs Daybook with a persistent `./data:/data` bind mount. |
|
||||
| `Dockerfile` | Builds the AgentFeed image. |
|
||||
| `docker-compose.yml` | Runs AgentFeed with a persistent `./data:/data` bind mount. |
|
||||
| `entrypoint.sh` | Runs migrations, collects static files, then starts Gunicorn. |
|
||||
| `env.example` | Template for deployment environment variables. |
|
||||
|
||||
|
|
@ -59,12 +59,12 @@ Edit `deploy/.env` and set at minimum:
|
|||
DJANGO_DEBUG=0
|
||||
DJANGO_SECRET_KEY=<generated-secret>
|
||||
DJANGO_ALLOWED_HOSTS=<vm-ip-or-dns-name>,localhost,127.0.0.1
|
||||
DAYBOOK_DATA_DIR=/data
|
||||
AGENTFEED_DATA_DIR=/data
|
||||
DATABASE_PATH=/data/db.sqlite3
|
||||
ARTIFACTS_ROOT=/data/artifacts_store
|
||||
```
|
||||
|
||||
There is no shared API token to configure — Daybook uses per-user API keys
|
||||
There is no shared API token to configure — AgentFeed uses per-user API keys
|
||||
created under Settings → API Keys once a user exists (each user can hold
|
||||
several, one per agent, each individually revocable). Do not commit
|
||||
`deploy/.env`.
|
||||
|
|
@ -80,7 +80,7 @@ Create your first login (superuser) so you have something to log in with and
|
|||
a personal API token to post through:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml exec daybook python manage.py createsuperuser
|
||||
docker compose -f deploy/docker-compose.yml exec agentfeed python manage.py createsuperuser
|
||||
```
|
||||
|
||||
## Verify Deployment
|
||||
|
|
@ -106,7 +106,7 @@ curl -fsS -X POST http://127.0.0.1:8000/api/artifacts \
|
|||
"source": "deploy-smoke",
|
||||
"kind": "markdown",
|
||||
"title": "Deployment Smoke Test",
|
||||
"content": "# Deployment Smoke Test\n\nDaybook accepted an artifact."
|
||||
"content": "# Deployment Smoke Test\n\nAgentFeed accepted an artifact."
|
||||
}'
|
||||
```
|
||||
|
||||
|
|
@ -127,17 +127,17 @@ picked up automatically. `deploy/.env` and `deploy/data/` are untouched by
|
|||
## Operational Commands
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml logs -f daybook
|
||||
docker compose -f deploy/docker-compose.yml restart daybook
|
||||
docker compose -f deploy/docker-compose.yml logs -f agentfeed
|
||||
docker compose -f deploy/docker-compose.yml restart agentfeed
|
||||
docker compose -f deploy/docker-compose.yml down
|
||||
docker compose -f deploy/docker-compose.yml up -d
|
||||
docker compose -f deploy/docker-compose.yml exec daybook python manage.py check
|
||||
docker compose -f deploy/docker-compose.yml exec daybook sh
|
||||
docker compose -f deploy/docker-compose.yml exec agentfeed python manage.py check
|
||||
docker compose -f deploy/docker-compose.yml exec agentfeed sh
|
||||
```
|
||||
|
||||
## Django Admin (`/admin/`)
|
||||
|
||||
Daybook ships Django's built-in admin at `/admin/` as an operator
|
||||
AgentFeed ships Django's built-in admin at `/admin/` as an operator
|
||||
break-glass tool — for raw-data fixes: reset a password, inspect or delete
|
||||
any user's artifacts, adjust a quota, audit or revoke API keys. It is
|
||||
separate from the in-app **Settings** area, which is the everyday product
|
||||
|
|
@ -147,17 +147,17 @@ surface (see the in-app guide / `docs/` for normal use).
|
|||
`is_staff = True`. The account you create with `createsuperuser` has it (plus
|
||||
full superuser rights). Note the two different admin concepts:
|
||||
|
||||
- `is_superuser` — Daybook's *in-app* admin (can reach Settings → Users).
|
||||
- `is_superuser` — AgentFeed's *in-app* admin (can reach Settings → Users).
|
||||
- `is_staff` — can log into *Django's* `/admin/` site.
|
||||
|
||||
The `createsuperuser` account has **both**. A user promoted in-app via
|
||||
Settings → Users ("Grant admin access") gets `is_superuser` only — so they
|
||||
can administer Daybook through its own UI but **cannot** reach `/admin/`.
|
||||
can administer AgentFeed through its own UI but **cannot** reach `/admin/`.
|
||||
Regular users have neither. To grant someone `/admin/` access, set staff
|
||||
explicitly:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml exec daybook \
|
||||
docker compose -f deploy/docker-compose.yml exec agentfeed \
|
||||
python manage.py shell -c "from django.contrib.auth.models import User; u=User.objects.get(username='NAME'); u.is_staff=True; u.save()"
|
||||
```
|
||||
|
||||
|
|
@ -172,23 +172,23 @@ Back up `deploy/data/` — it contains the SQLite database and all artifact
|
|||
files:
|
||||
|
||||
```bash
|
||||
tar -czf daybook-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /opt/daybook/deploy data
|
||||
tar -czf agentfeed-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /opt/daybook/deploy data
|
||||
```
|
||||
|
||||
For a consistent snapshot during heavy writes, stop the container first:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml down
|
||||
tar -czf daybook-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /opt/daybook/deploy data
|
||||
tar -czf agentfeed-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /opt/daybook/deploy data
|
||||
docker compose -f deploy/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
To restore: stop Daybook, replace `deploy/data/` with the backup contents,
|
||||
To restore: stop AgentFeed, replace `deploy/data/` with the backup contents,
|
||||
then start it again.
|
||||
|
||||
## Reverse Proxy Notes
|
||||
|
||||
If putting Daybook behind Nginx, Caddy, Traefik, or a cloud load balancer:
|
||||
If putting AgentFeed behind Nginx, Caddy, Traefik, or a cloud load balancer:
|
||||
|
||||
- Proxy to `http://<vm-ip>:8000`.
|
||||
- Add the public hostname to `DJANGO_ALLOWED_HOSTS`.
|
||||
|
|
@ -200,8 +200,8 @@ If putting Daybook behind Nginx, Caddy, Traefik, or a cloud load balancer:
|
|||
terminates TLS and forwards plain HTTP to the container.
|
||||
|
||||
```text
|
||||
DJANGO_ALLOWED_HOSTS=daybook.example.com,localhost,127.0.0.1
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=https://daybook.example.com
|
||||
DJANGO_ALLOWED_HOSTS=agentfeed.example.com,localhost,127.0.0.1
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=https://agentfeed.example.com
|
||||
```
|
||||
|
||||
Forwarding the proto header, by proxy:
|
||||
|
|
@ -220,7 +220,7 @@ Caddy: header_up X-Forwarded-Proto https
|
|||
- If behind a reverse proxy: `DJANGO_CSRF_TRUSTED_ORIGINS` is set and the
|
||||
proxy forwards `X-Forwarded-Proto: https` (login will 403 otherwise).
|
||||
- `deploy/data/` exists and persists across `git pull` + rebuild.
|
||||
- `docker compose -f deploy/docker-compose.yml ps` shows `daybook` healthy.
|
||||
- `docker compose -f deploy/docker-compose.yml ps` shows `agentfeed` healthy.
|
||||
- `GET /api/capabilities` returns 403 without auth, 200 with a valid token or
|
||||
session (check the status code — don't use `curl -f`, which errors on 403).
|
||||
- At least one superuser exists (`manage.py createsuperuser`) with a key
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# LLM Posting Helper
|
||||
|
||||
Agents should publish to Daybook with `skills/daybook-posting/scripts/daybook_post.py` instead of hand-building HTTP, JSON, curl, or PowerShell multipart requests.
|
||||
Agents should publish to AgentFeed with `skills/agentfeed-posting/scripts/agentfeed_post.py` instead of hand-building HTTP, JSON, curl, or PowerShell multipart requests.
|
||||
|
||||
The project also keeps `scripts/daybook_post.py` as a compatibility wrapper.
|
||||
The project also keeps `scripts/agentfeed_post.py` as a compatibility wrapper.
|
||||
|
||||
The helper is intentionally zero-dependency and uses only Python's standard library. It works on Windows and Linux.
|
||||
|
||||
|
|
@ -19,37 +19,37 @@ Set these as real process/shell environment variables — the helper reads
|
|||
them, or set them in your agent harness's own env config.
|
||||
|
||||
```text
|
||||
DAYBOOK_URL=http://127.0.0.1:8001
|
||||
DAYBOOK_TOKEN=your-personal-api-token
|
||||
DAYBOOK_SOURCE=agent
|
||||
AGENTFEED_URL=http://127.0.0.1:8001
|
||||
AGENTFEED_TOKEN=your-personal-api-token
|
||||
AGENTFEED_SOURCE=agent
|
||||
```
|
||||
|
||||
`DAYBOOK_TOKEN` is a personal API key, created under Settings → API Keys in the app (one per agent, individually revocable) — not a key shared across users. Override these per environment.
|
||||
`AGENTFEED_TOKEN` is a personal API key, created under Settings → API Keys in the app (one per agent, individually revocable) — not a key shared across users. Override these per environment.
|
||||
|
||||
## Commands
|
||||
|
||||
Check the instance:
|
||||
|
||||
```bash
|
||||
python skills/daybook-posting/scripts/daybook_post.py --check
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --check
|
||||
```
|
||||
|
||||
Post Markdown from a file:
|
||||
|
||||
```bash
|
||||
python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Build Report" --content-file report.md --tags build,report
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --kind markdown --title "Build Report" --content-file report.md --tags build,report
|
||||
```
|
||||
|
||||
Post Markdown from stdin:
|
||||
|
||||
```bash
|
||||
cat report.md | python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Build Report"
|
||||
cat report.md | python skills/agentfeed-posting/scripts/agentfeed_post.py --kind markdown --title "Build Report"
|
||||
```
|
||||
|
||||
Post a deck:
|
||||
|
||||
```bash
|
||||
python skills/daybook-posting/scripts/daybook_post.py --kind deck --title "Deployment Plan" --content-file deck.html
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --kind deck --title "Deployment Plan" --content-file deck.html
|
||||
```
|
||||
|
||||
`deck.html` must contain top-level `<section>...</section>` blocks.
|
||||
|
|
@ -57,44 +57,44 @@ python skills/daybook-posting/scripts/daybook_post.py --kind deck --title "Deplo
|
|||
Post an image/PDF/binary file:
|
||||
|
||||
```bash
|
||||
python skills/daybook-posting/scripts/daybook_post.py --file diagram.svg --title "Architecture Diagram"
|
||||
python skills/daybook-posting/scripts/daybook_post.py --file report.pdf --title "Generated Report"
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --file diagram.svg --title "Architecture Diagram"
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --file report.pdf --title "Generated Report"
|
||||
```
|
||||
|
||||
Post CSV or JSON as an interactive table/tree (auto-mode detects the kind
|
||||
from the extension; explicit `--kind` works too):
|
||||
|
||||
```bash
|
||||
python skills/daybook-posting/scripts/daybook_post.py results.csv
|
||||
python skills/daybook-posting/scripts/daybook_post.py --kind json --title "Run Summary" --content-file summary.json
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py results.csv
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --kind json --title "Run Summary" --content-file summary.json
|
||||
```
|
||||
|
||||
Post an `.xlsx` spreadsheet (converted server-side to a `csv` artifact —
|
||||
active sheet only, saved formula results only, no formatting preserved):
|
||||
|
||||
```bash
|
||||
python skills/daybook-posting/scripts/daybook_post.py balance_sheet.xlsx
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py balance_sheet.xlsx
|
||||
```
|
||||
|
||||
Render other code as a Markdown fenced block instead (no dedicated `code`
|
||||
kind yet):
|
||||
|
||||
```bash
|
||||
python skills/daybook-posting/scripts/daybook_post.py --file script.py --as-markdown --title "Generated Script"
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --file script.py --as-markdown --title "Generated Script"
|
||||
```
|
||||
|
||||
Embed an image inside a markdown/HTML artifact (upload first, then
|
||||
reference the printed URL in your content):
|
||||
|
||||
```bash
|
||||
python skills/daybook-posting/scripts/daybook_post.py --upload-image chart.png
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --upload-image chart.png
|
||||
# -> prints .../assets/<id>; put that in your markdown, then post normally
|
||||
```
|
||||
|
||||
Dry run:
|
||||
|
||||
```bash
|
||||
python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Draft" --content-file report.md --dry-run
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --kind markdown --title "Draft" --content-file report.md --dry-run
|
||||
```
|
||||
|
||||
The artifact URL is printed as the final stdout line.
|
||||
|
|
@ -118,7 +118,7 @@ The helper validates common LLM mistakes before sending:
|
|||
Use repeatable `--meta key=value` flags:
|
||||
|
||||
```bash
|
||||
python skills/daybook-posting/scripts/daybook_post.py \
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py \
|
||||
--kind markdown \
|
||||
--title "Run Summary" \
|
||||
--content-file summary.md \
|
||||
|
|
@ -129,7 +129,7 @@ python skills/daybook-posting/scripts/daybook_post.py \
|
|||
Or pass a JSON object:
|
||||
|
||||
```bash
|
||||
python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Run Summary" --content-file summary.md --meta-json '{"run_id":"run-001"}'
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --kind markdown --title "Run Summary" --content-file summary.md --meta-json '{"run_id":"run-001"}'
|
||||
```
|
||||
|
||||
`--meta` and `--meta-json` are merged if both are given. Metadata lives in
|
||||
|
|
@ -140,7 +140,7 @@ filtering).
|
|||
|
||||
| Flag | Default | Notes |
|
||||
|---|---|---|
|
||||
| `--source` | `DAYBOOK_SOURCE` env, else `agent` | What produced this — an agent-profile, script, or person name. |
|
||||
| `--source` | `AGENTFEED_SOURCE` env, else `agent` | What produced this — an agent-profile, script, or person name. |
|
||||
| `--source-type` | `agent` | One of `agent`, `person`, `system`. |
|
||||
| `--source-name` | The effective `--source` | Human-readable name, e.g. "Hermes Agent". |
|
||||
| `--source-id` | Empty | A stable external ID, if one exists. |
|
||||
|
|
@ -152,7 +152,7 @@ filtering).
|
|||
A thin LLM-facing wrapper lives at:
|
||||
|
||||
```text
|
||||
skills/daybook-posting/SKILL.md
|
||||
skills/agentfeed-posting/SKILL.md
|
||||
```
|
||||
|
||||
Its key instruction is: do not hand-build requests; use `scripts/daybook_post.py`.
|
||||
Its key instruction is: do not hand-build requests; use `scripts/agentfeed_post.py`.
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
# Daybook Posting Helper
|
||||
# AgentFeed Posting Helper
|
||||
|
||||
`scripts/daybook_post.py` is a compatibility wrapper for the canonical zero-dependency helper at `skills/daybook-posting/scripts/daybook_post.py`.
|
||||
`scripts/agentfeed_post.py` is a compatibility wrapper for the canonical zero-dependency helper at `skills/agentfeed-posting/scripts/agentfeed_post.py`.
|
||||
|
||||
The helper is for agents and scripts that need to publish artifacts to Daybook without hand-crafting JSON, curl, or multipart requests.
|
||||
The helper is for agents and scripts that need to publish artifacts to AgentFeed without hand-crafting JSON, curl, or multipart requests.
|
||||
|
||||
Configuration:
|
||||
|
||||
```text
|
||||
DAYBOOK_URL=http://127.0.0.1:8001
|
||||
DAYBOOK_TOKEN=your-personal-api-token
|
||||
DAYBOOK_SOURCE=agent
|
||||
AGENTFEED_URL=http://127.0.0.1:8001
|
||||
AGENTFEED_TOKEN=your-personal-api-token
|
||||
AGENTFEED_SOURCE=agent
|
||||
```
|
||||
|
||||
`DAYBOOK_TOKEN` is a personal per-user token from Settings → Account in the app, not shared across users.
|
||||
`AGENTFEED_TOKEN` is a personal per-user token from Settings → Account in the app, not shared across users.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
python skills/daybook-posting/scripts/daybook_post.py --check
|
||||
python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Build Report" --content-file report.md --tags build,report
|
||||
cat report.md | python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Build Report"
|
||||
python skills/daybook-posting/scripts/daybook_post.py --kind deck --title "Plan" --content-file deck.html
|
||||
python skills/daybook-posting/scripts/daybook_post.py --file diagram.svg --title "Architecture Diagram"
|
||||
python skills/daybook-posting/scripts/daybook_post.py --file results.csv --as-markdown --title "Results CSV"
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --check
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --kind markdown --title "Build Report" --content-file report.md --tags build,report
|
||||
cat report.md | python skills/agentfeed-posting/scripts/agentfeed_post.py --kind markdown --title "Build Report"
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --kind deck --title "Plan" --content-file deck.html
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --file diagram.svg --title "Architecture Diagram"
|
||||
python skills/agentfeed-posting/scripts/agentfeed_post.py --file results.csv --as-markdown --title "Results CSV"
|
||||
```
|
||||
|
||||
The artifact browser URL is printed as the final stdout line.
|
||||
|
|
|
|||
16
scripts/agentfeed_post.py
Normal file
16
scripts/agentfeed_post.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Compatibility wrapper for the AgentFeed posting helper.
|
||||
|
||||
The canonical helper lives inside the self-contained skill folder:
|
||||
skills/agentfeed-posting/scripts/agentfeed_post.py
|
||||
"""
|
||||
|
||||
import runpy
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
HELPER = Path(__file__).resolve().parent.parent / "skills" / "agentfeed-posting" / "scripts" / "agentfeed_post.py"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
runpy.run_path(str(HELPER), run_name="__main__")
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Compatibility wrapper for the Daybook posting helper.
|
||||
|
||||
The canonical helper lives inside the self-contained skill folder:
|
||||
skills/daybook-posting/scripts/daybook_post.py
|
||||
"""
|
||||
|
||||
import runpy
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
HELPER = Path(__file__).resolve().parent.parent / "skills" / "daybook-posting" / "scripts" / "daybook_post.py"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
runpy.run_path(str(HELPER), run_name="__main__")
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Post one demo artifact of every kind, tagged, dated today.
|
||||
|
||||
Posts through the real ingest API using the existing daybook_post.py helper,
|
||||
Posts through the real ingest API using the existing agentfeed_post.py helper,
|
||||
so seeded data goes through the same normalization as real agent output.
|
||||
|
||||
Usage (from project root, with the dev server running):
|
||||
|
|
@ -13,7 +13,7 @@ import sys
|
|||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
HELPER = Path(__file__).resolve().parent.parent / "skills" / "daybook-posting" / "scripts" / "daybook_post.py"
|
||||
HELPER = Path(__file__).resolve().parent.parent / "skills" / "agentfeed-posting" / "scripts" / "agentfeed_post.py"
|
||||
PERSONA = "seed-demo"
|
||||
TAGS = "seed-demo,demo"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
name: daybook-posting
|
||||
description: Use when you have produced a finished deliverable such as a report, summary, brief, diagram, generated HTML, PDF, deck, or status update that a human should review later outside this chat. Publishes the artifact to a Daybook instance and returns a URL.
|
||||
name: agentfeed-posting
|
||||
description: Use when you have produced a finished deliverable such as a report, summary, brief, diagram, generated HTML, PDF, deck, or status update that a human should review later outside this chat. Publishes the artifact to a AgentFeed instance and returns a URL.
|
||||
---
|
||||
|
||||
# Daybook Posting
|
||||
# AgentFeed Posting
|
||||
|
||||
Daybook is a private artifact journal for completed agent outputs. It stores, indexes, and displays artifacts for human review. It does not execute commands, run workflows, or continue conversations.
|
||||
AgentFeed is a private artifact journal for completed agent outputs. It stores, indexes, and displays artifacts for human review. It does not execute commands, run workflows, or continue conversations.
|
||||
|
||||
You post to a **running Daybook server**, which may be anywhere — a cloud VM, a container, or a workstation. You do **not** need a checkout of Daybook's source. The only things that tie you to a particular instance are two environment variables: `DAYBOOK_URL` (where the server is) and `DAYBOOK_TOKEN` (who you are). Set those and the rest of this skill works unchanged regardless of where you or the server run.
|
||||
You post to a **running AgentFeed server**, which may be anywhere — a cloud VM, a container, or a workstation. You do **not** need a checkout of AgentFeed's source. The only things that tie you to a particular instance are two environment variables: `AGENTFEED_URL` (where the server is) and `AGENTFEED_TOKEN` (who you are). Set those and the rest of this skill works unchanged regardless of where you or the server run.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
@ -17,20 +17,20 @@ config) — the helper reads `os.environ` directly. There is no `.env` file
|
|||
support; a `.env` file in the working directory is not read automatically.
|
||||
|
||||
```text
|
||||
DAYBOOK_URL=https://your-daybook-instance.example.com # your server's base URL
|
||||
DAYBOOK_TOKEN=your-personal-api-key # from Settings -> API Keys
|
||||
DAYBOOK_SOURCE=agent # a stable label for what's posting
|
||||
AGENTFEED_URL=https://your-agentfeed-instance.example.com # your server's base URL
|
||||
AGENTFEED_TOKEN=your-personal-api-key # from Settings -> API Keys
|
||||
AGENTFEED_SOURCE=agent # a stable label for what's posting
|
||||
```
|
||||
|
||||
- **`DAYBOOK_URL`** — the base URL of the Daybook instance you're posting to.
|
||||
- **`AGENTFEED_URL`** — the base URL of the AgentFeed instance you're posting to.
|
||||
Set it to your server. If it's unset, the helper falls back to
|
||||
`http://127.0.0.1:8001`, which is only correct when Daybook is running
|
||||
`http://127.0.0.1:8001`, which is only correct when AgentFeed is running
|
||||
locally on the same machine (its development default). For any remote or
|
||||
deployed instance you **must** set this.
|
||||
- **`DAYBOOK_TOKEN`** — a personal API key, created in the app under Settings →
|
||||
- **`AGENTFEED_TOKEN`** — a personal API key, created in the app under Settings →
|
||||
API Keys (name one per agent so you can revoke them individually). Not a key
|
||||
shared by everyone; artifacts posted with it land only in your feed.
|
||||
- **`DAYBOOK_SOURCE`** — a free-text label for whatever is posting
|
||||
- **`AGENTFEED_SOURCE`** — a free-text label for whatever is posting
|
||||
(agent-profile, script, or person name). Optional; can also be passed per
|
||||
call with `--source`.
|
||||
|
||||
|
|
@ -41,29 +41,29 @@ PowerShell multipart, or inline shell content (a `curl` fallback for
|
|||
environments without the helper is documented at the end — the script is
|
||||
still preferred everywhere it's available).
|
||||
|
||||
The helper lives at **`scripts/daybook_post.py` inside this skill's own
|
||||
The helper lives at **`scripts/agentfeed_post.py` inside this skill's own
|
||||
directory** — not in any project you happen to be working in. Invoke it by
|
||||
its absolute path. For brevity, the examples below assume you've captured
|
||||
that path once:
|
||||
|
||||
```bash
|
||||
# Point this at the helper inside THIS skill directory (absolute path).
|
||||
DAYBOOK_POST=/abs/path/to/this/skill/scripts/daybook_post.py
|
||||
AGENTFEED_POST=/abs/path/to/this/skill/scripts/agentfeed_post.py
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
python "$DAYBOOK_POST" --check
|
||||
python "$DAYBOOK_POST" --kind markdown --title "Build Report" --content-file report.md
|
||||
cat report.md | python "$DAYBOOK_POST" --kind markdown --title "Build Report"
|
||||
python "$DAYBOOK_POST" --file diagram.svg --title "Architecture Diagram"
|
||||
python "$AGENTFEED_POST" --check
|
||||
python "$AGENTFEED_POST" --kind markdown --title "Build Report" --content-file report.md
|
||||
cat report.md | python "$AGENTFEED_POST" --kind markdown --title "Build Report"
|
||||
python "$AGENTFEED_POST" --file diagram.svg --title "Architecture Diagram"
|
||||
```
|
||||
|
||||
The artifact URL is printed as the final stdout line.
|
||||
|
||||
(If you *are* working inside a Daybook source checkout, a compatibility
|
||||
wrapper also exists at `scripts/daybook_post.py` in the repo root, but the
|
||||
(If you *are* working inside a AgentFeed source checkout, a compatibility
|
||||
wrapper also exists at `scripts/agentfeed_post.py` in the repo root, but the
|
||||
canonical helper is the one bundled in this skill.)
|
||||
|
||||
## Kind Cheat Sheet
|
||||
|
|
@ -100,7 +100,7 @@ with no API equivalent at all yet.
|
|||
|
||||
## CDN Snippets For `html_full`
|
||||
|
||||
`html_full` is Daybook's escape hatch for custom pages — nothing is
|
||||
`html_full` is AgentFeed's escape hatch for custom pages — nothing is
|
||||
injected into it, so a poster wanting Tailwind or a charting library has to
|
||||
supply its own `<link>`/`<script>` tags. Known-good ones:
|
||||
|
||||
|
|
@ -113,13 +113,13 @@ supply its own `<link>`/`<script>` tags. Known-good ones:
|
|||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
```
|
||||
|
||||
**Offline caveat:** Daybook can run fully self-hosted/air-gapped. Any
|
||||
**Offline caveat:** AgentFeed can run fully self-hosted/air-gapped. Any
|
||||
artifact that links a CDN script won't render fully (or at all, for
|
||||
Tailwind's in-browser compiler) without internet access. If the deployment
|
||||
target is offline, don't reach for these — use `markdown`/`html_fragment`
|
||||
instead, which are styled entirely server-side with no external requests.
|
||||
|
||||
**Self-hosted alternative for tables/JSON — no CDN needed:** Daybook already
|
||||
**Self-hosted alternative for tables/JSON — no CDN needed:** AgentFeed already
|
||||
vendors Grid.js and renderjson locally (used to render `csv`/`json`
|
||||
artifacts), and they're reachable as plain static files from any artifact,
|
||||
including `html_full`:
|
||||
|
|
@ -138,7 +138,7 @@ tables/JSON; there's no vendored equivalent for Tailwind/Chart.js, so the
|
|||
CDN snippets above are still the only option for those.
|
||||
|
||||
Only use this for `html_full` specifically — never add CDN references to
|
||||
`markdown`/`html_fragment`/`deck`. Those kinds exist so Daybook, not the
|
||||
`markdown`/`html_fragment`/`deck`. Those kinds exist so AgentFeed, not the
|
||||
poster, owns styling; injecting a default CDN habit into them would
|
||||
undermine that.
|
||||
|
||||
|
|
@ -149,12 +149,12 @@ artifact), upload it first and reference the returned URL:
|
|||
|
||||
```bash
|
||||
# 1. Upload -> prints the URL as the last line
|
||||
python "$DAYBOOK_POST" --upload-image chart.png
|
||||
python "$AGENTFEED_POST" --upload-image chart.png
|
||||
|
||||
# 2. Reference that URL in your markdown, then post as usual.
|
||||
# The printed URL is absolute against your DAYBOOK_URL, e.g.
|
||||
# 
|
||||
python "$DAYBOOK_POST" report.md
|
||||
# The printed URL is absolute against your AGENTFEED_URL, e.g.
|
||||
# 
|
||||
python "$AGENTFEED_POST" report.md
|
||||
```
|
||||
|
||||
The image is linked to the artifact that references it and is deleted along with it.
|
||||
|
|
@ -167,15 +167,15 @@ just post the file positionally; auto-mode detects `.csv`/`.json` and picks
|
|||
the right kind automatically:
|
||||
|
||||
```bash
|
||||
python "$DAYBOOK_POST" results.csv
|
||||
python "$DAYBOOK_POST" --kind json --title "Run Summary" --content-file summary.json
|
||||
python "$AGENTFEED_POST" results.csv
|
||||
python "$AGENTFEED_POST" --kind json --title "Run Summary" --content-file summary.json
|
||||
```
|
||||
|
||||
`.xlsx` spreadsheets work too, with no special flag needed — just `--file`
|
||||
(or post it positionally, same as any other file):
|
||||
|
||||
```bash
|
||||
python "$DAYBOOK_POST" balance_sheet.xlsx
|
||||
python "$AGENTFEED_POST" balance_sheet.xlsx
|
||||
```
|
||||
|
||||
The server converts it to a `csv` artifact automatically. Known
|
||||
|
|
@ -196,7 +196,7 @@ as a binary, use `--file path --as-markdown` to wrap it in a Markdown
|
|||
fenced code block (still gets syntax highlighting):
|
||||
|
||||
```bash
|
||||
python "$DAYBOOK_POST" --file script.py --as-markdown --title "Generated Script"
|
||||
python "$AGENTFEED_POST" --file script.py --as-markdown --title "Generated Script"
|
||||
```
|
||||
|
||||
Without `--as-markdown`, it uploads as a downloadable binary file instead.
|
||||
|
|
@ -208,7 +208,7 @@ All optional, all default to something reasonable if omitted:
|
|||
| Flag | Default | Notes |
|
||||
|---|---|---|
|
||||
| `--source-type` | `agent` | One of `agent`, `person`, `system`. |
|
||||
| `--source-name` | Same as the effective source (`--source`, or `DAYBOOK_SOURCE` if `--source` wasn't given) | Human-readable name, e.g. "Hermes Agent". |
|
||||
| `--source-name` | Same as the effective source (`--source`, or `AGENTFEED_SOURCE` if `--source` wasn't given) | Human-readable name, e.g. "Hermes Agent". |
|
||||
| `--source-id` | Empty string | A stable external ID, if the poster has one. |
|
||||
| `--source-prompt` | Empty string | The prompt/instruction that produced this artifact — useful for later tracing why something was posted. |
|
||||
|
||||
|
|
@ -219,9 +219,9 @@ context like `run_id` or `model` — these end up in the artifact's
|
|||
|
||||
## Rules
|
||||
|
||||
- Run `--check` first if unsure Daybook is reachable.
|
||||
- Make sure `DAYBOOK_URL` points at the right instance and `DAYBOOK_TOKEN` is set before posting anything real.
|
||||
- Use a stable `DAYBOOK_SOURCE` or pass `--source` (the agent-profile, script, or person name that produced this artifact).
|
||||
- Run `--check` first if unsure AgentFeed is reachable.
|
||||
- Make sure `AGENTFEED_URL` points at the right instance and `AGENTFEED_TOKEN` is set before posting anything real.
|
||||
- Use a stable `AGENTFEED_SOURCE` or pass `--source` (the agent-profile, script, or person name that produced this artifact).
|
||||
- Use `--tags a,b,c` for filtering.
|
||||
- Use `--meta run_id=... --meta model=...` for machine-readable context.
|
||||
- Never inline multiline content into the shell command; use a file or stdin.
|
||||
|
|
@ -236,8 +236,8 @@ with no Python). Text artifacts post as JSON to `POST /api/artifacts` with
|
|||
bearer-token auth:
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "$DAYBOOK_URL/api/artifacts" \
|
||||
-H "Authorization: Bearer $DAYBOOK_TOKEN" \
|
||||
curl -sS -X POST "$AGENTFEED_URL/api/artifacts" \
|
||||
-H "Authorization: Bearer $AGENTFEED_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"kind":"markdown","title":"Build Report","source":"agent","content":"# Hello\n\nBody text."}'
|
||||
```
|
||||
|
|
@ -245,12 +245,12 @@ curl -sS -X POST "$DAYBOOK_URL/api/artifacts" \
|
|||
File/image artifacts use multipart instead:
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "$DAYBOOK_URL/api/artifacts" \
|
||||
-H "Authorization: Bearer $DAYBOOK_TOKEN" \
|
||||
curl -sS -X POST "$AGENTFEED_URL/api/artifacts" \
|
||||
-H "Authorization: Bearer $AGENTFEED_TOKEN" \
|
||||
-F "kind=image" -F "title=Architecture Diagram" -F "source=agent" \
|
||||
-F "file=@diagram.png"
|
||||
```
|
||||
|
||||
`GET $DAYBOOK_URL/api/capabilities` (no auth needed beyond a login/token per
|
||||
`GET $AGENTFEED_URL/api/capabilities` (no auth needed beyond a login/token per
|
||||
that instance's config) lists supported kinds, methods, and the server
|
||||
version if you need to confirm the contract before posting.
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Post artifacts to Daybook without shell-specific JSON or multipart quoting.
|
||||
"""Post artifacts to AgentFeed without shell-specific JSON or multipart quoting.
|
||||
|
||||
Zero third-party dependencies. Content comes from a file or stdin so agents do not
|
||||
need to inline multiline JSON/HTML/Markdown in shell commands.
|
||||
|
|
@ -26,11 +26,11 @@ DEFAULT_SOURCE = "agent"
|
|||
|
||||
def main():
|
||||
args = parse_args()
|
||||
base_url = os.environ.get("DAYBOOK_URL", DEFAULT_URL).rstrip("/")
|
||||
# DAYBOOK_TOKEN is your personal per-user token (Settings -> Account in the app);
|
||||
base_url = os.environ.get("AGENTFEED_URL", DEFAULT_URL).rstrip("/")
|
||||
# AGENTFEED_TOKEN is your personal per-user token (Settings -> API Keys in the app);
|
||||
# there is no shared fallback token, so posting without it cannot work.
|
||||
token = os.environ.get("DAYBOOK_TOKEN", "")
|
||||
source = args.source or os.environ.get("DAYBOOK_SOURCE", DEFAULT_SOURCE)
|
||||
token = os.environ.get("AGENTFEED_TOKEN", "")
|
||||
source = args.source or os.environ.get("AGENTFEED_SOURCE", DEFAULT_SOURCE)
|
||||
|
||||
if args.check:
|
||||
try:
|
||||
|
|
@ -43,8 +43,8 @@ def main():
|
|||
|
||||
if not token:
|
||||
print(
|
||||
"error: DAYBOOK_TOKEN is not set. Get your personal API token from "
|
||||
f"{base_url}/settings/account/ and set DAYBOOK_TOKEN.",
|
||||
"error: AGENTFEED_TOKEN is not set. Get your personal API token from "
|
||||
f"{base_url}/settings/api-keys/ and set AGENTFEED_TOKEN.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
|
@ -84,11 +84,11 @@ def main():
|
|||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Publish an artifact to Daybook.")
|
||||
parser = argparse.ArgumentParser(description="Publish an artifact to AgentFeed.")
|
||||
parser.add_argument("path", nargs="?", help="Auto mode: one file path. Kind, content-type, and title are inferred. The simplest way to post.")
|
||||
parser.add_argument("--kind", choices=sorted(TEXT_KINDS), help="Text artifact kind. Omit when using --file or a positional path.")
|
||||
parser.add_argument("--title", help="Artifact title. Required unless --check is used.")
|
||||
parser.add_argument("--source", help="Artifact source (agent-profile, script, or person name). Defaults to DAYBOOK_SOURCE or 'agent'.")
|
||||
parser.add_argument("--source", help="Artifact source (agent-profile, script, or person name). Defaults to AGENTFEED_SOURCE or 'agent'.")
|
||||
parser.add_argument("--content-file", help="Read text artifact content from this file.")
|
||||
parser.add_argument("--file", help="Upload this file as image/pdf/binary, or render as markdown with --as-markdown.")
|
||||
parser.add_argument("--tags", help="Comma-separated tags.")
|
||||
|
|
@ -352,7 +352,7 @@ def post_payload(base_url, token, payload, args):
|
|||
|
||||
|
||||
def multipart_body(payload):
|
||||
boundary = f"----daybook-{uuid.uuid4().hex}"
|
||||
boundary = f"----agentfeed-{uuid.uuid4().hex}"
|
||||
chunks = []
|
||||
upload_path = payload["upload_path"]
|
||||
for key, value in payload.items():
|
||||
|
|
@ -394,11 +394,11 @@ def content_type_for(path):
|
|||
|
||||
|
||||
def upload_asset(base_url, token, path):
|
||||
"""Upload an image and return its Daybook URL for embedding in content."""
|
||||
"""Upload an image and return its AgentFeed URL for embedding in content."""
|
||||
if not path.is_file():
|
||||
raise ValueError(f"image file does not exist: {path}")
|
||||
|
||||
boundary = f"----daybook-{uuid.uuid4().hex}"
|
||||
boundary = f"----agentfeed-{uuid.uuid4().hex}"
|
||||
chunks = [
|
||||
f"--{boundary}\r\n".encode("utf-8"),
|
||||
f'Content-Disposition: form-data; name="file"; filename="{path.name}"\r\n'.encode("utf-8"),
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
{% load daybook_extras %}
|
||||
{% load agentfeed_extras %}
|
||||
<header class="topbar">
|
||||
<div class="brandline">
|
||||
<a class="brand-link" href="{% url 'feed' %}"><img class="brand-logo" src="/static/logo.png" alt=""><h1>AgentFeed</h1></a>
|
||||
|
|
@ -48,10 +48,10 @@
|
|||
const isDark = document.documentElement.getAttribute("data-theme") === "dark";
|
||||
if (isDark) {
|
||||
document.documentElement.removeAttribute("data-theme");
|
||||
try { localStorage.setItem("daybook.theme", "light"); } catch (e) {}
|
||||
try { localStorage.setItem("agentfeed.theme", "light"); } catch (e) {}
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
try { localStorage.setItem("daybook.theme", "dark"); } catch (e) {}
|
||||
try { localStorage.setItem("agentfeed.theme", "dark"); } catch (e) {}
|
||||
}
|
||||
syncLabel();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
<title>AgentFeed</title>
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="/static/daybook.css">
|
||||
<link rel="stylesheet" href="/static/agentfeed.css">
|
||||
<script>
|
||||
// Set the theme before first paint so dark-mode users never see a light flash.
|
||||
(function () {
|
||||
try {
|
||||
if (localStorage.getItem("daybook.theme") === "dark") {
|
||||
if (localStorage.getItem("agentfeed.theme") === "dark") {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
}
|
||||
} catch (e) {}
|
||||
|
|
@ -141,8 +141,8 @@
|
|||
const sourceSelected = Boolean(feed.dataset.selectedSource);
|
||||
|
||||
const sizes = {
|
||||
days: Number(localStorage.getItem("daybook.daysWidth")) || 144,
|
||||
artifacts: Number(localStorage.getItem("daybook.artifactsWidth")) || 280,
|
||||
days: Number(localStorage.getItem("agentfeed.daysWidth")) || 144,
|
||||
artifacts: Number(localStorage.getItem("agentfeed.artifactsWidth")) || 280,
|
||||
};
|
||||
|
||||
const applySizes = () => {
|
||||
|
|
@ -177,7 +177,7 @@
|
|||
};
|
||||
|
||||
const up = () => {
|
||||
localStorage.setItem(`daybook.${target}Width`, String(sizes[target]));
|
||||
localStorage.setItem(`agentfeed.${target}Width`, String(sizes[target]));
|
||||
document.body.classList.remove("is-resizing");
|
||||
splitter.removeEventListener("pointermove", move);
|
||||
splitter.removeEventListener("pointerup", up);
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
<title>Log in — AgentFeed</title>
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="/static/daybook.css">
|
||||
<link rel="stylesheet" href="/static/agentfeed.css">
|
||||
<script>
|
||||
// Set the theme before first paint so dark-mode users never see a light flash.
|
||||
(function () {
|
||||
try {
|
||||
if (localStorage.getItem("daybook.theme") === "dark") {
|
||||
if (localStorage.getItem("agentfeed.theme") === "dark") {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
}
|
||||
} catch (e) {}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div class="about-head">
|
||||
<img class="about-logo" src="/static/logo.png" alt="">
|
||||
<div>
|
||||
<h2>Daybook</h2>
|
||||
<h2>AgentFeed</h2>
|
||||
<p class="about-version">Version {{ version }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<h3>API Keys</h3>
|
||||
<p>
|
||||
Posting to Daybook uses named API keys — one per agent or script, each
|
||||
Posting to AgentFeed uses named API keys — one per agent or script, each
|
||||
revocable on its own. You have {{ key_count }} key{{ key_count|pluralize }}.
|
||||
</p>
|
||||
<p><a href="{% url 'settings-api-keys' %}">Manage API keys →</a></p>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<h2>API Keys</h2>
|
||||
<p>
|
||||
One key per agent or script. Name each key so you know which agent it's
|
||||
for, copy it into that agent's <code>DAYBOOK_TOKEN</code>, and revoke keys
|
||||
for, copy it into that agent's <code>AGENTFEED_TOKEN</code>, and revoke keys
|
||||
individually here. Use a key as
|
||||
<code>Authorization: Bearer <key></code> when posting.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
<title>Settings — AgentFeed</title>
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="/static/daybook.css">
|
||||
<link rel="stylesheet" href="/static/agentfeed.css">
|
||||
<script>
|
||||
// Set the theme before first paint so dark-mode users never see a light flash.
|
||||
(function () {
|
||||
try {
|
||||
if (localStorage.getItem("daybook.theme") === "dark") {
|
||||
if (localStorage.getItem("agentfeed.theme") === "dark") {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
}
|
||||
} catch (e) {}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from artifacts.models import ApiKey
|
|||
User = get_user_model()
|
||||
|
||||
|
||||
@override_settings(DAYBOOK_KEY_PROVISIONING_API=True)
|
||||
@override_settings(AGENTFEED_KEY_PROVISIONING_API=True)
|
||||
class ApiKeyProvisioningEnabledTestCase(APITestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username="alice", password="pw")
|
||||
|
|
@ -78,7 +78,7 @@ class ApiKeyProvisioningEnabledTestCase(APITestCase):
|
|||
self.assertEqual(response.data["methods"]["/api/keys"], ["GET", "POST"])
|
||||
|
||||
|
||||
@override_settings(DAYBOOK_KEY_PROVISIONING_API=False)
|
||||
@override_settings(AGENTFEED_KEY_PROVISIONING_API=False)
|
||||
class ApiKeyProvisioningDisabledTestCase(APITestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username="alice", password="pw")
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class ArtifactAPITestCase(APITestCase):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.artifacts_root = Path(tempfile.mkdtemp(prefix="daybook-test-artifacts-"))
|
||||
cls.artifacts_root = Path(tempfile.mkdtemp(prefix="agentfeed-test-artifacts-"))
|
||||
cls.settings_override = override_settings(ARTIFACTS_ROOT=cls.artifacts_root)
|
||||
cls.settings_override.enable()
|
||||
|
||||
|
|
@ -96,9 +96,9 @@ class ArtifactAPITestCase(APITestCase):
|
|||
body = response.content.decode()
|
||||
# Frontmatter is intact and the config placeholder is rewritten to this
|
||||
# instance's origin (http://testserver under the test client).
|
||||
self.assertIn("name: daybook-posting", body)
|
||||
self.assertIn("DAYBOOK_URL=http://testserver", body)
|
||||
self.assertNotIn("your-daybook-instance.example.com", body)
|
||||
self.assertIn("name: agentfeed-posting", body)
|
||||
self.assertIn("AGENTFEED_URL=http://testserver", body)
|
||||
self.assertNotIn("your-agentfeed-instance.example.com", body)
|
||||
|
||||
def test_skill_json_format(self):
|
||||
response = self.client.get(
|
||||
|
|
@ -107,7 +107,7 @@ class ArtifactAPITestCase(APITestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["version"], __version__)
|
||||
self.assertEqual(response.data["url"], "http://testserver")
|
||||
self.assertIn("name: daybook-posting", response.data["skill"])
|
||||
self.assertIn("name: agentfeed-posting", response.data["skill"])
|
||||
|
||||
def test_skill_with_session(self):
|
||||
self.client.force_login(self.user)
|
||||
|
|
@ -442,7 +442,7 @@ class ArtifactAPITestCase(APITestCase):
|
|||
self.assertEqual(stored_html.count("</script>"), 3)
|
||||
|
||||
# The escaped content must still round-trip to the exact original value.
|
||||
start = stored_html.index('id="daybook-data">') + len('id="daybook-data">')
|
||||
start = stored_html.index('id="agentfeed-data">') + len('id="agentfeed-data">')
|
||||
end = stored_html.index("</script>", start)
|
||||
embedded = stored_html[start:end]
|
||||
self.assertEqual(json.loads(embedded)["value"], "</script><script>alert(1)</script>")
|
||||
|
|
|
|||
|
|
@ -6,76 +6,76 @@ from pathlib import Path
|
|||
SCRIPT_PATH = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "skills"
|
||||
/ "daybook-posting"
|
||||
/ "agentfeed-posting"
|
||||
/ "scripts"
|
||||
/ "daybook_post.py"
|
||||
/ "agentfeed_post.py"
|
||||
)
|
||||
|
||||
|
||||
def _load_module():
|
||||
spec = importlib.util.spec_from_file_location("daybook_post", SCRIPT_PATH)
|
||||
spec = importlib.util.spec_from_file_location("agentfeed_post", SCRIPT_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
daybook_post = _load_module()
|
||||
agentfeed_post = _load_module()
|
||||
|
||||
|
||||
class SniffTextKindTestCase(unittest.TestCase):
|
||||
def test_markdown_suffixes(self):
|
||||
for suffix in (".md", ".markdown", ".txt"):
|
||||
self.assertEqual(daybook_post.sniff_text_kind(suffix, "anything"), "markdown")
|
||||
self.assertEqual(agentfeed_post.sniff_text_kind(suffix, "anything"), "markdown")
|
||||
|
||||
def test_json_suffix(self):
|
||||
self.assertEqual(daybook_post.sniff_text_kind(".json", '{"a": 1}'), "json")
|
||||
self.assertEqual(agentfeed_post.sniff_text_kind(".json", '{"a": 1}'), "json")
|
||||
|
||||
def test_csv_suffix(self):
|
||||
self.assertEqual(daybook_post.sniff_text_kind(".csv", "a,b\n1,2"), "csv")
|
||||
self.assertEqual(agentfeed_post.sniff_text_kind(".csv", "a,b\n1,2"), "csv")
|
||||
|
||||
def test_html_full_sniffed_from_content(self):
|
||||
self.assertEqual(
|
||||
daybook_post.sniff_text_kind(".html", "<!doctype html><html></html>"), "html_full"
|
||||
agentfeed_post.sniff_text_kind(".html", "<!doctype html><html></html>"), "html_full"
|
||||
)
|
||||
|
||||
def test_deck_sniffed_from_content(self):
|
||||
self.assertEqual(daybook_post.sniff_text_kind(".html", "<section>slide</section>"), "deck")
|
||||
self.assertEqual(agentfeed_post.sniff_text_kind(".html", "<section>slide</section>"), "deck")
|
||||
|
||||
def test_html_fragment_is_the_fallback(self):
|
||||
self.assertEqual(daybook_post.sniff_text_kind(".html", "<p>hi</p>"), "html_fragment")
|
||||
self.assertEqual(agentfeed_post.sniff_text_kind(".html", "<p>hi</p>"), "html_fragment")
|
||||
|
||||
|
||||
class ValidatePayloadTestCase(unittest.TestCase):
|
||||
def test_valid_json_passes(self):
|
||||
daybook_post.validate_payload({"kind": "json", "content": '{"a": 1}'}, None) # no raise
|
||||
agentfeed_post.validate_payload({"kind": "json", "content": '{"a": 1}'}, None) # no raise
|
||||
|
||||
def test_invalid_json_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
daybook_post.validate_payload({"kind": "json", "content": "{not json"}, None)
|
||||
agentfeed_post.validate_payload({"kind": "json", "content": "{not json"}, None)
|
||||
|
||||
def test_deck_without_section_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
daybook_post.validate_payload({"kind": "deck", "content": "<h1>no slides</h1>"}, None)
|
||||
agentfeed_post.validate_payload({"kind": "deck", "content": "<h1>no slides</h1>"}, None)
|
||||
|
||||
def test_html_fragment_with_nested_html_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
daybook_post.validate_payload(
|
||||
agentfeed_post.validate_payload(
|
||||
{"kind": "html_fragment", "content": "<html><body>x</body></html>"}, None
|
||||
)
|
||||
|
||||
def test_html_full_without_html_tag_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
daybook_post.validate_payload({"kind": "html_full", "content": "<p>x</p>"}, None)
|
||||
agentfeed_post.validate_payload({"kind": "html_full", "content": "<p>x</p>"}, None)
|
||||
|
||||
def test_blank_markdown_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
daybook_post.validate_payload({"kind": "markdown", "content": " "}, None)
|
||||
agentfeed_post.validate_payload({"kind": "markdown", "content": " "}, None)
|
||||
|
||||
def test_csv_has_no_shape_check(self):
|
||||
daybook_post.validate_payload({"kind": "csv", "content": "anything at all"}, None) # no raise
|
||||
agentfeed_post.validate_payload({"kind": "csv", "content": "anything at all"}, None) # no raise
|
||||
|
||||
def test_upload_path_skips_all_checks(self):
|
||||
daybook_post.validate_payload({"upload_path": Path("whatever.png")}, None) # no raise
|
||||
agentfeed_post.validate_payload({"upload_path": Path("whatever.png")}, None) # no raise
|
||||
|
||||
|
||||
class ResolveAutoPathTestCase(unittest.TestCase):
|
||||
|
|
@ -93,7 +93,7 @@ class ResolveAutoPathTestCase(unittest.TestCase):
|
|||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "data.json"
|
||||
path.write_text('{"a": 1}', encoding="utf-8")
|
||||
args = daybook_post.resolve_auto_path(self._args(path))
|
||||
args = agentfeed_post.resolve_auto_path(self._args(path))
|
||||
self.assertEqual(args.kind, "json")
|
||||
self.assertEqual(args.content_file, str(path))
|
||||
|
||||
|
|
@ -101,14 +101,14 @@ class ResolveAutoPathTestCase(unittest.TestCase):
|
|||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "data.csv"
|
||||
path.write_text("a,b\n1,2", encoding="utf-8")
|
||||
args = daybook_post.resolve_auto_path(self._args(path))
|
||||
args = agentfeed_post.resolve_auto_path(self._args(path))
|
||||
self.assertEqual(args.kind, "csv")
|
||||
|
||||
def test_image_file_becomes_multipart_upload(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "chart.png"
|
||||
path.write_bytes(b"\x89PNG\r\n")
|
||||
args = daybook_post.resolve_auto_path(self._args(path))
|
||||
args = agentfeed_post.resolve_auto_path(self._args(path))
|
||||
self.assertIsNone(args.kind)
|
||||
self.assertEqual(args.file, str(path))
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ class MakeRoomForTestCase(TestCase):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.artifacts_root = Path(tempfile.mkdtemp(prefix="daybook-test-storage-"))
|
||||
cls.artifacts_root = Path(tempfile.mkdtemp(prefix="agentfeed-test-storage-"))
|
||||
cls.settings_override = override_settings(ARTIFACTS_ROOT=cls.artifacts_root)
|
||||
cls.settings_override.enable()
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class SharingTestCase(TestCase):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.artifacts_root = Path(tempfile.mkdtemp(prefix="daybook-test-sharing-"))
|
||||
cls.artifacts_root = Path(tempfile.mkdtemp(prefix="agentfeed-test-sharing-"))
|
||||
cls.settings_override = override_settings(ARTIFACTS_ROOT=cls.artifacts_root)
|
||||
cls.settings_override.enable()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue