6265ffa894
- APP_INTERNAL_URL: in-network base URL for §5.2 callbacks and signed attachment URLs handed to the external services (compose: http://app:8787) - work-performer image runs as the node user with ~/.claude mounted into /home/node — the claude CLI refuses --dangerously-skip-permissions as root - scripts/acceptance.sh: re-run-safe live verification of the §13 checklist (demo import within one poll, subdivide sum=1 + editable, extend sibling, publish/bounty math, decline/claim/approve, changes-requested loop, approval award in metrics, unassign, AI job through real Claude Code with signed idempotent callback, breaker independence between the two services) - README/DECISIONS: sudo HOME gotcha, internal URL, non-root performer Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8.1 KiB
8.1 KiB
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/httpis namedhttpxto avoid clashing with stdlibnet/httpinside its own files. The directory staysinternal/httpper spec §14; importers alias it (httpx "bountyboard/internal/http"). /readyzuses a pluggable checker registry (AddReadinessCheck): required checks (Mongo, Phase 2) gate readiness with 503; optional checks (atomizer / work performer, Phase 11) are reported asdegradedbut stay 200, matching §6's "non-fatal, reported".- Runtime image is
alpine:3.21, not distroless — busyboxwgetenables the compose healthcheck without adding tooling, andca-certificatesis needed for outbound HTTPS to Jira/ADO/YouTrack. Non-root user. - Config validation is strict and fail-fast:
CREDENTIALS_ENC_KEYis mandatory at startup (base64, exactly 32 bytes) andSESSION_SECRETmust 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 viaerrors.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).
.envloader semantics: real environment variables win over.envvalues; unquoted values support trailing# comment; missing.envfile is not an error (containers receive env via composeenv_file).- Makefile
seed/backup/restoreare 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 on127.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 abountyboard_test_<ulid>database and drops it on cleanup. UpdateVersionedrejects updates that touchversionand mergesupdatedAt/$inc versioninto the caller's operators; it distinguishesErrNotFoundfromErrVersionConflictwith 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.filesmetadata (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 assha256("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.
SetPasswordis deliberately not version-checked: a password change must never lose an optimistic-locking race against a profile edit. - Forced password change (
mustChange) is enforced inrequireAuthvia 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(andregister|clientIPfor registration), using the trusted-proxy-resolved IP. - Mongo runs with
--wiredTigerCacheSizeGB 0.5in 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-inlinefor scripts from day one (§12), so all JS is external ES modules (theme.jsloads synchronously in<head>to avoid a theme flash; everything else istype=module). Inline event handlers are never used. PATCH /api/v1/profileis a partial update: only fields present in the body change.versionis 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 5–12 (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/atomizerhas its owngo.mod(stdlib only);services/work-performeris plain Node with zero npm dependencies (plus the globally installed claude CLI in its image). - AI-approved tasks earn no
bountyAwardsrow — §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_URLstays browser-facing.- The work-performer container runs as the
nodeuser (uid 1000) with${HOME}/.claudemounted into/home/node/instead of/root/(§9.2 shows /root): the claude CLI refuses--dangerously-skip-permissionsas root, so the literal spec mount can never execute jobs. - Run compose with the real user's HOME —
sudo docker composeresolves${HOME}to/rootand silently mounts the wrong Claude credentials. Usesudo --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.shautomates 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.