[E00-S04-T02] Missing required setting gives field-specific startup error #183

Closed
opened 2026-08-27 00:08:43 +00:00 by kpcto · 8 comments
Owner

Parent story: [E00-S04] Configuration service (#61)

Intent

Ensure a missing required setting produces a field-specific startup error.

Acceptance criteria

  • missing required setting gives a field-specific startup error
  • the error names the missing field

Explicitly out of scope

  • TypeBox/Ajv schema (E00-S04-T01)
  • secret redaction (E00-S04-T03)
  • process.env access rule (E00-S04-T04)

Test plan

  • start with a missing required field and confirm the error names it

Rollback note

  • revert the validation error handling

Owning stream

platform

Risk quadrant

agent-full

> Parent story: [E00-S04] Configuration service (#61) ## Intent Ensure a missing required setting produces a field-specific startup error. ## Acceptance criteria - missing required setting gives a field-specific startup error - the error names the missing field ## Explicitly out of scope - TypeBox/Ajv schema (E00-S04-T01) - secret redaction (E00-S04-T03) - process.env access rule (E00-S04-T04) ## Test plan - start with a missing required field and confirm the error names it ## Rollback note - revert the validation error handling ### Owning stream platform ### Risk quadrant agent-full
kpcto added this to the Sprint 0 milestone 2026-08-27 00:08:43 +00:00
kpcto added the
status
ready
kind
task
labels 2026-08-27 00:08:43 +00:00
bot-dispatcher added
status
proposed
and removed
status
ready
kind
task
labels 2026-08-27 00:08:44 +00:00
Member

Auto-reverted by dispatcher: DoR lint: required section "Intent" is empty; required section "Acceptance criteria" is empty; required section "Explicitly out of scope" is empty; required section "Test plan" is empty; required section "Rollback note" is empty; acceptance criteria: no bullet assertions found

status/ready may only be applied by a human maintainer.

> Auto-reverted by dispatcher: DoR lint: required section "Intent" is empty; required section "Acceptance criteria" is empty; required section "Explicitly out of scope" is empty; required section "Test plan" is empty; required section "Rollback note" is empty; acceptance criteria: no bullet assertions found `status/ready` may only be applied by a human maintainer.
kpcto added the
kind
task
label 2026-08-27 21:25:05 +00:00
kpcto added
status
ready
and removed
status
proposed
labels 2026-08-30 02:44:16 +00:00
bot-dispatcher added
loop
1
status
in-progress
and removed
status
ready
labels 2026-08-30 02:44:25 +00:00
Member
agent: dispatcher
phase: complete
plan_summary: Dispatch #183: [E00-S04-T02] Missing required setting gives field-specific startup error (platform, security-gated)
```yaml agent: dispatcher phase: complete plan_summary: Dispatch #183: [E00-S04-T02] Missing required setting gives field-specific startup error (platform, security-gated) ```
bot-dispatcher added the needs/security-reviewneeds/security-review labels 2026-08-30 02:44:26 +00:00
Member
agent: implementer
phase: start
issue: 183
objective: "[E00-S04-T02] Missing required setting gives field-specific startup error"
plan_summary: >
  Add the field-specific startup error to packages/config (MissingRequiredSettingError +
  assertValidConfig, building on the T01 schema), wire startup validation into
  apps/server (fail fast naming the missing field), provide EPPP_SESSION_SECRET for the
  compose app service, ship the config package in the server Dockerfile, add
  tests/config-startup-error.test.mjs + a CI job, and update the affected fixtures
  (health-endpoint/app-readiness probes, docs, lockfile).
out_of_scope: T01 schema (done), T03 secret redaction, T04 process.env adapter
```yaml agent: implementer phase: start issue: 183 objective: "[E00-S04-T02] Missing required setting gives field-specific startup error" plan_summary: > Add the field-specific startup error to packages/config (MissingRequiredSettingError + assertValidConfig, building on the T01 schema), wire startup validation into apps/server (fail fast naming the missing field), provide EPPP_SESSION_SECRET for the compose app service, ship the config package in the server Dockerfile, add tests/config-startup-error.test.mjs + a CI job, and update the affected fixtures (health-endpoint/app-readiness probes, docs, lockfile). out_of_scope: T01 schema (done), T03 secret redaction, T04 process.env adapter ```
Member
agent: security
issue: 183
pr: 397
head_branch: feature/183
status: started
review_type: security-audit
started_at: 2026-08-30T03:10:00Z
scope: security review of PR #397 (feature/183) per security-audit + security-baseline + review-checklist
note: structured YAML comment — final verdict (approve | request-changes) follows in a closing comment
```yaml agent: security issue: 183 pr: 397 head_branch: feature/183 status: started review_type: security-audit started_at: 2026-08-30T03:10:00Z scope: security review of PR #397 (feature/183) per security-audit + security-baseline + review-checklist note: structured YAML comment — final verdict (approve | request-changes) follows in a closing comment ```
Member
agent: implementer
phase: complete
issue: 183
pr: 397
objective: "[E00-S04-T02] Missing required setting gives field-specific startup error"
summary: >
  PR #397 open with CI green (10/10 jobs, incl. the new config-startup-error job) and the
  criterion→test table in the description. packages/config gains assertValidConfig plus
  MissingRequiredSettingError (message + missingField name the missing setting) and
  ConfigStartupError (names each violating field), building on the T01 TypeBox/Ajv schema;
  apps/server validates the required EPPP_SESSION_SECRET before binding and fails fast with
  "missing required setting: sessionSecret"; compose provides the secret (dev-only >= 32
  char default) and the Dockerfile ships the config package. tests/config-startup-error.test.mjs
  (17 tests) locks in both acceptance criteria with static + mutation + deterministic probes,
  executing the issue's test plan ("start with a missing required field and confirm the error
  names it"): booting the committed server without the secret exits non-zero naming
  sessionSecret, a too-short secret also names the field, and a valid secret boots to
  GET /health 200. Out of scope per brief (T01 schema, T03 redaction, T04 env adapter)
  untouched.
```yaml agent: implementer phase: complete issue: 183 pr: 397 objective: "[E00-S04-T02] Missing required setting gives field-specific startup error" summary: > PR #397 open with CI green (10/10 jobs, incl. the new config-startup-error job) and the criterion→test table in the description. packages/config gains assertValidConfig plus MissingRequiredSettingError (message + missingField name the missing setting) and ConfigStartupError (names each violating field), building on the T01 TypeBox/Ajv schema; apps/server validates the required EPPP_SESSION_SECRET before binding and fails fast with "missing required setting: sessionSecret"; compose provides the secret (dev-only >= 32 char default) and the Dockerfile ships the config package. tests/config-startup-error.test.mjs (17 tests) locks in both acceptance criteria with static + mutation + deterministic probes, executing the issue's test plan ("start with a missing required field and confirm the error names it"): booting the committed server without the secret exits non-zero naming sessionSecret, a too-short secret also names the field, and a valid secret boots to GET /health 200. Out of scope per brief (T01 schema, T03 redaction, T04 env adapter) untouched. ```
Member
agent: security
phase: complete
issue: 183
pr: 397
head_branch: feature/183
head_sha: 873264004a5727af1e4f8b45dc86fab8d16185b9
verdict: approve
summary: >
  No blocking security findings. The diff adds a fail-fast startup validation
  boundary (packages/config assertValidConfig + MissingRequiredSettingError)
  wired into the server BEFORE server.listen, a compose dev-only secret
  default, Dockerfile config-package shipping, docs, and tests. No new
  endpoints, no authz decisions, no data access, no outbound requests, no
  unsafe deserialization; startup error text is value-free (verified with a
  canary secret); scanners clean; lockfile adds only a workspace link. The CI
  workflow change is strictly additive and matches the security-approved #396
  precedent.
scanners:
  gitleaks:
    command: "gitleaks detect --source . --no-git --redact  +  gitleaks detect --log-opts='ecc945c..8732640'"
    result: pass
    evidence: "no leaks found (exit 0) on the working tree AND on all 3 PR commits (0ce790f, 1a9fd59, 8732640)"
  osv-scanner:
    command: "osv-scanner --recursive ."
    result: pass
    evidence: "No issues found across 25 locked packages in pnpm-lock.yaml (exit 0); the lockfile diff is ONLY the apps/server -> @personal-blog/config workspace link (no new external dependencies)"
  semgrep:
    result: skipped
    reason: "not installed in the review image; gap covered by the manual authz/input trace and an independent behavioral probe"
  integrity_check:
    result: pass
    evidence: "independent build: committed packages/config sources byte-identical (sha256-verified copies) compiled clean with the pinned toolchain (typescript 6.0.3, ajv 8.20.0, @sinclair/typebox 0.34.52, strict/NodeNext/verbatimModuleSyntax)"
checks:
  secrets: pass
  dependencies: pass
  authz: pass
  input_validation: pass
  injection: pass
  ssrf: pass
  unsafe_deserialization: pass
  error_leakage: pass
  ci_workflow: pass
authz_trace:
  new_endpoints: "none — routes unchanged (GET /health 200/503, 404 catch-all); the only new call path is assertValidConfig at module load, before server.listen (source-verified and mutation-probed by the suite)"
  privilege_path: "no authz checks hand-rolled, no default-allow path introduced; validation is a pure function over the startup config object; a failure throws at startup (process exits non-zero) — it never boots in a degraded/allow state"
  bypass_surface: "none — no internal API, queue or admin path added; the compose/Dockerfile changes only deliver the secret the startup check now requires"
input_boundaries:
  validation: "independent behavioral probe against the compiled boundary (22 checks): valid configs pass incl. the no-database path; missing sessionSecret throws MissingRequiredSettingError with missingField === 'sessionSecret' and message 'missing required setting: sessionSecret'; short secret / unknown property / empty databaseUrl / port 65536 / port 0 / port '3000' / null / array all rejected with field-naming ConfigStartupError"
  error_leakage: "canary-secret probe: error text (message + violations) NEVER echoes the rejected value — Ajv messages carry constraint text only (e.g. 'sessionSecret: must NOT have fewer than 32 characters'); T03 redaction remains correctly out of scope"
  ajv_codegen: "clean — ajv.compile() receives only the committed compile-time configSchema; no env- or user-influenced schema reaches codegen"
  injection: "clean — no SQL/command/template construction; test spawns use fixed argv with no shell; no outbound request URL is user-influenced (no SSRF); no untrusted deserialization"
  secrets_handling: "no secrets in code/tests/docs; test secrets are dummies ('s'.repeat(32)); compose EPPP_SESSION_SECRET dev-only default (46 chars, documented, .env/shell override) mirrors the pre-existing POSTGRES_PASSWORD:-eppp pattern; Dockerfile keeps the no-ARG/ENV, runtime-injection posture (.dockerignore unchanged; secrets-not-embedded suite still gates)"
  ci_workflow: "tripwire evaluated explicitly: new config-startup-error job is strictly additive (frozen-lockfile install, builds config + database-postgres, runs the suite); zero secrets references and zero ${{ }} interpolation in the entire ci.yml; actions pinned to the same majors as all pre-existing jobs; the app-readiness job change only extends its build step — no job removed, weakened or re-gated. Matches the security-approved #396 (E00-S04-T01) precedent; final merge remains the human decision"
test_honesty: "no existing test deleted, skipped or weakened; fixture boot-env updates (health-endpoint, app-readiness) are required by the new startup requirement; mutation probes prove the static assertions non-vacuous; deterministic probes execute the issue's test plan (boot without EPPP_SESSION_SECRET -> non-zero naming sessionSecret; valid secret -> GET /health 200)"
findings:
  - id: F1
    severity: info
    location: "packages/config/src/startup.ts:115-124"
    what: >-
      required-keyword errors short-circuit: a config missing sessionSecret AND
      carrying other violations reports only the missing field on that boot;
      the other violations surface on the next boot.
    exploit_path: "none — fail-fast UX tradeoff, not a security issue"
    fix: "Optional: append remaining violations to the MissingRequiredSettingError message (or log them) when T03/T04 build on this boundary."
  - id: F2
    severity: info
    location: "compose.yaml:104"
    what: >-
      the committed dev-only default session secret is publicly known; combined
      with the pre-existing "${APP_PORT:-3000}:3000" all-interfaces publish, a
      dev stack left exposed to a network runs with a known secret.
    exploit_path: "requires deliberately publishing the local dev stack; accepted pattern (POSTGRES_PASSWORD:-eppp), documented dev-only with .env/shell override; no session-auth code exists yet (Fastify shell lands later)"
    fix: "None required now; when T05 lands, .env.example should stress the override, and a later task may warn loudly when the app boots with the default."
  - id: F3
    severity: info
    location: "apps/server/src/index.ts:49"
    what: "host is hardcoded '0.0.0.0' in the validated startup config (binds all interfaces)."
    exploit_path: "none — pre-existing binding posture, not introduced by this diff"
    fix: "T04's environment adapter owns HOST; no action in this PR."
notes:
  - >-
    Criterion → test mapping verified: both acceptance criteria and the issue
    test plan map to real tests in tests/config-startup-error.test.mjs (static
    assertions + 8 mutation probes + deterministic boundary and server-boot
    probes), gated by the new additive CI job. Scope check: all 15 changed
    files are inside the task brief; T01/T03/T04 stay untouched (the new
    process.env.EPPP_SESSION_SECRET read is the feature itself, per the
    issue's out-of-scope list).
  - >-
    Review performed at PR head 8732640 against base ecc945c; working tree
    verified unmodified after the review; scanners executed in the review
    sandbox with outputs quoted above (no artifact store available).
completed_at: 2026-08-30T03:15:00Z

Security review of PR #397 (feature/183) for issue #183 — verdict: approve. The startup boundary is sound: validation runs before the server binds and fails fast naming the missing field, the error text never leaks secret values (canary-verified against the compiled package built from byte-identical sources with the pinned toolchain), and there is no new authz, injection, SSRF or deserialization surface. Secrets, dependencies and lockfile integrity are clean, and the CI change is a strictly additive job matching the reviewed #396 precedent — the three findings are informational notes for the follow-up tasks (T03/T04/T05), none blocking.

```yaml agent: security phase: complete issue: 183 pr: 397 head_branch: feature/183 head_sha: 873264004a5727af1e4f8b45dc86fab8d16185b9 verdict: approve summary: > No blocking security findings. The diff adds a fail-fast startup validation boundary (packages/config assertValidConfig + MissingRequiredSettingError) wired into the server BEFORE server.listen, a compose dev-only secret default, Dockerfile config-package shipping, docs, and tests. No new endpoints, no authz decisions, no data access, no outbound requests, no unsafe deserialization; startup error text is value-free (verified with a canary secret); scanners clean; lockfile adds only a workspace link. The CI workflow change is strictly additive and matches the security-approved #396 precedent. scanners: gitleaks: command: "gitleaks detect --source . --no-git --redact + gitleaks detect --log-opts='ecc945c..8732640'" result: pass evidence: "no leaks found (exit 0) on the working tree AND on all 3 PR commits (0ce790f, 1a9fd59, 8732640)" osv-scanner: command: "osv-scanner --recursive ." result: pass evidence: "No issues found across 25 locked packages in pnpm-lock.yaml (exit 0); the lockfile diff is ONLY the apps/server -> @personal-blog/config workspace link (no new external dependencies)" semgrep: result: skipped reason: "not installed in the review image; gap covered by the manual authz/input trace and an independent behavioral probe" integrity_check: result: pass evidence: "independent build: committed packages/config sources byte-identical (sha256-verified copies) compiled clean with the pinned toolchain (typescript 6.0.3, ajv 8.20.0, @sinclair/typebox 0.34.52, strict/NodeNext/verbatimModuleSyntax)" checks: secrets: pass dependencies: pass authz: pass input_validation: pass injection: pass ssrf: pass unsafe_deserialization: pass error_leakage: pass ci_workflow: pass authz_trace: new_endpoints: "none — routes unchanged (GET /health 200/503, 404 catch-all); the only new call path is assertValidConfig at module load, before server.listen (source-verified and mutation-probed by the suite)" privilege_path: "no authz checks hand-rolled, no default-allow path introduced; validation is a pure function over the startup config object; a failure throws at startup (process exits non-zero) — it never boots in a degraded/allow state" bypass_surface: "none — no internal API, queue or admin path added; the compose/Dockerfile changes only deliver the secret the startup check now requires" input_boundaries: validation: "independent behavioral probe against the compiled boundary (22 checks): valid configs pass incl. the no-database path; missing sessionSecret throws MissingRequiredSettingError with missingField === 'sessionSecret' and message 'missing required setting: sessionSecret'; short secret / unknown property / empty databaseUrl / port 65536 / port 0 / port '3000' / null / array all rejected with field-naming ConfigStartupError" error_leakage: "canary-secret probe: error text (message + violations) NEVER echoes the rejected value — Ajv messages carry constraint text only (e.g. 'sessionSecret: must NOT have fewer than 32 characters'); T03 redaction remains correctly out of scope" ajv_codegen: "clean — ajv.compile() receives only the committed compile-time configSchema; no env- or user-influenced schema reaches codegen" injection: "clean — no SQL/command/template construction; test spawns use fixed argv with no shell; no outbound request URL is user-influenced (no SSRF); no untrusted deserialization" secrets_handling: "no secrets in code/tests/docs; test secrets are dummies ('s'.repeat(32)); compose EPPP_SESSION_SECRET dev-only default (46 chars, documented, .env/shell override) mirrors the pre-existing POSTGRES_PASSWORD:-eppp pattern; Dockerfile keeps the no-ARG/ENV, runtime-injection posture (.dockerignore unchanged; secrets-not-embedded suite still gates)" ci_workflow: "tripwire evaluated explicitly: new config-startup-error job is strictly additive (frozen-lockfile install, builds config + database-postgres, runs the suite); zero secrets references and zero ${{ }} interpolation in the entire ci.yml; actions pinned to the same majors as all pre-existing jobs; the app-readiness job change only extends its build step — no job removed, weakened or re-gated. Matches the security-approved #396 (E00-S04-T01) precedent; final merge remains the human decision" test_honesty: "no existing test deleted, skipped or weakened; fixture boot-env updates (health-endpoint, app-readiness) are required by the new startup requirement; mutation probes prove the static assertions non-vacuous; deterministic probes execute the issue's test plan (boot without EPPP_SESSION_SECRET -> non-zero naming sessionSecret; valid secret -> GET /health 200)" findings: - id: F1 severity: info location: "packages/config/src/startup.ts:115-124" what: >- required-keyword errors short-circuit: a config missing sessionSecret AND carrying other violations reports only the missing field on that boot; the other violations surface on the next boot. exploit_path: "none — fail-fast UX tradeoff, not a security issue" fix: "Optional: append remaining violations to the MissingRequiredSettingError message (or log them) when T03/T04 build on this boundary." - id: F2 severity: info location: "compose.yaml:104" what: >- the committed dev-only default session secret is publicly known; combined with the pre-existing "${APP_PORT:-3000}:3000" all-interfaces publish, a dev stack left exposed to a network runs with a known secret. exploit_path: "requires deliberately publishing the local dev stack; accepted pattern (POSTGRES_PASSWORD:-eppp), documented dev-only with .env/shell override; no session-auth code exists yet (Fastify shell lands later)" fix: "None required now; when T05 lands, .env.example should stress the override, and a later task may warn loudly when the app boots with the default." - id: F3 severity: info location: "apps/server/src/index.ts:49" what: "host is hardcoded '0.0.0.0' in the validated startup config (binds all interfaces)." exploit_path: "none — pre-existing binding posture, not introduced by this diff" fix: "T04's environment adapter owns HOST; no action in this PR." notes: - >- Criterion → test mapping verified: both acceptance criteria and the issue test plan map to real tests in tests/config-startup-error.test.mjs (static assertions + 8 mutation probes + deterministic boundary and server-boot probes), gated by the new additive CI job. Scope check: all 15 changed files are inside the task brief; T01/T03/T04 stay untouched (the new process.env.EPPP_SESSION_SECRET read is the feature itself, per the issue's out-of-scope list). - >- Review performed at PR head 8732640 against base ecc945c; working tree verified unmodified after the review; scanners executed in the review sandbox with outputs quoted above (no artifact store available). completed_at: 2026-08-30T03:15:00Z ``` **Security review of PR #397 (`feature/183`) for issue #183 — verdict: approve.** The startup boundary is sound: validation runs before the server binds and fails fast naming the missing field, the error text never leaks secret values (canary-verified against the compiled package built from byte-identical sources with the pinned toolchain), and there is no new authz, injection, SSRF or deserialization surface. Secrets, dependencies and lockfile integrity are clean, and the CI change is a strictly additive job matching the reviewed #396 precedent — the three findings are informational notes for the follow-up tasks (T03/T04/T05), none blocking.
bot-dispatcher removed the needs/security-reviewneeds/security-review labels 2026-08-30 03:11:43 +00:00
Member

Security approved; dispatching reviewer.

> Security approved; dispatching reviewer.
Author
Owner
agent: reviewer
verdict: approve
reviewed:
  pr: 397
  issue: 183
  head_branch: feature/183
  head_sha: 873264004a5727af1e4f8b45dc86fab8d16185b9
summary: |
  Human-maintainer review (the reviewer worker exited without posting a verdict,
  so the human decision on this stalled card is to approve directly). Reviewed
  the full PR #397 diff against the acceptance criteria and EPPP standards:

  - packages/config/src/startup.ts: assertValidConfig throws
    MissingRequiredSettingError (message + missingField name the missing
    setting) for the required-keyword case, and ConfigStartupError (field-naming
    violations) otherwise; built on the T01 Ajv-compiled schema.
  - apps/server/src/index.ts: calls assertValidConfig with the startup config
    before server.listen, so a missing EPPP_SESSION_SECRET fails fast naming
    sessionSecret.
  - compose.yaml: app service provides EPPP_SESSION_SECRET with a >= 32-char
    dev-only default (override via .env/shell), matching the dev-only
    POSTGRES_* pattern.
  - Dockerfile: ships the compiled config package (build source + runtime dist)
    so the server import resolves in the image.
  - CI green (Actions run #98: 10/10 jobs incl. config-startup-error); security
    approved (3 info findings, none blocking); tests execute the issue's test
    plan (boot without secret -> non-zero naming sessionSecret; valid secret ->
    GET /health 200).
findings: []
```yaml agent: reviewer verdict: approve reviewed: pr: 397 issue: 183 head_branch: feature/183 head_sha: 873264004a5727af1e4f8b45dc86fab8d16185b9 summary: | Human-maintainer review (the reviewer worker exited without posting a verdict, so the human decision on this stalled card is to approve directly). Reviewed the full PR #397 diff against the acceptance criteria and EPPP standards: - packages/config/src/startup.ts: assertValidConfig throws MissingRequiredSettingError (message + missingField name the missing setting) for the required-keyword case, and ConfigStartupError (field-naming violations) otherwise; built on the T01 Ajv-compiled schema. - apps/server/src/index.ts: calls assertValidConfig with the startup config before server.listen, so a missing EPPP_SESSION_SECRET fails fast naming sessionSecret. - compose.yaml: app service provides EPPP_SESSION_SECRET with a >= 32-char dev-only default (override via .env/shell), matching the dev-only POSTGRES_* pattern. - Dockerfile: ships the compiled config package (build source + runtime dist) so the server import resolves in the image. - CI green (Actions run #98: 10/10 jobs incl. config-startup-error); security approved (3 info findings, none blocking); tests execute the issue's test plan (boot without secret -> non-zero naming sessionSecret; valid secret -> GET /health 200). findings: [] ```
bot-dispatcher added
status
review
and removed
status
in-progress
loop
1
kind
task
labels 2026-08-30 03:31:55 +00:00
kpcto added
kind
task
status
done
and removed
status
review
labels 2026-08-30 03:32:29 +00:00
kpcto closed this issue 2026-08-30 03:32:34 +00:00
Sign in to join this conversation.