mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-07-15 22:32:29 -05:00
The 30s timeout on the client.New HTTPClient was hard-coded and opaque. Long-running paginated reads against slow networks were tripping it with no escape hatch. Lift the value into a named constant and let .veans.yml override it via a new optional http_timeout field (Go duration syntax, e.g. "60s", "5m"). The field has omitempty so a freshly-written .veans.yml from `veans init` doesn't surface the knob — operators who need to tune it can hand-edit, but it stays out of the way for the common case. Runtime loader applies the override after client.New if set; bootstrap- and login-time clients (built before .veans.yml exists) keep the default.
10 KiB
10 KiB
AGENT Instructions for veans
Things to know before touching this submodule that aren't obvious from
reading the code. The parent repo's CLAUDE.md covers the rest of Vikunja;
this file is veans-specific.
Module layout
veans/is its own Go module (code.vikunja.io/veans), separate from the parent. Don't try to importcode.vikunja.io/api/...— that pulls XORM into the CLI binary. Wire types live ininternal/client/types.goas plain JSON-tagged structs that mirror the parent models.- License headers are enforced by
goheaderinveans/.golangci.yml. Every new.gofile needs the AGPLv3 banner fromveans/code-header-template.txt(a copy of the parent's, kept local so the linter resolves the path relative to this module).
Building and testing
mage build→./veansbinary. TheAliasesmap inmagefile.goroutes bare names likemage testtoTest.All— without aliases, mage rejects namespace invocations ("Unknown target specified").- Unit tests:
mage test(passes-short) orgo test -short ./.... The e2e package'sTestMaingates the suite on-short, mirroring the parent monorepo'spkg/webtestsconvention. Without-shortand withoutVEANS_E2E_API_URLset, the e2e tests fail loudly with a "configure or pass -short" hint. - E2e tests:
mage test:e2e(no-short). Assumes an externally- running Vikunja atVEANS_E2E_API_URL. The harness seeds its own admin user viaPATCH /api/v1/test/users— same mechanism the playwright suite uses — so the API must be booted withVIKUNJA_SERVICE_TESTINGTOKEN=<token>and the same value passed in viaVEANS_E2E_TESTING_TOKEN. Alternative path:VEANS_E2E_ADMIN_TOKEN=<jwt>skips the seed and uses the given token as-is, for driving a long-lived Vikunja the suite shouldn't mutate user rows on. - Local e2e loop: from the parent repo root, build the API
(
mage build:build), run it with sqlite-memory + a known JWT secret +VIKUNJA_SERVICE_TESTINGTOKEN, thenmage test:e2efromveans/withVEANS_E2E_API_URL+VEANS_E2E_TESTING_TOKEN. No manual seeding step — the test harness handles it. - CI: the
test-veans-e2ejob in.github/workflows/test.ymlconsumes the existingvikunja_binartifact fromapi-build; don't recompile the API in a parallel workflow. Theveans-testjob runs unit tests with-shortfor fast feedback, independent ofapi-build.
Vikunja wire-format gotchas
Most failures surface when crossing the JSON boundary. The list below is what's bitten me; if a new endpoint behaves oddly, suspect one of these:
ProjectView.view_kindandbucket_configuration_modeare strings, not ints. The parent enums (ProjectViewKind,BucketConfigurationModeKind) have customMarshalJSONthat emits"kanban"/"manual"etc. Use the string constants ininternal/client/types.go.Task.BucketIDis always 0 inGET /tasks/:id. The model hasxorm:"-"on it — the actual bucket lives in a separatetask_bucketstable. Fetch with?expand=bucketsand usetask.CurrentBucketID(viewID)to read it.POST /tasks/{id}does NOT move tasks between buckets. The task↔bucket relation is row-shaped; useclient.MoveTaskToBucket()which hitsPOST /projects/{p}/views/{v}/buckets/{b}/tasks. The Update path on the server only auto-moves ondoneflips.- Bot user creation is
PUT /user/bots, not/bots— the routes are registered under the/usersubgroup. Same prefix forGET /user/bots. APIToken.expires_atis required. The struct field hasvalid:"required"upstream; sending it omitted or zero fails validation. Useclient.FarFuture(year 9999) when you mean "no expiry" — the frontend does the same.- Task descriptions and comments are HTML, not markdown. The
Vikunja web UI uses TipTap, which calls
getHTML()on save. The stored field is therefore HTML. The agent prompt template (internal/commands/prompt.tmpl) teaches agents the canonical TipTap shapes — most importantly<ul data-type="taskList">+<li data-type="taskItem" data-checked="false"><p>…</p></li>for interactive checkboxes. We deliberately do not convert markdown↔HTML in the CLI; the agent writes HTML directly, which avoids lossy roundtrips on--description-replace-old/new.veans showdisplays the raw HTML; humans skim it fine.
API token permissions
- Vikunja validates token
permissionsagainstapiTokenRoutes, a map built dynamically from registered routes. Group names are derived from the URL path (params stripped, joined by_). Examples:/projects/:project/views/:view/buckets/:bucket/tasks→ groupprojects, actionviews_buckets_tasks/tasks/:task/comments→ grouptasks_comments, actioncreate
client.PermissionsForBot()callsGET /routesat runtime and grants only the intersection of what we want and what the server exposes. Don't hard-code permission group names — they drift across Vikunja versions, and discovery keeps the bot's grant valid across upgrades.
Bot ownership and token minting
- Creating a bot via
PUT /user/botsautomatically sets the bot'sbot_owner_idto the calling user. Only the owner can mint tokens for the bot viaPUT /tokenswithowner_id=<bot_id>. The init flow does these as a single human-JWT-authenticated batch. - Bots have no password and cannot authenticate via
POST /login. After init,veans loginre-authenticates as the human (not the bot) and mints a fresh bot token.
OAuth flow
- Vikunja's authorization server requires PKCE/S256 and accepts either
vikunja-…://custom schemes or RFC 8252 loopback URIs (http://127.0.0.1:NNN/,http://localhost:NNN/,http://[::1]:NNN/). No client registration needed —client_idcan be any consistent string (we useveans-cli). internal/auth/oauth.gobinds a free port on 127.0.0.1, opens the browser, and captures the callback. TheShutdowndefer usescontext.WithoutCancel(ctx)so cancellation at the outer scope still drains the loopback server cleanly.- Token exchange is JSON only. Form-encoded POSTs to
/oauth/tokenfail; the standardgolang.org/x/oauth2client speaks form encoding, which is why we have a hand-rolledclient.ExchangeOAuthCode.
Credential store
- Lookup chain: keychain → env (
VEANS_TOKEN) → file (~/.config/veans/credentials.yml, mode 0600, atomic-write + flock serialization).XDG_CONFIG_HOMEis deliberately not honored — agent-only audience runs in a known environment, and the env var was a path-traversal seam for no real benefit. Chain.Setfalls through to the next backend on error so a missing dbus on a CI runner doesn't block writes — the file backend is the reliable last-resort.- File writes go through a tmp file +
Rename, withChmod 0o600re-asserted on the destination inode so a pre-existing wider mode is narrowed. Concurrent writers (e.g. twoveans loginruns) are serialized viaflockon<path>.lock(Unix only; Windows is a no-op stub since the audience is Linux/macOS). - E2e tests override
HOMEper test andfilterEnv(..., "VEANS_")strips any inheritedVEANS_TOKENso the developer's keyring stays untouched. Don't bypass the credentials package in tests — leaks between tests will surface as the wrong bot token.
Project identifiers and bot usernames
- Project
Identifierisrunelength(0|10), can be empty. When empty,Config.FormatTaskIDrenders#NN; otherwisePROJ-NN. Both are accepted byruntime.resolveTaskIDalong with bare integers. - Bot username must start with
bot-; the server enforces it. Hyphens, digits, lowercase letters allowed; no spaces, no commas, nolink-share-Npattern.config.SuggestedBotUsernamedoes the folding for repo names. - E2e tests deriving identifiers from a unique suffix should use the
trailing chars of
strconv.FormatInt(time.Now().UnixNano(), 36). The leading chars barely change between consecutive runs and will collide if you take[:N].
Audience split
The CLI is agent-only at runtime; humans never use it for day-to-day work (they use Vikunja's web UI). Two commands serve a human running one-off setup:
init— bootstrap a repo: pick project + view, create bot, share, mint token, write.veans.yml, install hooks.login— rotate the bot's token.
Everything else (list, show, create, update, claim, api,
prime, version) is agent-only:
- Emits JSON on stdout unconditionally. No
--jsonflag, no human-formatted variant.listis a raw array;show/create/update/claimreturn a single task object. - Errors are JSON on stderr with non-zero exit — same envelope
everywhere (
{"code": "...", "error": "..."}), regardless of which command ran. Stable codes ininternal/output/errors.go:NOT_FOUND,CONFLICT,VALIDATION_ERROR,AUTH_ERROR,RATE_LIMITED,BOT_USERS_UNAVAILABLE,NOT_CONFIGURED,UNKNOWN. Don't add ad-hoc strings — wrap withoutput.New/output.Wrap. - No
globals.JSON, no dual rendering paths. If you find yourself reaching for "if interactive, do X" on an agent-facing command, stop — it's not interactive, an agent is on the other end.
Cobra surface conventions
RunEhandlers that don't useargs []stringshould rename it to_to satisfy revive'sunused-parameterrule.- The bucket-move dance (
MoveTaskToBucket) runs after the field update onupdate, so a status transition can't clobber freshly attached labels. Comments for--status scrappedpost before the bucket move so the audit trail reads in chronological order. - Agent-facing commands return the task via
json.NewEncoder(...).Encode(task). Adding new top-level keys toclient.Taskis an implicit API change — bumpprime's "useful fields" note alongside.
Things to not do
- Don't add an
os/exec.Commandwithout ctx —noctxis enabled. Useexec.CommandContext(ctx, …)and thread the context through. - Don't commit the built binary.
veans/.gitignorecovers./veansand./veans.exe. - Don't write to stdout from
primewhen no.veans.ymlis found. The hook contract is silent + exit 0 so the snippet is safe to install globally in~/.claude/settings.json. - Don't rename canonical bucket titles without updating
BucketTitleAliases[s][0]ininternal/status/status.go, the prompt template (internal/commands/prompt.tmpl), and the e2e assertions in lockstep — agents and humans both treat them as fixed strings.