Files
BountyBoard/DECISIONS.md
T
etalon f3897907a3 feat: WeKan ticketing source
- wekan connector: board id as project key, cards assigned to the
  consultant's WeKan username (assignees with member fallback), archived
  cards skipped, client-side modifiedAt since-filtering, username/password
  login with token reuse (or pre-issued token), case-insensitive identity
- fake-WeKan unit tests: factory validation, test-connection, fetch
  filtering, key listing, token reuse
- admin customer wizard: WeKan option + credential fields
- OpenAPI enum + credential shapes, README identity mapping docs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:15:34 +02:00

153 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Decisions
Spec-silent choices, recorded as required by the build instructions.
## Phase 1
- **Go toolchain 1.26.4** (latest stable; spec requires ≥ 1.22). Installed at
`/usr/local/go`.
- **Module name `bountyboard`** — no remote repository exists; a single-word
module keeps import paths short (`bountyboard/internal/...`).
- **Package in `internal/http` is named `httpx`** to avoid clashing with
stdlib `net/http` inside its own files. The directory stays `internal/http`
per spec §14; importers alias it (`httpx "bountyboard/internal/http"`).
- **`/readyz` uses a pluggable checker registry** (`AddReadinessCheck`):
required checks (Mongo, Phase 2) gate readiness with 503; optional checks
(atomizer / work performer, Phase 11) are reported as `degraded` but stay
200, matching §6's "non-fatal, reported".
- **Runtime image is `alpine:3.21`, not distroless** — busybox `wget` enables
the compose healthcheck without adding tooling, and `ca-certificates` is
needed for outbound HTTPS to Jira/ADO/YouTrack. Non-root user.
- **Config validation is strict and fail-fast**: `CREDENTIALS_ENC_KEY` is
mandatory at startup (base64, exactly 32 bytes) and `SESSION_SECRET` must be
≥ 16 chars, even before the features using them land — a misconfigured
deployment should die at boot, not at first use. All validation errors are
reported together via `errors.Join`.
- **Trusted-proxy resolution walks X-Forwarded-For right-to-left**, skipping
addresses inside `TRUSTED_PROXY_CIDRS`; the first untrusted address is the
client. A malformed entry stops the walk (everything left of it is
attacker-controllable). If the whole chain is trusted, the leftmost entry is
used. Headers are ignored entirely when the direct peer is untrusted.
- **ULID**: canonical 48-bit ms timestamp + 80-bit crypto randomness, no
intra-millisecond monotonicity (spec only needs sortability at ms
resolution; randomness makes collisions negligible).
- **`.env` loader semantics**: real environment variables win over `.env`
values; unquoted values support trailing ` # comment`; missing `.env` file
is not an error (containers receive env via compose `env_file`).
- **Makefile `seed`/`backup`/`restore` are failing stubs until Phase 12** so
the targets exist but cannot be mistaken for working.
## Phase 2
- **Integration tests reach Mongo via `docker-compose.test.yml`**, an explicit
test-only overlay publishing Mongo on `127.0.0.1:27017`. The normal compose
file still never publishes Mongo (§12); the spec's integration-test
requirement (§2.1) needs host access, and a loopback-only opt-in overlay is
the smallest hole. Each run uses a `bountyboard_test_<ulid>` database and
drops it on cleanup.
- **`UpdateVersioned` rejects updates that touch `version`** and merges
`updatedAt`/`$inc version` into the caller's operators; it distinguishes
`ErrNotFound` from `ErrVersionConflict` with a follow-up existence check.
- **File MIME type = sniffed (`http.DetectContentType`) unless inconclusive**
(`application/octet-stream`), in which case the client-declared type is
kept. A confident sniff overrides a lying declaration.
- **sha256 of uploads is recorded post-upload** via an update on `fs.files`
metadata (GridFS metadata must be supplied before streaming; the hash is
only known after).
- **Signed file tokens** are `base64url(exp || hmac-sha256(fileID|exp))` with
a key derived as `sha256("bountyboard/file-url/v1" + SESSION_SECRET)` so
file tokens can never collide with other uses of the session secret.
## Phase 3
- **OIDC linking requires `email_verified=true`.** An unverified IdP email
matching an existing local account is rejected (`oidc_email_unverified`)
instead of linking or creating a duplicate — linking on unverified email
would allow account takeover via a rogue IdP account.
- **OIDC provider discovery is lazy** (first login attempt, retried on
failure) so an unreachable IdP cannot block app startup.
- **CSRF protection applies to authenticated mutations only**; login and
register are pre-session and protected by rate limiting + SameSite=Lax.
The CSRF cookie is intentionally JS-readable (double-submit pattern).
- **Password change revokes all other sessions** of the user (recovery
semantics) but keeps the current one. `SetPassword` is deliberately not
version-checked: a password change must never lose an optimistic-locking
race against a profile edit.
- **Forced password change (`mustChange`)** is enforced in `requireAuth` via
a path allowlist (`me`, `logout`, `logout-all`, `change-password`).
- **Sessions slide at most once per hour** (refreshedAt watermark) instead of
updating expiry on every request.
- **Login rate-limit key is `clientIP|email`** (and `register|clientIP` for
registration), using the trusted-proxy-resolved IP.
- **Mongo runs with `--wiredTigerCacheSizeGB 0.5`** in compose — the target
host has 4 GB RAM; default cache (≈50% of RAM) caused a restart under the
integration-test load.
## Phase 4
- **CSP has no `unsafe-inline` for scripts from day one** (§12), so all JS is
external ES modules (`theme.js` loads synchronously in `<head>` to avoid a
theme flash; everything else is `type=module`). Inline event handlers are
never used.
- **`PATCH /api/v1/profile` is a partial update**: only fields present in the
body change. `version` is optional — when present the update is
version-checked (409 + reload toast on conflict); the nav theme toggle
omits it so flipping themes can't conflict with a profile edit.
- **Avatar uploads must sniff as `image/*`**; the rejected/replaced GridFS
files are deleted eagerly. Replacing an avatar removes the previous file.
- **File serving (`GET /files/{id}`)**: valid signed token (?st=) OR session.
With a session: avatars are visible to any logged-in user; other scopes
(chat/task) currently allow owner/admin/consultant and will be tightened as
those features land.
- **Pages that need login redirect to `/login?next=…`** (HTML UX), while API
routes return 401 JSON. Forced password change redirects all pages to
/change-password.
- **Pages for later phases render an "under construction" placeholder** so
role-based navigation is complete and clickable now.
## Phases 512 (selected)
- **`internal/extsvc`** (not in the §14 list) holds the circuit breaker and
retrying JSON client shared by the two service clients — sharing plumbing,
not state; each service keeps its own breaker, URL, and token.
- **Mock services are separate codebases**: `services/atomizer` has its own
`go.mod` (stdlib only); `services/work-performer` is plain Node with zero
npm dependencies (plus the globally installed claude CLI in its image).
- **AI-approved tasks earn no `bountyAwards` row** — §4.5 awards are a
developer performance ledger; the AI submitter has no developer identity.
- **Bulk archive/publish are partial-success APIs** returning
`{done[], failed{}}` instead of failing the whole batch.
- **Extension siblings record `parentId = source task`** (spec literal),
so the UI tree shows extensions beneath their source.
## Phase 13 (deployment)
- **`APP_INTERNAL_URL` (compose: `http://app:8787`)** is handed to the
external services for callback URLs and signed attachment URLs — the §5.2
example uses the in-network hostname; `APP_BASE_URL` stays browser-facing.
- **The work-performer container runs as the `node` user** (uid 1000) with
`${HOME}/.claude` mounted into `/home/node/` instead of `/root/` (§9.2
shows /root): the claude CLI refuses `--dangerously-skip-permissions` as
root, so the literal spec mount can never execute jobs.
- **Run compose with the real user's HOME** — `sudo docker compose` resolves
`${HOME}` to `/root` and silently mounts the wrong Claude credentials. Use
`sudo --preserve-env=HOME docker compose …` (or run docker unprivileged).
- **UFW**: a rule allowing `172.16.0.0/12` (docker networks) to reach the
host was added so container→host callbacks work in contract tests.
- **`scripts/acceptance.sh`** automates the §13 checklist live (import →
subdivide → extend → publish → claim/decline/approve → review → award →
AI job via real Claude Code → breaker independence) and is re-run-safe.
## Post-deploy additions
- **WeKan connector**: `projectKey` is the board id; "assigned to the
consultant" means the consultant's WeKan username (from
`ticketingIdentities.wekan`) appears in a card's `assignees` (falling back
to `members` when no assignee is set). Archived cards are skipped. WeKan
has no epic/story hierarchy → all cards map to type `task`; the API offers
no server-side updated-since filter, so the connector filters on
`modifiedAt` client-side (one details request per card — fine for board
sizes WeKan handles). Auth: username+password login per sync with a 10-min
token reuse window, or a pre-issued token. Card attachments are not
imported in v1.