Commit Graph

14181 Commits

Author SHA1 Message Date
kolaente 95f65c4712 ci: prebuild static build/mage for repo metadata containers
publish-repos runs inside ubuntu/fedora/archlinux containers that don't
ship a Go toolchain. Compile build/magefile.go into a static binary in
the test workflow (mirroring the existing mage_bin job for the parent
magefile) and upload it as the build_mage_bin artifact so publish-repos
can chmod+x and run it without setup-go.
2026-05-27 13:01:44 +00:00
kolaente c690f74d75 feat(ci): add release-binaries and release-os-package composite actions
Two reusable composite actions wrap the CI side of the release pipeline:

- release-binaries: setup-go, install mage + upx, cache xgo, invoke
  `mage release:build <project>` from build/, GPG-sign the zip bundles,
  upload to S3, store binaries and zips as workflow artifacts.

- release-os-package: download a binaries artifact, install mage,
  `mage release:prepare-nfpm-config <project> <arch>`, stage the binary,
  nfpm pack (with rpm signing inline and archlinux signing after), upload
  to S3, store the package as an artifact.

Both actions are parameterized on project name, output paths, artifact
names, S3 target, and GPG/S3 secrets — adding a third Go binary to the
monorepo just means defining its project in build/magefile.go and adding
a four-line call site in release.yml.
2026-05-27 13:01:44 +00:00
kolaente 8313d032ea feat(build): add centralized release magefile module
New build/ Go module hosts the full release pipeline (xgo cross-compile,
upx, sha256, zip bundles, nfpm templating, deb/rpm/apk repo metadata)
for every Go binary in the monorepo. Parametric on project name —
`mage release:build vikunja` and `mage release:build veans` both flow
through the same code.

The module is intentionally self-contained: it depends on nothing but
stdlib + mage, and duplicates the small filesystem helpers (copyFile,
moveFile, sha256File) rather than importing them from a project
magefile. That keeps the release tooling free to evolve without
touching project code.
2026-05-27 13:01:44 +00:00
kolaente f39cf00290 ci: register custom actionlint runner labels
Declare namespace-profile-default and blacksmith-8vcpu-ubuntu-2204 as
known self-hosted runners so actionlint stops flagging them as unknown.
2026-05-27 13:01:44 +00:00
kolaente 5f00fca166 feat(veans): build and publish veans alongside vikunja
Cross-compile veans for the same OS/arch matrix as the main vikunja
binary, wrap each into a signed zip, build deb/rpm/apk/archlinux
packages via nfpm, and merge those into the existing dl.vikunja.io
package repos so `apt install veans` works from the same source.

- veans/magefile.go: Release namespace (xgo cross-compile, upx, sha256,
  per-target zip bundle, nfpm.yaml templating).
- veans/nfpm.yaml: minimal — binary at /usr/local/bin/veans, no service
  or postinstall.
- .github/workflows/release.yml: veans-binaries + veans-os-package
  jobs, veans artifacts merged into publish-repos and create-release.
S3 layout mirrors vikunja under /veans/<version>/.
2026-05-27 13:01:44 +00:00
kolaente d5ab54941f
fix(deps): bump ip-address to >=10.1.1 in desktop workspace
Resolves a medium-severity XSS in Address6 HTML-emitting methods
(GHSA / Dependabot alert #224). Vulnerable range: <=10.1.0,
patched in 10.1.1. The package is pulled in transitively through
socks -> socks-proxy-agent in the Electron build chain
(devDependency only), but we add a pnpm override to ensure the
patched version is used everywhere. The frontend workspace already
has the equivalent override.
2026-05-27 11:10:24 +02:00
kolaente e08f05119d
fix(deps): bump qs to 6.15.2 in desktop
Resolves Dependabot alert #233: qs.stringify crashes with TypeError on
null/undefined entries in comma-format arrays when encodeValuesOnly is
set (DoS, medium severity).

Updates transitive dependency via pnpm update from 6.15.0 to 6.15.2.
2026-05-27 11:09:27 +02:00
kolaente 7be5026113
fix(deps): bump tmp to >=0.2.6 to fix path traversal vulnerability
Adds a pnpm override for `tmp` in both the `frontend` and `desktop`
workspaces to force the patched version (0.2.6). The previous transitive
resolutions (`tmp@0.0.33` via external-editor in frontend, `tmp@0.2.3`
via tmp-promise in desktop) are vulnerable to a path traversal via
unsanitized prefix/postfix that enables directory escape.

Addresses Dependabot alerts #234 (desktop) and #235 (frontend).
2026-05-27 11:09:20 +02:00
Tink bot 98affb265a test(veans): cover bootstrap validators and prompt UX
Three helpers I added recently have no e2e coverage because the
suite always passes --bot-username with a valid name and
--yes-buckets to skip prompts.

Nine tests in a new bootstrap_test.go:

- TestValidateBotUsername — table-driven, 18 rows: valid shapes
  (bot-foo, bot-foo-bar, bot-foo123, bot-foo_bar, bot-foo.bar,
  bot-a), invalid prefix (foo, Bot-foo, ""), invalid chars
  (spaces, commas, uppercase, !, embedded space), the reserved
  link-share-N pattern, and the bare "bot-" edge.

- TestConfirmOverwriteExistingConfig — file-missing path, the
  OverwriteExistingConfig=true short-circuit, every interesting
  prompt answer (y, yes, Y, Yes, "  yes  " → proceed; n, "",
  garbage → CodeConflict with path in message; prompter error
  → CodeUnknown wrapping the original via errors.Is).

- TestBootstrapBuckets_{AllPresent,AutoApprove,PromptDeclined,
  PromptAborted,PromptUnknownCap,PromptAccepted} — drive the
  function against a stub httptest server (bucketServer helper)
  that records ListBuckets responses and CreateBucket payloads,
  with a scripted queuePrompter for the prompt-driven cases.
  Covers the alias-match short circuit, the auto-approve path,
  the new declined/aborted/retry-cap paths, and the y-accepted
  path.

Local helpers (queuePrompter for scripted answers with injectable
error; bucketServer for the stubbed bucket endpoints) stay in the
test file — no production code changes.
2026-05-27 08:21:57 +00:00
Tink bot 964fdb71d1 test(veans): cover OAuth callback handler error paths
The e2e suite bypasses the OAuth flow via --token, so the callback
handler's error branches had zero coverage. Eight tests appended to
oauth_test.go drive the handler directly:

- happy path: code+state arrive on the channel; response is HTML
- authz-server error path: ?error=access_denied&error_description=…
  bubbles up as a non-nil err containing the description (not the code)
- only-code fallback: when error_description is missing, the error
  message falls back to the error code
- empty code: handler captures it; waitForCallback's job to reject
- non-GET method: 405 with Allow: GET, nothing pushed to channel
  (defense against forged POST from a same-origin page)
- wrong path: 404, nothing pushed
- HTML-escaping: an error containing <script>…</script> renders as
  &lt;script&gt; — XSS regression guard
- nil-err success page: 200 with 'veans is authorized'

Plus generateState shape coverage (length, charset, uniqueness)
to match the existing TestGeneratePKCE_*.

Sanity-checked the XSS test by deleting the html.EscapeString call —
it fails with raw <script> in the body. Restored.
2026-05-27 08:21:57 +00:00
Tink bot 4cda019336 test(veans): cover client error mapping, pagination, and 404→BOT_USERS_UNAVAILABLE
internal/client/ had no coverage for the helpers that turn HTTP
responses into the stable error envelope. e2e exercises happy paths
but never asserts the envelope's Code field for each status, so a
refactor of mapHTTPError could silently drift.

Seven tests in client_test.go:

- TestMapHTTPError_StatusCodeMapping table-drives 401/403/404/409/
  429/400/422/500 → the right output.Code constants.
- TestMapHTTPError_RetryAfterAppendedToMessage asserts the
  (retry-after <dur>) suffix on 429s.
- TestMapHTTPError_BodyTruncation pumps 600 bytes and asserts the
  message ends with …(truncated) and Cause stays nil (per the earlier
  'drop synthetic Cause' change).
- TestMapHTTPError_VikunjaJSONTakesPrecedenceOverRawBody asserts the
  parsed {code,message} payload wins over the raw body for the
  embedded message text.
- TestParseRetryAfter handles delta-seconds, HTTP-date forms (with a
  tolerance window because the parser uses time.Until), and the
  unparseable/empty/negative/past-date cases.
- TestPaginationDone covers the header-authoritative and len-heuristic
  branches across full, short, and empty pages.
- TestCreateBotUser_404TranslatesToBotUsersUnavailable drives a
  fake httptest server returning 404 on PUT /api/v1/user/bots and
  asserts the error code is BOT_USERS_UNAVAILABLE (the translation
  lives in users.go:37-42).
2026-05-27 08:21:57 +00:00
Tink bot 9b95d05811 test(veans): cover the stable error-envelope contract
internal/output/ had zero unit tests; the envelope shape it produces
is consumed by every agent integration on the other side of stdin, so
locking it down with a small test file is high-leverage.

Six tests in errors_test.go:
- TestAsError_Nil, TestAsError_PreservesKnownCode,
  TestAsError_UnwrapsThroughFmtErrorf,
  TestAsError_PlainErrorBecomesUnknown — pin AsError's contract
  against nil / direct / wrapped / plain inputs.
- TestEmitError_EnvelopeShape — round-trips through bytes.Buffer and
  asserts exactly two keys ("code", "error"), correct values, and a
  trailing newline.
- TestWrap_PreservesCauseForErrorsIs — confirms errors.Is and
  errors.As walk through Wrap so future sentinel introductions work.

A comment block documents why EmitError's encode-failure fallback
isn't exercised — json.Marshal of {Code, Message} cannot fail, so the
branch is unreachable from outside the package.
2026-05-27 08:21:57 +00:00
Tink bot c715520ab9 test(veans): cover credential-store hardening invariants
Four unit tests in internal/credentials/file_test.go for behaviors that
have no e2e coverage (e2e exercises file-backend writes round-trip but
never stats the mode, never simulates a crash, never races two
processes, never observes the fallback warning):

- TestFileBackend_SetReassertsMode pre-creates the file at 0o644 and
  asserts Set narrows it to 0o600 via Chmod-after-Rename.
- TestFileBackend_SetCleansUpTmpFile scans the dir after Set and
  fails on any leftover .credentials-*.tmp.
- TestFileBackend_ConcurrentWritersSerialize runs two goroutines
  writing distinct keys; both records must survive (verifies the
  flock around load-mutate-save).
- TestChain_SetWarnsOnFallback captures ChainStderr via bytes.Buffer
  and asserts the one-line warning when a writable backend errors
  before the file backend succeeds.
2026-05-27 08:21:57 +00:00
Tink bot f04930137e test(veans): pin runUpdate's call-order invariants
The two ordering rules in commands/update.go::runUpdate aren't enforced
by anything beyond the lines being written in that sequence:

  1. MoveTaskToBucket runs AFTER UpdateTask, so a status transition
     doesn't clobber freshly attached labels.
  2. The scrapped-reason comment posts BEFORE the bucket move, so the
     audit trail reads chronologically.

Both are documented in CLAUDE.md but neither is exercised by the e2e
suite: TestUpdate_DescriptionReplaceUniqueness is the only update-side
e2e and it only covers --description-replace-old/new.

Add two unit tests that drive runUpdate against an httptest.Server and
assert the exact (method, path) sequence. Sanity-checked locally by
swapping the field-update and bucket-move blocks — both tests fail with
a clear order diff, confirming they catch the regression that's most
likely to slip through review.
2026-05-27 08:21:57 +00:00
Tink bot ba6615f378 feat(veans): warn when Chain.Set falls back past a failed backend
A keyring transient failure on Set silently falls through to the file
backend today, which leaves a stale keyring entry from any prior
successful write shadowing the new file-backend token. Fixing the
shadow itself is deferred (would need a Set-and-Delete coordination,
or a stricter contract).

What we can do cheaply: surface the fallback so an operator hitting
the shadow has a breadcrumb. On Chain.Set fallthrough past a writable
backend that errored, print:

  veans: credential store: keyring rejected write (X); falling back to file

The warning goes to stderr (not the structured envelope — Set still
returns nil because the write landed somewhere). Env-backend's
read-only skip is unchanged and silent.

ChainStderr is exposed as a package var so tests can capture/assert
the warning when we backfill credential-store coverage.
2026-05-27 08:21:57 +00:00
Tink bot 75e546f0c1 feat(veans): make the HTTP client timeout configurable via .veans.yml
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.
2026-05-27 08:21:57 +00:00
Tink bot c4a0575305 feat(veans): offer "create a new project" from init's picker
The project picker used to require at least one pre-existing project
and would otherwise hard-error: "no projects visible to this user —
create one in the Vikunja UI first". Now it always offers an extra
numbered entry "Create a new project" and, when the user picks it,
prompts for a title (required) + identifier (optional). Empty-list
case routes straight to creation.

Backed by a new client.CreateProject(ctx, *Project) method (`PUT
/projects`); the e2e harness now uses that instead of the raw c.Do
call it did before.

Also fixed a latent bufio bug in StdPrompter.ReadLine that this work
surfaced: every call created a fresh bufio.Reader, which read-ahead a
buffer and threw it away on return. Second+ prompts read empty. Reuse
one buffered reader on the StdPrompter instance.
2026-05-27 08:21:57 +00:00
Tink bot 9b8ad4d027 feat(veans): URL discovery on init, port of the frontend's heuristic
The previous init flow took whatever the user typed for --server and
called GET <url>/api/v1/info on it. If the user typed
"vikunja.example.com" (no scheme), or pasted the URL with /api/v1 in
it (double-suffix), or pointed at a localhost install on the default
:3456 port without typing the port, we'd hand back a raw HTTP error.

New `client.DiscoverServer` ports the frontend's
helpers/checkAndSetApiUrl.ts discovery: probe a small ordered set of
plausible bases for /api/v1/info, return the first one that returns
parseable Info. Candidate order:

  1. scheme://host[:port]/path           (as the user typed it)
  2. scheme://host:3456/path             (default API port)
  3. opposite scheme of (1)
  4. opposite scheme of (2)

Heuristics:
- Missing scheme → https for public hosts, http for localhost /
  127.0.0.1 / [::1] (matches most CLIs' behaviour)
- Trailing /api/v1 from a pasted URL is stripped before probing, so
  we don't double up to /api/v1/api/v1/info
- Trailing slashes normalized

Errors now list everything we tried + the last underlying network
error, so the user can see why a URL failed instead of just
"GET /info: connection refused":

  veans: VALIDATION_ERROR: couldn't find a Vikunja instance reachable
    from "vikunja.example.com" — tried:
    - https://vikunja.example.com/api/v1/info
    - https://vikunja.example.com:3456/api/v1/info
    - http://vikunja.example.com/api/v1/info
    - http://vikunja.example.com:3456/api/v1/info
    last error: dial tcp: lookup vikunja.example.com: no such host

bootstrap.Init now defers URL canonicalisation to DiscoverServer and
caches the matched info from the probe (no second /info round-trip).

Unit tests cover the candidate-builder across the common shapes:
bare hostname, localhost, /api/v1-suffixed paste, explicit port,
subpath install, 127.0.0.1:3456, trailing slash. e2e green.
2026-05-27 08:21:57 +00:00
Tink bot 814b2a635f feat(veans): install agent hooks during init instead of just printing
Adds a final step to bootstrap.Init that offers to wire `veans prime`
into Claude Code and OpenCode automatically. Per-agent yes/no prompts
default to "yes" for Claude Code and "no" for OpenCode; --install-claude
/ --install-opencode flags skip the prompt for scripted contexts;
--no-hooks falls back to the previous behaviour of just printing the
snippets.

Claude Code:
  - Writes/merges .claude/settings.json
  - JSON merge preserves existing keys (model, permissions, other hooks)
    and only appends a `veans prime` command entry under SessionStart
    and PreCompact if one isn't already there
  - Idempotent: re-running reports "Already configured" without
    duplicating entries

OpenCode:
  - Writes .opencode/plugin/veans-prime.ts with the standard handler
    skeleton
  - Existing files are left alone (no TS-merge story for v0)

Failures during hook install are non-fatal: the repo is already
configured, so the user gets a warning + the printed snippets as a
fallback path.

Unit tests cover the merge logic (fresh file, idempotent rerun,
preserving user's other hooks/keys), the install actions
("Wrote"/"Updated"/"Already configured"), and the offer flow
(flags-bypass-prompt vs prompt-when-unset vs no-hooks).
2026-05-27 08:21:57 +00:00
Tink bot 1bc3afa430 feat(veans): match existing bucket titles via case-insensitive alias table 2026-05-27 08:21:57 +00:00
Tink bot 4ac89741e3 feat(veans): reuse owned bot or prompt for fresh name on collision 2026-05-27 08:21:57 +00:00
Tink bot cd7cc113a1 docs(veans): AGENTS.md cheat sheet for coding agents
Captures the non-obvious things an agent will hit working on this
submodule:

- Wire-format quirks (view_kind/bucket_configuration_mode are JSON
  strings; Task.BucketID is always 0 in GET — use ?expand=buckets and
  CurrentBucketID; POST /tasks doesn't move buckets, use the dedicated
  bucket-tasks endpoint; bot creation is at /user/bots; APIToken
  expires_at is required, use FarFuture for "no expiry").
- Permission discovery via /routes (group names are path-derived; use
  PermissionsForBot at runtime instead of hard-coding).
- OAuth shape (PKCE/S256 mandatory, no client registration, JSON-only
  token exchange, loopback redirect via 127.0.0.1:0, Shutdown uses
  context.WithoutCancel to drain on outer cancel).
- Credential chain order + per-test HOME/XDG override.
- Identifier validation (runelength only) + base-36 timestamp suffix
  trick for unique e2e identifiers.
- mage Aliases map (without it, `mage test` rejects the namespace).
- License-header enforcement via local .golangci.yml + code-header-
  template.txt copy.
- Things to actively avoid: bare exec.Command, committing the built
  binary, stdout from `prime` outside a configured workspace.

CLAUDE.md is a symlink to AGENTS.md so Claude Code picks it up via
either name.
2026-05-27 08:21:57 +00:00
Tink bot 632579b304 ci(veans): add fast veans-test job for unit tests 2026-05-27 08:21:57 +00:00
Tink bot c1d5272afe feat(veans): switch OAuth handshake to a loopback HTTP server 2026-05-27 08:21:57 +00:00
Tink bot b18762171d feat(veans): add browser launcher helpers for OAuth flow 2026-05-27 08:21:57 +00:00
Tink bot 952ad89a8b chore(veans): apply veans golangci pass across sources 2026-05-27 08:21:57 +00:00
Tink bot 202a5f60b0 ci(veans): add veans-lint job to Test workflow 2026-05-27 08:21:57 +00:00
Tink bot 8ef796f016 chore(veans): add veans-local golangci config 2026-05-27 08:21:57 +00:00
Tink bot 35aa486eb5 feat(veans): use OAuth 2.0 Authorization Code + PKCE as default auth
Vikunja's built-in OAuth server (Vikunja 2.3+) does not require client
registration and accepts arbitrary client_ids — it just enforces PKCE
(S256) and constrains redirect URIs to the vikunja- scheme. Earlier I
deferred OAuth on the assumption it needed a registered client; that
was wrong, and the docs make the path much smoother than POST /login.

The custom-scheme constraint (no http:// loopback) is side-stepped by
manual paste-back: veans prints the authorize URL, the user signs in,
their browser fails to open vikunja-veans-cli://callback?code=... and
shows an error, the user copies the URL from the address bar and
pastes it back. CLI extracts code + state, verifies state for CSRF,
exchanges via POST /api/v1/oauth/token (JSON body — Vikunja rejects
form-encoded), and returns the access token.

Resolution order in AcquireHumanToken:
  1. --token (paste-in JWT or personal API token; SSO/OIDC users)
  2. --use-password / --username + --password (POST /login)
  3. OAuth flow (interactive default)

login command supports the same --use-password / --token escape hatches
for token rotation on instances with OAuth disabled.

Includes unit tests for the PKCE generator (verifier shape per RFC 7636,
challenge = SHA256(verifier) base64url-no-pad), authorize-URL
construction, and the lenient callback parser (full URL / query-only /
bare code).
2026-05-27 08:21:57 +00:00
Tink bot d0c77ad6fe docs(veans): add README with quick-start guide 2026-05-27 08:21:57 +00:00
Tink bot 950d41df91 ci(veans): add veans-e2e workflow 2026-05-27 08:21:57 +00:00
Tink bot 4c3d449a35 test(veans): add e2e suite covering init, tasks, claim, prime flows 2026-05-27 08:21:57 +00:00
Tink bot 3a7bcb2a50 chore(veans): gitignore built binary 2026-05-27 08:21:57 +00:00
Tink bot df7a60d137 feat(veans): add login command for token rotation 2026-05-27 08:21:57 +00:00
Tink bot 2e2393121b feat(veans): add api passthrough command 2026-05-27 08:21:57 +00:00
Tink bot e8cdfcf023 feat(veans): add prime command for agent prompt injection 2026-05-27 08:21:57 +00:00
Tink bot b9551d55ba feat(veans): add claim command for assigning and bucket transition 2026-05-27 08:21:57 +00:00
Tink bot 6ebe25bfbc feat(veans): add update command with description and status transitions 2026-05-27 08:21:57 +00:00
Tink bot 6b756d92c3 feat(veans): add create command with labels and relations 2026-05-27 08:21:57 +00:00
Tink bot 2425d9923e feat(veans): add label get-or-create helper 2026-05-27 08:21:57 +00:00
Tink bot e88427ca3c feat(veans): add show command with PROJ-NN/#NN ID resolver 2026-05-27 08:21:57 +00:00
Tink bot 5e80c17281 feat(veans): add list command with filters and JSON output 2026-05-27 08:21:57 +00:00
Tink bot 081373bb48 feat(veans): add shared command runtime and git branch helper 2026-05-27 08:21:57 +00:00
Tink bot 81f4845a6b feat(veans): wire init cobra command 2026-05-27 08:21:57 +00:00
Tink bot 37b6ff538b feat(veans): orchestrate init bootstrap from probe to config write 2026-05-27 08:21:57 +00:00
Tink bot d2c3f3244d feat(veans): discover /routes for permission-group negotiation 2026-05-27 08:21:57 +00:00
Tink bot 1f5abaa6fb feat(veans): require APIToken.ExpiresAt with FarFuture sentinel 2026-05-27 08:21:57 +00:00
Tink bot 6b48a37710 feat(veans): add canonical status to bucket-title mapping 2026-05-27 08:21:57 +00:00
Tink bot 36fb0f0ace feat(veans): add .veans.yml schema and config helpers 2026-05-27 08:21:57 +00:00
Tink bot 878233f758 feat(veans): add transient human auth flow 2026-05-27 08:21:57 +00:00