HarperFast / HarperFast/harper

Typed component options: declare option types so components receive coerced values, not raw env-expanded strings

Open
#1,897 0 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
JavaScript
Stars
89
Forks
10
Avg merge
2d 6h
Merged PRs (30d)
200

Description

## Problem

Components receive their config block raw (`scope.options`): YAML quoting and `${ENV_VAR}` expansion both deliver **strings** for documented booleans/numbers, and an *unset* env var leaves the literal placeholder string (`"${FLAG}"`) in place. Every component must re-implement type coercion at every consumption site — and a missed site silently flips whichever direction the gate happens to test, because junk strings are truthy.

Concrete motivating case: HarperFast/oauth#192 (review thread) — `mcp.refreshTokenRequiresOfflineAccess: ${FLAG}` with `FLAG` unset survived as the truthy string `"${FLAG}"` and activated a documented default-off gate. The oauth component now carries four bespoke coercers (`coerceConfigBoolean`, `coerceTtl`, `coerceInterval`, `resolveRateLimit`) plus a total normalizer pass to defend against this class — and that only protects one component.

## Proposal

Let a component declare types for its options (schema in its config manifest, or a schema export the loader picks up). Core's options layer (OptionsWatcher / `scope.options`) then, after env expansion:

- coerces declared booleans/numbers from documented string forms to real types;
- treats an unresolved `${VAR}` placeholder on a typed option as an explicit config error (or absent + warning) rather than passing the literal through;
- rejects undeclarable junk on typed options with a clear startup error naming the path.

Components then gate on plain truthiness with no per-site defensive typing; component-side normalizers shrink to genuinely domain-specific validation (e.g. oauth's `allowedHosts` shape rules).

## Relationship to existing issues

- #1057 — same principle for **root** config values on the CLI set path; this issue is the component-options surface.
- #671 — config-system overhaul umbrella; this could ride whatever module boundary that lands.
- #1889 — the plugin capability manifest would be a natural home for the option schema declaration.

## Acceptance sketch

- A component can declare `myFlag: boolean` and receive `true`/`false`/absent — never a string — from `scope.options`.
- `myFlag: ${UNSET_VAR}` produces a clear startup diagnostic, not a truthy literal.
- Undeclared options keep today's pass-through behavior (no forced migration).

🤖 Filed by Claude on behalf of Nathan, from the HarperFast/oauth#192 review discussion.

Contributor guide

Open the contributing guide

Research direction

Start by tracing the OptionsWatcher and scope.options path, then review the config manifest and plugin capability manifest references in the issue. Define how a component declares option types and how env expansion reports unresolved placeholders or invalid values; done means the acceptance cases work while undeclared options retain pass-through behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, nodejs
Domain
backend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.