Skip to content

[Feature]: Web onboarding parity with zeroclaw onboard via gateway CRUD endpoints #6175

@singlerider

Description

@singlerider

Summary

Bring the zeroclaw onboard experience to the web dashboard so a fresh-install user can complete provider auth, model selection, channels, memory, and the rest entirely from the browser. Add a thin gateway HTTP CRUD surface (per-property GET/PUT/DELETE/PATCH, OPTIONS for schema discovery, list/init/migrate) that the dashboard's onboarding flow walks alongside a schema-driven form renderer. The CLI zeroclaw config subcommands gain matching parity (patch, structured errors, --comment) so CLI / HTTP / dashboard are all thin clients of the same Config mutation core. Plain REST. No WebSockets. No long-lived shared mutable state.

Problem statement

zeroclaw onboard shipped as a schema-driven, idempotent flow in #5960 with three UI backends. The wizard owns six sections (Workspace, Providers, Channels, Memory, Hardware, Tunnel), atomic per-field persistence via Config::set_prop + cfg.save(), and per-section completion markers in cfg.onboard_state.completed_sections. Re-running the wizard skips already-completed sections; --force / --reinit flags cover the recovery cases.

The web dashboard has none of this. The current config UI (web/src/pages/config/useConfigState.ts) is whole-file getConfig / putConfig against the gateway's GET/PUT /api/config endpoints. There is no /api/onboard* route, no per-property CRUD endpoint, no notion of section completion in the web client. New users hitting the dashboard on a fresh install have no in-browser path to a working agent.

This is the first un-done item under "First-Run Onboarding (Web)" in the v0.7.5-web platform tracker (#6151). This Issue decomposes the work, conforms to RFC #5890 Section 6's CRUD-parity mandate, and reuses the wizard pattern from #5960.

Proposed solution

1. Per-property CRUD endpoints (zeroclaw config set parity over HTTP)

  • GET /api/config/prop?path=providers.fallback → returns the user's current value via Config::get_prop for non-secret fields. For secret fields, returns only {populated: true|false} — never the value, never the length, never a masked stand-in.
  • PUT /api/config/prop body {path, value, comment?} → sets via Config::set_prop. Returns the updated value for non-secrets; {populated: true} only for secrets.
  • DELETE /api/config/prop?path=... → unsets the path. For secrets, returns {populated: false}.
  • GET /api/config/list?prefix=providers → mirrors zeroclaw config list. Secret entries appear as {path, populated: true|false} only — no values, no lengths, no --secrets HTTP equivalent.
  • POST /api/config/init?section=providers → mirrors zeroclaw config init.
  • POST /api/config/migrate → mirrors zeroclaw config migrate.
  • OPTIONS /api/config → whole-config JSON Schema in the body (schemars-generated), Allow: GET, PUT, PATCH header. What's POSSIBLE/allowed for this resource — type definitions, constraints, secret markers, defaults, allowed enum values. Static per build; build-time ETag for If-None-Match / 304. Does NOT include user values.
  • OPTIONS /api/config/prop?path=... → JSON Schema fragment for that path (type, constraints, #[secret] marker, default, doc-comment-as-description), Allow: GET, PUT, DELETE header. Per-field capability discovery.
  • GET endpoints return user state. OPTIONS endpoints return capabilities. A form first issues OPTIONS to learn what types/constraints exist, then GETs to populate current values, then PUT/PATCHes to write.
  • CORS preflight: requests carrying Access-Control-Request-Method get the standard preflight response only; all other OPTIONS requests return the schema body.
  • PATCH /api/config body: JSON Patch (RFC 6902) operations — single endpoint shape covers per-element array CRUD AND cross-field atomic batches.

2. Web dashboard onboarding UI — schema-driven forms over plain CRUD

The dashboard reads the schema (OPTIONS /api/config), reads which sections are complete (GET /api/config/prop?path=onboard_state.completed_sections), renders a form for the next incomplete section, and PATCHes the answers. Marks the section complete and moves on. One round-trip per save. No streaming, no shared mutable state across sessions.

  • New route under web/src/pages/onboard/.
  • On load: cache the schema response (TanStack Query / equivalent), fetch completion markers.
  • Determine the next incomplete section from Section::from_path() server-side; each /api/config/list entry carries an onboard_section field. Section names come from the gateway response (which derives them from the existing Section enum in crates/zeroclaw-runtime/src/onboard/mod.rs), not a hardcoded list in TS. No per-field #[onboard_section] attribute is needed — the section is implicit in the path's first segment, mapped once by Section::from_path.
  • For that section, render a form whose fields come from the schema entries tagged with that section. Field type drives input rendering: bool → toggle, enum → dropdown, Vec<String> → list editor, Option<String> with #[secret] → masked input with a "populated?" indicator from GET /api/config/prop?path=....
  • User clicks Save → one PATCH /api/config with the section's answers. Server validates and saves atomically. On success, mark the section complete. On structured-error response, render errors next to the offending fields.
  • Move to the next section, or finish.
  • Add/remove UX for pattern-keyed sections ([providers.models.<name>], [mcp.servers.<name>]).

Adding a new config field that's tagged with a section makes it appear in the form automatically. No frontend changes required.

The dashboard's existing "advanced" config editor (web/src/pages/config/) follows the same model on a smaller scale: OPTIONS-driven form rendering, PATCH per save.

3. Auth and pairing

/api/config/prop* and the other new endpoints reuse the existing pairing/bearer auth. The PairingGuard struct lives at crates/zeroclaw-config/src/pairing.rs; the gateway enforces it on every /api/* route. First-run pairing UX is tracked in #6151 and #5266.

4. zeroclaw config CLI parity — same core, same validator, same errors

The CLI is already a peer of the HTTP API in scope (zeroclaw config get/set/list/init/migrate exist today). With the JSON Patch + validator + structured-errors infrastructure landing in this Issue, the CLI grows to match — both CLI and HTTP are thin frontends over the same Config mutation core.

  • New: zeroclaw config patch <file-or-stdin> — apply a JSON Patch document. Prints the same structured response shape the HTTP PATCH returns, formatted for the terminal. Exits non-zero on any operation failure with the structured error code in stderr.
  • Updated: zeroclaw config set/get/init/migrate — error reporting moves from ad-hoc anyhow strings to the same structured error codes (config_changed_externally, dangling_reference, validation failures, reload failures, etc.) the HTTP endpoints emit. Codes show in --json output for scripts; rendered as friendly text by default.
  • New: zeroclaw config docs — when the daemon is running, prints the explorer URL and opens it via the OS handler if a TTY is attached. When the daemon isn't running, prints the static path to web/dist/openapi.json and a zeroclaw daemon start hint.
  • Updated: zeroclaw config schema — keeps emitting JSON Schema for backward compat. --format=openapi opts into the OpenAPI 3.x JSON. --path <prop> returns the JSON Schema fragment for that path (same payload OPTIONS /api/config/prop?path=... returns over HTTP).
  • New: --comment "..." flag on set and per-op on patch.
  • No CLI rewrite of direct file writes. zeroclaw config set still writes config.toml directly when the daemon isn't running. When the daemon is running, the validator step on direct write returns the same config_changed_externally conflict error the HTTP path returns. Full HTTP-routing of CLI writes is RFC: Multi-agent UX flow — design #5890 territory.

This is the user-facing shape of the "schema as the only source of truth" goal: every interface (CLI, HTTP, dashboard) is a thin client over the same core. Adding a config field affects exactly one place — the schema — and propagates to every surface automatically.

5. API documentation — schema as the only source of truth

The HTTP surface ships with a generated, browsable API explorer at /api/docs. The doc is not hand-maintained — it is generated from the same schemars annotations that the runtime endpoint dispatch uses. Drift between docs and behavior should be impossible by construction.

  • Reuses existing schemars infra. schemars = "1.2" is already in crates/zeroclaw-config/Cargo.toml:25, gated by feature schema-export which is in default = ["schema-export"]. schemars IS in the default release binary today. Zero new Rust runtime dependencies.
  • cargo xtask gen-openapi — separate xtask binary walks the gateway's handler list, pulls schemars-generated JSON schemas for each request/response type, emits web/dist/openapi.json and the same content at crates/zeroclaw-gateway/openapi.json (committed). CI re-runs xtask and fails if the committed file is stale.
  • OpenAPI 3.1 — aligns with JSON Schema 2020-12 that schemars 1.x emits natively.
  • Explorer UI: Scalar. Single <script> tag, NPM only. Mounted as a static page at /api/docs. Scalar's Authentication panel prompts for the bearer token before any "Try it out" call.
  • TypeScript client generation: openapi-typescript (NPM). web/src/lib/api-generated.ts produced from openapi.json during npm run build. The hand-maintained web/src/lib/api.ts becomes a thin convenience layer or is deleted entirely. Adding a new endpoint regenerates the client; tsc fails the build until the dashboard consumes the new shape.

Net: zero Rust runtime dep additions. NPM dep additions are fine per the binary-size constraint.

Required Configurable derive enhancements

The macro at crates/zeroclaw-macros/src/lib.rs already produces set_prop, get_prop, the #[secret] marker, and nested-section traversal. New for this Issue:

  • #[derived_from_secret] attribute — reserved (no fields use it today); the rule is documented for future schema additions where a field's value is computed from a secret. Subject to the same write-only / no-readback rules as #[secret].
  • No #[onboard_section] attribute. Section binding is derived from the path's first segment via Section::from_path() and Section::as_path_prefix() helpers on the existing Section enum at crates/zeroclaw-runtime/src/onboard/mod.rs. Single source of truth shared by CLI wizard and HTTP CRUD; no per-field markup needed.
  • The existing Config::prop_fields() is the metadata source. Returns PropFieldInfo with name, category, display value, type hint, kind, secret marker, derived-from-secret marker, enum variants, doc-comment-as-description. Endpoint dispatchers and the OpenAPI generator both consume this. No separate list_props_with_metadata is added.
  • x-zeroclaw-* schemars extension fields — future, opt-in via additional schemars annotations as specific UI hints prove necessary. Not in scope for the initial implementation; the JSON Schema's standard fields (type, enum, description) cover form rendering for the common case. Specific extensions land when a real form-rendering need can't be expressed in standard JSON Schema.
  • derive(JsonSchema) extends to gateway request/response types — new types in zeroclaw-gateway (ConfigPatchRequest, ConfigPropResponse, error envelope, drift summary, JSON Patch operation envelope) need their own derive(JsonSchema) for the OpenAPI generator to pick them up.

Typed value handling — Vec<String> and Option<Vec<String>>

Several config fields are Vec<String> (e.g. channels.matrix.allowed_users, reliability.fallback_providers) or Option<Vec<String>> (most channel allowlists where None means "no restriction" and Some(vec![]) means "deny all"). HTTP must accept first-class JSON arrays so type semantics survive cleanly.

  • Request: value is serde_json::Value, not String. Handler dispatches on the field's declared type (from schema metadata).
    • Vec<String>: accept JSON ["a", "b"]. Reject non-array. Reject array with non-strings. [] is a valid value.
    • Option<Vec<String>>: accept ["a","b"] for Some(vec!["a","b"]), [] for Some(vec![]), null (or omitted) for None. Three states preserved.
    • String-shaped CLI input ("a,b,c") is a CLI affordance; HTTP layer does not accept it.
  • Response: Vec<String>{path, value: ["a","b"]}. Option<Vec<String>> → explicit null for None, [] for empty, ["a"] for populated. Path not in schema → 404.
  • Delete: DELETE on Vec<String> resets to default. DELETE on Option<Vec<String>> sets to None. PUT nullDELETE on Option<Vec<String>>.

PATCH operations — JSON Patch (RFC 6902)

PATCH /api/config accepts a JSON Patch document: an array of operations executed in order against the current config. Atomic across the array.

Op Notes
add For arrays: path: "/.../allowed_users/-" appends; path: "/.../allowed_users/0" prepends; path: "/.../allowed_users/2" inserts at index. For previously-None Option<T> fields: equivalent to replace setting Some(value).
remove For arrays: path: "/.../allowed_users/2" removes by index. For Option<T> fields: sets to None.
replace Per-field replace. For Option<T> fields: value: nullNone; value: []Some(vec![]); value: ["a"]Some(vec!["a"]).
move Disabled in this PR. Renaming aliased instances requires a reference graph that doesn't exist yet. Returns 400 with op_not_supported. Reference graph + safe rename is #5890 territory.
copy Disabled in this PR. Same reason as move. Returns 400 with op_not_supported.
test Optimistic concurrency. Banned against secret paths (would leak secret values via differential timing). Server returns 400 if test targets a secret path or a #[derived_from_secret] path.

Path syntax: JSON Pointer (RFC 6901). The pointer /providers/models/openrouter/api_key corresponds to the dotted CLI path providers.models.openrouter.api_key. Both forms accepted.

Batch semantics:

  • Operations execute in order against an in-memory copy.
  • After all ops apply, Config::validate() runs once.
  • If validation passes, comment-preserving atomic save runs.
  • If validation fails, on-disk and in-memory config are untouched; response is 400 with the validation error and the index of the failing op.
  • Single PATCH is the atomic unit. Concurrent PATCHes serialize via the existing config write mutex.

Optional comment per operation:

Each JSON Patch op accepts an optional comment field. When provided, the comment is written alongside the value in the TOML via the comment-preserving save infrastructure. Existing comments survive value-only edits; explicit {"comment": ""} clears.

{"op": "replace", "path": "/providers/fallback", "value": "openrouter", "comment": "switched from anthropic for cost"}

Produces TOML:

# switched from anthropic for cost
fallback = "openrouter"

Dashboard form fields can surface an optional "why?" text input. CLI exposes --comment "..." on set and per-op on patch.

Validator-gated daemon reload — atomic write flow

Every successful PATCH/PUT/DELETE follows the same flow:

  1. Take an in-memory snapshot of the current config.
  2. Apply the operation(s) to a working copy.
  3. Run Config::validate() on the working copy. Failure → return structured error; on-disk and in-memory state untouched.
  4. Comment-preserving atomic save to disk.
  5. Trigger daemon reload. Daemon re-reads from disk, re-instantiates affected components.
  6. Reload succeeds → 200 with operation results.
  7. Reload fails → revert disk to the snapshot, return 500 with the reload error so on-disk and running state stay consistent.

Dashboard renders "Applying…" while reload runs. CLI prints the same status to the terminal. Reload mechanism is implementation-defined (in-process re-instantiation where possible, process respawn for changes that require it). The contract is observable: after a successful write response, the next request observes the new config.

PATCH response shape:

{
  "saved": true,
  "results": [...]
}

No live_reloaded / requires_restart arrays. The write either succeeds (and the daemon reflects it on the next request) or it fails (and on-disk + running state are unchanged).

Validator step + structured errors

Existing Config::validate() at crates/zeroclaw-config/src/schema.rs:10151 is rich — covers tunnel/gateway/autonomy/security/scheduler/routes/provider profiles + the providers.fallback dangling-reference check (line 10349). Substantive validator, not shape-only.

This Issue wraps every anyhow::bail!(...) site in a structured error type with stable codes (provider_fallback_dangling, gateway_host_empty, tunnel_openvpn_missing, temperature_out_of_range, etc.) so HTTP/CLI consumers can match programmatically. Friendly text becomes the error's message field.

Error categories surfaced to API consumers:

  • Schema validation failures (existing Config::validate() checks)
  • config_changed_externally — server compares its in-memory Config to file-on-disk on read boundaries; non-secrets compared directly, secrets compared via SHA-256 hashes server-side (hashes never leave the server). Drift surfaces in GET responses as [{path, in_memory_value, on_disk_value}] (secrets get {path, drifted: true, secret: true}). Apply = daemon restart.
  • dangling_reference — e.g. providers.fallback set to a key not in providers.models.
  • reload_failed — daemon reload failed; on-disk reverted to snapshot.

Each error has a stable code the frontend matches against, plus a human-readable message and the offending path. Errors render in the dashboard contextually next to the offending field and clear on the next successful operation.

Secrets handling — non-negotiable boundary

The frontend must never be able to retrieve a secret's value by any means, including length, masked stand-in, encrypted form, or hash. The only information the frontend can know about a secret is whether it is populated or not populated.

Server-side rules:

  • Identify secret fields via the existing #[secret] schema marker (and the new #[derived_from_secret] for fields computed from secrets). No per-endpoint allowlist that could drift.
  • GET /api/config/prop on a secret path: response is {path, populated: <bool>}. No value, no length, no masked string, no null ambiguity — return the explicit {populated: false} shape consistently.
  • PUT /api/config/prop on a secret path: write-only. Response confirms {populated: true} only.
  • PATCH /api/config: any secret path in the batch is write-only with the same response shape.
  • DELETE /api/config/prop on a secret path: returns {path, populated: false}. Symmetric.
  • GET /api/config/list: every secret entry is {path, populated} only.
  • GET /api/config (existing whole-file endpoint): continue to mask via mask_sensitive_fields(). Audit the masking output; if it currently includes a length-preserving placeholder, shorten to a fixed-width sentinel.
  • Validation/error messages: scrub secret values via existing scrub_credentials().
  • Logs: server-side logs of any HTTP request involving a secret path must redact the body before recording. Add a regression test against the existing tracing filter.
  • JSON Patch test op against a secret path: 400 rejected (would leak via differential outcomes).
  • No timing-attack mitigation required — secrets stored locally on the same machine as the gateway. Documented as a non-goal so future reviewers don't add length-padding that obscures legitimate audit signals.

TLS posture (unchanged): This Issue does not change the gateway's TLS posture. Localhost binding default; over-the-network onboarding requires TLS termination at the gateway or in front of it. Pairing/bearer auth model is the same as today. CRUD endpoints are not safe to expose unauthenticated over the network regardless of TLS.

Acceptance criteria — implementation checklist

Tick each box as the work lands. Grouped by surface so a sequenced PR can land them in dependency order. Every box must be checked before merge.

Status (live snapshot): branch feat/6175-web-onboarding-crud is feature-complete for the substrate this Issue defines. Shipped: the macro/schema groundwork, the structured ConfigApiError type plus a best-effort classifier (no per-bail!-site refactor required), every HTTP CRUD endpoint, the JSON Patch surface (atomic, snapshot-reverting on save failure), runtime-generated OpenAPI 3.1 spec at /api/openapi.json, Scalar explorer mounted at /api/docs with offline-fallback, the GET /api/config masking audit (already fixed-width sentinel), drift detection (GET /api/config/drift + drifted summary in list), comment-preserving PATCH/PUT (write-time TOML decoration via toml_edit key decor), typed-value validation at the API boundary (Vec rejects non-array/non-string), the schema-driven web onboarding route at /onboard, the new TS API helpers, zeroclaw config patch / docs / schema --path / set --comment / --json on get/set/init/migrate, the docs page, CHANGELOG-next entry, drift / comment / secret-shape / classifier unit tests, and a banner in the existing config editor pointing at the new flow. Deferred: true daemon-process reload (re-instantiating channels/providers/MCP) per #5890 substrate; a Playwright e2e harness (NPM browsers infra needed); openapi-typescript codegen wiring (the OpenAPI spec is live but TS types still hand-maintained — web/src/lib/api.ts carries the new shapes by hand).

Macro / schema groundwork:

  • Configurable derive accepts new #[derived_from_secret] attribute (reserved; no fields use it today, rule documented). PropFieldInfo and make_prop_field carry the marker through.
  • Section::from_path() and Section::as_path_prefix() helpers on the existing Section enum so HTTP / CLI / dashboard share one section-mapping source. No #[onboard_section] macro attribute — the section is derived from the path's first segment.
  • schemars derive(JsonSchema) extends to new gateway request/response types — ConfigApiCode, ConfigApiError, PropQuery, ListQuery, PropPutBody, PatchOp, PatchOpResult, PatchResponse, PropResponse, SecretResponse, ListEntry, ListResponse, InitQuery, InitResponse, MigrateResponse, DriftEntry, DriftResponse all carry the derive under the schema-export feature.

Validator / structured errors:

  • ConfigApiError type lives in crates/zeroclaw-config/src/api_error.rs with stable codes: path_not_found, validation_failed, provider_fallback_dangling, value_type_mismatch, op_not_supported, secret_test_forbidden, config_changed_externally, reload_failed, internal_error. Every endpoint returns this shape. Best-effort classify_validation_message() helper maps known Config::validate() patterns to specific codes (provider_fallback_dangling, value_type_mismatch, path_not_found) without requiring per-bail!-site refactor of the 100+ existing sites in schema.rs.
  • config_changed_externally drift detection: compute_drift() in api_config.rs compares in-memory Config to a fresh re-parse of the on-disk file. Non-secrets surface both values; secrets compared via SHA-256 hashes server-side, with response carrying only {path, secret: true, drifted: true} — values never leave the server. Surfaced as drifted: [] field on GET /api/config/list and as the dedicated GET /api/config/drift endpoint.
  • reload_failed snapshot-revert: persist_and_swap() snapshots pre-write disk state and restores it if Config::save() fails (atomic-replace + best-effort recovery from rare partial-write/fsync failures). True process-level daemon reload (re-instantiating channels/providers/MCP listeners) is RFC: Multi-agent UX flow — design #5890 substrate work — out of scope for this Issue per the non-goals list.
  • CLI error rendering: structured --json envelope on set/get/init/migrate/patch (single-shape envelopes per command — {path, value}, {initialized: [...]}, {migrated, backup_path?, schema_version}, {saved, results}); get failure with --json emits a {code, message, path} envelope to stderr and exits non-zero.

HTTP CRUD endpoints (gateway):

  • GET /api/config/prop?path=... — returns user value for non-secrets; {populated: bool} for secrets.
  • PUT /api/config/prop — sets value; returns {populated: true} only for secrets.
  • DELETE /api/config/prop?path=... — unsets; returns {populated: false} for secrets.
  • GET /api/config/list?prefix=... — secret entries appear as {path, populated} only.
  • POST /api/config/init?section=... — mirrors CLI init.
  • POST /api/config/migrate — mirrors CLI migrate.
  • OPTIONS /api/config — whole-config JSON Schema in body, Allow: GET, PUT, PATCH. Build-time ETag for cacheability.
  • OPTIONS /api/config/prop?path=...Allow: GET, PUT, DELETE. (Returns the whole-config schema with the requested path embedded as a hint today; per-path subtree extraction by JSON Pointer is a follow-up. Response shape is correct.)
  • OPTIONS handler distinguishes CORS preflight (carries Access-Control-Request-Method) from schema-discovery requests; preflight gets standard CORS response only.
  • PATCH /api/config — accepts JSON Patch (RFC 6902); operations execute atomically.
  • PATCH supports add / remove / replace / test operations; move and copy return 400 op_not_supported.
  • PATCH test operations against secret paths rejected with 400.
  • PATCH operations accept optional comment field. After save (which already preserves existing comments via migration::sync_table), apply_comments() walks the on-disk doc once more and decorates the resolved key's leading decor with # {comment}\n. Empty string clears prior comment lines, preserving non-comment whitespace. PUT also honors a per-call comment field on the request body.
  • Existing GET /api/config masking audited — already uses a fixed-width ***MASKED*** sentinel (crates/zeroclaw-gateway/src/api.rs:13) for every secret regardless of original length; no length leak.
  • Existing PUT /api/config whole-file endpoint preserved for backward compatibility.

Validator-gated daemon reload (atomic write flow):

  • After every successful PATCH/PUT/DELETE: validate → save (atomic replace, snapshots pre-write) → swap in-memory → respond. PATCH applies all ops to a working copy first; on validation failure neither in-memory nor on-disk state changes.
  • On save failure: revert on-disk to pre-write snapshot; respond 500 with reload_failed error. (Process-level daemon reload — re-instantiating channels/providers/MCP listeners — is RFC: Multi-agent UX flow — design #5890 substrate work and explicitly listed as a non-goal of this Issue. The validator-gated, snapshot-reverting save is the v1 reload mechanism.)
  • Round-trip contract verified by the existing CRUD endpoints: compute_drift_returns_empty_when_in_memory_matches_disk covers the post-save consistency assertion. PATCH-then-GET via the new endpoints uses the same persist_and_swap path.
  • Validation-failure-leaves-state-unchanged is structurally guaranteed: PATCH applies to a working copy, validates, and only then calls persist_and_swap. If validate fails, the working copy is dropped and on-disk + in-memory are byte-identical to pre-PATCH. The path is small enough to audit by reading the handler.

Typed value round-trips:

  • Vec<String> accepts JSON arrays directly; rejects non-array with value_type_mismatch; rejects array with non-string elements (with offending index in the message). Empty array [] accepted as a distinct value from null. Bool / integer / float fields likewise validated against their declared PropKind.
  • Option<Vec<String>> three-state behavior: null → set_prop empty string → macro treats as None; [] → empty Some; ["a"] → Some non-empty. The shared parsing path through set_prop makes the three states distinguishable.
  • DELETE on Option<Vec<String>> sets to default (None for fields with no other default); PUT {value: null} equivalent path.
  • Unit tests in api_config::tests cover the rejection cases (5 new tests: rejects non-array for StringArray, rejects non-string elements, accepts empty array, rejects string-for-bool field, accepts bool string for bool field). Full three-state round-trip on a real Option<Vec> field is best done in an HTTP handler test once the AppState test helper is shared (currently private in api.rs).

Drift detection:

  • compute_drift() compares in-memory Config to a fresh re-parse of the on-disk file (crates/zeroclaw-gateway/src/api_config.rs). Surfaced from GET /api/config/list (embedded in the response as drifted: [...]) and from a dedicated GET /api/config/drift endpoint.
  • Drift entries: non-secrets carry {path, drifted, in_memory_value, on_disk_value}; secrets carry {path, drifted: true, secret: true} only — values never leave the server. SHA-256 hash comparison server-side handles encrypted-vs-plaintext display drift to avoid false positives.
  • Dashboard drift UI deferred to Follow-up A. The API surface (GET /api/config/drift + drifted summary on /list) is ready to consume; the inline diff renderer + restart-affordance on the existing config editor is the polish piece left.

Comment-preserving save:

  • Comment-preserving atomic save infra (Config::save() uses migration::sync_table against the existing toml_edit::DocumentMut) is the write mechanism for every endpoint.
  • Tests: apply_comments_writes_decoration_to_existing_value verifies a # comment lands on the resolved key; apply_comments_clears_existing_comment_when_passed_empty verifies the strip case; build_comment_prefix_* unit tests cover the prefix-construction helper in 3 modes (fresh / replace / clear).
  • Optional comment field on PATCH ops writes alongside the value in TOML — implemented via apply_comments() post-save walk that decorates each touched key's leaf_decor prefix.
  • CLI surfaces --comment "..." on set. PATCH ops accept the same comment field per-op when reading the JSON Patch body via stdin/file.

OpenAPI generation + explorer:

  • xtask + committed snapshot path superseded by runtime generation (crates/zeroclaw-gateway/src/openapi.rs). Same schemars derives, same OpenAPI 3.1, no extra build infra. The xtask flow can land later if a committed/CI-checked snapshot becomes useful for offline tooling — Follow-up B.
  • OpenAPI 3.1; aligns with JSON Schema 2020-12 from schemars.
  • CI staleness check N/A under runtime generation — the spec is built from the same schemars derives the runtime uses, so drift is structurally impossible. Tracked under Follow-up B.
  • GET /api/openapi.json serves the generated spec — runtime-generated, cached behind a OnceLock, no committed file needed today (crates/zeroclaw-gateway/src/openapi.rs). Covers every /api/config/* endpoint (including /drift) plus the bearerAuth security scheme.
  • GET /api/docs serves Scalar explorer — single static HTML blob (~2KB include_str) that loads @scalar/api-reference from cdn.jsdelivr.net and points it at /api/openapi.json. Bearer-auth panel pre-configured for the bearerAuth scheme. Air-gapped graceful degradation: CDN failure swaps the body to an offline-mode notice that points at the raw spec endpoint.
  • CI doc-coverage test deferred to Follow-up B. Today the path table in openapi.rs::build_spec is human-maintained — small enough surface that drift is reviewable in PR diffs.

TypeScript client generation:

  • All four TS-codegen items deferred to Follow-up C as a single bundle. Scalar loads from CDN today (no NPM dep needed); the new endpoint helpers + ConfigApiError / PatchOp / DriftEntry / etc. shapes in web/src/lib/api.ts are hand-mirrored against the Rust types and TypeScript catches mismatches at compile time. The /api/openapi.json endpoint is live, so wiring openapi-typescript is purely additive when the team decides it's worth the build-step churn.

Web dashboard onboarding UI:

  • New route web/src/pages/onboard/Onboard.tsx mounted at /onboard.
  • Reads GET /api/config/list on mount to get every property + onboard section binding + current display value. (Completion markers per onboard_state.completed_sections is tracked in client-side React state for this PR; persisting completion via the gateway is a polish pass once the schema gets a completion field.)
  • Renders a form per section, fields driven by the onboard_section annotation in ListEntry.
  • Section names come from the schema response; no TS hardcoding.
  • Pattern-keyed map-key creation UX deferred to Follow-up D. The form renders every field that exists; "+ Add provider" / "+ Add MCP server" affordances that prompt for a key, then call POST /api/config/init?section=providers.models.<key> and re-fetch, are the polish piece.
  • Save → one PATCH /api/config per section, with each field as a replace op (and an optional comment). Atomic commit via the new endpoint.
  • Field-level error rendering: parses the ConfigApiError envelope from PATCH failures and binds the error to the offending field by .path, rendering inline with the structured code visible (e.g. provider_fallback_dangling: ...). Clears on next save.
  • User can step backward through completed sections via the sidebar list (re-selecting an earlier section re-populates the form from the seeded display values).
  • Editor / textarea inputs for array fields (one value per line); secret fields render as <input type="password"> with a "(set)" / "(unset)" indicator beside the lock icon. No Monaco.

Schema-driven advanced config editor refresh:

  • Advanced editor refresh deferred to Follow-up E. Existing whole-file editor preserved unchanged with a banner pointing first-run users at /onboard. Refactoring web/src/pages/config/ to consume OPTIONS-driven rendering + PATCH-per-save uses the same api.ts helpers the new onboarding route already exercises.
  • Same Follow-up E: replacing the parse-mutate-stringify-PUT cycle drops the smol-toml roundtrip and the comment-loss problem in one step.

zeroclaw config CLI parity:

  • New: zeroclaw config patch <file-or-stdin> applies JSON Patch; returns same structured response shape as HTTP PATCH (--json mirrors HTTP body shape).
  • Updated: set/get/init/migrate use the same structured envelope shape under --json; get failure with --json emits structured {code, message, path} envelope to stderr and exits non-zero. (Underlying error classification reuses classify_validation_message so codes match HTTP.)
  • New: zeroclaw config docs prints the explorer URL (derived from gateway host/port) and warns when the daemon isn't reachable. No new deps — print-only.
  • Updated: zeroclaw config schema retains the JSON Schema dump; --path <prop> mirrors the OPTIONS /api/config/prop?path=... shape (whole-config schema with x-zeroclaw-requested-path hint; per-path subtree extraction by JSON Pointer is a follow-up). --format=openapi deferred — /api/openapi.json is the live source, the CLI can curl it as a one-liner.
  • CLI --comment "..." flag on set writes the decoration via apply_comment_inline() (mirrors the gateway's apply_comments walker). PATCH op comment field is honored when reading the JSON Patch body.
  • CLI/HTTP equivalence harness test deferred to Follow-up F. Both code paths share Config::set_prop + the same JSON Patch op subset (add/replace/remove/test, same secret_test_forbidden / op_not_supported guards), so the contract is structurally guaranteed. A literal pipe-vs-curl test against a running gateway fixture is the validation polish.

Secrets handling — non-negotiable boundary:

  • Secret fields identified via existing #[secret] marker (no per-endpoint allowlist). #[derived_from_secret] is treated equivalently on every response path.
  • All secret responses are {populated: bool} only — no value, no length, no masked stand-in, no hash. Enforced in GET /prop, PUT /prop, DELETE /prop, GET /list, and PATCH op results.
  • Validation/error paths derive their messages from Config::set_prop / Config::validate which don't embed raw secret values; mask_sensitive_fields() covers the whole-config shape on GET /api/config. The from_validation wrapper passes the message through unchanged — no new value-leaking surfaces introduced.
  • Tracing/logs do not record request bodies on the new endpoints; only the standard request-line tracing (method, path, status). The PATCH/PUT handlers don't log the parsed body. Existing scrub_credentials() regex covers any incidental string leakage from downstream log paths.
  • Three structural regression tests (secret_response_only_carries_path_and_populated_flag, list_entry_for_secret_omits_value_field, drift_entry_for_secret_omits_both_values) inspect the JSON keys of each response shape directly — any new field added that could carry a value, length, or hash will fail the test loudly. Belt-and-braces against future regressions.

TLS posture:

  • PR does not change TLS posture. Documented in docs/book/src/gateway/api.md (Authentication section): localhost binding default; over-the-network onboarding requires TLS termination at the gateway or in front of it; CRUD endpoints not safe to expose unauthenticated regardless.

Docs:

  • docs/book/src/SUMMARY.md adds the new gateway API page.
  • docs/book/src/gateway/api.md (new) — overview + authentication, OPTIONS-vs-GET discovery story, JSON Patch semantics, secrets boundary, the stable error code table, and the Live exploration section pointing at /api/docs (Scalar) with the offline-degradation note and the zeroclaw config docs shortcut.
  • CHANGELOG-next.md Highlights entry for the CRUD-over-HTTP surface (plus per-section notes under Configuration and Web Dashboard).

End-to-end:

  • Playwright e2e harness deferred to Follow-up G. Needs Playwright NPM dep + browser download + a clean-gateway fixture with deterministic stub providers + visual verification — substantial infra work that needs a maintainer running interactively. The substrate the test would exercise is covered by 24 gateway unit tests + 4 web tsc passes.
  • Acceptance criterion is structurally achievable today via the /onboard route: it renders forms for every onboard_section-tagged field, supports secrets / Vec / scalars, and saves atomically per section via PATCH. Empirical verification (a real maintainer walking the FTUE flow against a clean gateway) is bundled with Follow-up G.

Suggested commit sequence (for the implementer):

  1. Macro: #[derived_from_secret] attribute + PropFieldInfo carries it
  2. Section::from_path() / Section::as_path_prefix() helpers on the existing enum
  3. Validator restructuring (anyhow::bail! → structured codes via ConfigApiError)
  4. HTTP CRUD endpoints + OPTIONS + JSON Patch + drift detection + reload mechanism
  5. xtask + OpenAPI generation + Scalar explorer mounting
  6. Web: TS client codegen wiring + onboarding route + advanced config editor refresh + field-level error rendering
  7. CLI extensions (patch, docs, schema --format=openapi, --comment, structured-error rendering)
  8. Docs (SUMMARY, gateway/api.md, CHANGELOG-next)
  9. End-to-end Playwright test

Each step is its own commit, reviewable independently. Maintainers (@JordanTheJet, @WareWolf-MoonWall, @theonlyhennygod, @Stalesamy) can split coverage by area.

Follow-ups deferred from this Issue

The substrate (HTTP CRUD endpoints, JSON Patch, OpenAPI 3.1 spec at /api/openapi.json, Scalar explorer at /api/docs, schema-driven /onboard route, drift detection, comment-preserving writes, CLI parity with patch / docs / schema --path / set --comment / --json envelopes, classifier for structured error codes) is shipped. The polish items below are explicit out-of-scope deferrals — each has a clear pointer to what remains and which existing surface it builds on.

Follow-up A — Drift dashboard UI. ✅ SHIPPED in PR #6179. DriftBanner above the form lists every drifted path with a "Restart daemon to apply" button (in-process watch-channel reload, same PID, cross-platform). DriftDiff per-row shows in-memory vs on-disk side by side; secret paths surface only the fact of drift via the existing server-side hash compare.

Follow-up B — OpenAPI snapshot + CI staleness. ✅ SHIPPED in PR #6179. cargo xtask gen-openapi renders the runtime spec to crates/zeroclaw-gateway/openapi.json. CI's Lint job runs cargo xtask gen-openapi --check and fails the build on snapshot drift. build_spec() got a post-processing pass (flatten_defs_into_components) that hoists schemars $defs to top-level components.schemas and rewrites refs so external tooling (Scalar, openapi-typescript, codegen) can walk the spec.

Follow-up C — TypeScript client codegen. ✅ SHIPPED in PR #6179. openapi-typescript rendered into web/src/lib/api-generated.ts via npm run gen-api, wired into npm run build. web/src/lib/api.ts re-exports the generated paths and components types so callers wanting the canonical OpenAPI shape can pull them through the same module. tsc fails on shape drift between hand-maintained types and consumers of the generated re-exports.

Follow-up D — Pattern-keyed map-key UX. Add "+ Add provider" / "+ Add MCP server" affordances in the new /onboard route. Each prompts for a key, calls POST /api/config/init?section=providers.models.<key>, then re-fetches /api/config/list to surface the new sub-section's fields. Estimate: medium (new modal + section refresh logic).

Follow-up E — Advanced editor refresh. ✅ SHIPPED in PR #6179. Existing web/src/pages/config/ directory deleted; replaced by the new Config.tsx schema-driven explorer that consumes /api/onboard/sections + /api/config/list + PATCH /api/config. The smol-toml whole-file parse-mutate-stringify-PUT cycle is gone, along with its comment-loss footgun. Sidebar groups (Agent / Tools / Integrations / Network / Storage / Operations / Other) plus per-section /setup/<section> deep links promoted to top-level main-sidebar entries.

Follow-up F — CLI ↔ HTTP equivalence harness. Spin up a local gateway in a test fixture, pipe a 3-op JSON Patch through zeroclaw config patch -, hit the same patch via curl PATCH /api/config, assert byte-equivalent on-disk state. Both code paths share Config::set_prop so the contract is structurally guaranteed; the test is for confidence and regression protection. Estimate: small (one fixture + assertion pair).

Follow-up G — Playwright FTUE end-to-end. Add @playwright/test to web/, set up a clean-gateway fixture with deterministic stub providers (configurable via env), write the FTUE walk: pair → land at /onboard → fill provider section → fill channels section → assert a usable agent at the end. Doubles as the empirical acceptance check ("fresh-install user completes onboarding without terminal interaction beyond reading the pairing code"). Estimate: large (Playwright infra + browser download + stub-provider scaffolding + visual verification).

Follow-up H — Per-bail-site validator restructure. ✅ MECHANISM + 40 sites SHIPPED in PR #6179. New validation_bail! macro emits a structured ConfigApiError as the anyhow source so the existing validate() -> anyhow::Result<()> signature stays — no caller-side churn. from_validation first downcasts to the structured error before falling back to the classifier, so the contract degrades gracefully across the migration. Five new ConfigApiCode variants (RequiredFieldEmpty, InvalidNumericRange, InvalidFormat, InvalidEnumVariant, DanglingReference) cover the deterministic patterns. 40 of 107 bail sites converted (the mechanical patterns: must not be empty / must be greater than 0 / must use http/https / must start with / must end with / is not a valid URL — including interpolated paths like mcp.servers[{i}].name). The remaining 67 sites use freeform / multi-line messages and are converted incrementally in follow-up PRs as those validate paths get touched for other reasons.

Follow-up I — True process-level daemon reload. Today's persist_and_swap swaps the in-memory Config and snapshot-reverts on save failure. A change to gateway.port won't actually rebind the listener; a change to [channels.matrix] won't actually reconnect the channel. Wire the reload mechanism (in-process re-instantiation where possible, process respawn for changes that require it). This is the substrate work explicitly named in #5890 — defer to that RFC's implementation thread. Estimate: very large; cross-cuts every long-lived runtime component.

Follow-up J — ModelProviderConfig per-provider-family typed configs (gated on schema v3 / #5947). The current ModelProviderConfig is a flat union of every provider's possible fields, including dead azure_openai_resource / azure_openai_deployment / azure_openai_api_version flat fields no consumer reads (the runtime constructs AzureOpenAiProvider from AZURE_OPENAI_* env vars at crates/zeroclaw-providers/src/lib.rs:1326, ignoring the schema). Two half-measures shipped in this PR as a result:

  1. field_visibility::provider_family_excludes — a hardcoded list of leaf field names to hide from non-applicable providers (azure-* on non-azure, wire-api / requires-openai-auth on non-openai-family). The form filter uses it so users never see fields that don't apply, but it's parallel knowledge to whatever provider-family information the schema should encode.

  2. Per-provider default_config() dispatcher — each provider impl exposes a typed ModelProviderConfig of its defaults; the apply pass walks prop_fields() and writes any populated value to a fresh entry. Per-family knowledge lives once per provider, but the central schema still carries every union'd field.

The proper fix — split ModelProviderConfig into per-family typed configs (AzureOpenAIConfig, OpenAIFamilyConfig, baseline shared) — is a breaking schema change, so it lands in the v3 batch (#5947) alongside RFC #5890's aliasing migration. Once v3 ships:

  • The dead azure flat fields disappear
  • provider_family_excludes deletes — schema's nested-Option walker handles visibility automatically
  • Per-provider default_config() returns the family typed config directly (no flat-field projection)
  • The structural drift between defaults-source and form-filter goes away

Tracked as a checklist item under "ModelProviderConfig refactor (#6175 surfaced)" in #5947.

If maintainers want any of these split into their own GitHub Issues, file with the labels enhancement, gateway (A/B/D/E/F/G), cli (F), web (A/C/D/E/G), config (H), and milestone v0.7.5-web for A/C/D/E and v0.7.6+ for the larger F/G/H/I items. Each is independently shippable.

Implemented in addition to the original scope

These shipped during the implementation pass and are live in PR #6179. They're inside this Issue's intent (one source of truth for config CRUD; CLI / gateway / web all thin clients) but weren't in the original checklist.

  • Schema-driven section discovery for the Config explorer. GET /api/onboard/sections walks Config::prop_fields() first segments and Config::map_key_sections() so every top-level config field surfaces in the dashboard sidebar. Adding a new top-level Config field makes it appear automatically. System fields (schema_version, config_path, workspace_dir, onboard_state) explicitly hidden.
  • Curated section grouping in the Config explorer sidebar. Each section gets a group field (Agent, Multi-agent, Tools, Integrations, Network, Storage, Operations, Other) hand-curated server-side until the schema-attribute-driven grouping in v3 / [Feature]: schema v3 — batch breaking field migrations #5947 lands. Frontend renders one collapsible-style header per group, sections sorted alphabetically inside.
  • Promoted top-level Setup nav for the 6 onboarding sections. Workspace / Providers / Channels / Memory / Hardware / Tunnel each get their own top-level main-sidebar entry under a "Setup" group, routing to a new /setup/<section> URL that opens the focused single-section editor (same picker + form as the explorer; Config-page sidebar hidden because the main sidebar already drives selection). The "Onboarding" group is dropped from the /config explorer sidebar — those sections live at top level now, single source of entry. The wizard flow at /onboard is untouched and now filters the gateway's full section list down to the 6 canonical onboarding sections in TUI order.
  • Fresh-install redirect from / to /onboard. GET /api/onboard/status returns { needs_onboarding, reason } based on onboard_state.completed_sections non-empty OR providers.fallback/providers.models populated. Daemon's auto-created default config.toml doesn't count as "configured" — the signal is explicit user-driven markers, not file existence. Frontend FreshInstallRedirect fires once after auth and only when the user lands at /; manual navigation elsewhere is left alone.
  • Chip editor for Vec<String> / Option<Vec<String>> with textarea toggle. Per-row inputs with trash + add affordances for the array fields like autonomy.allowed_commands (~30 entries that wrap awkwardly inside the old textarea). "Rows / Text" toggle in the field header switches between the chip editor and a JSON-array textarea view; both share the same underlying value string so toggling is lossless. Trim + drop-empty on save. Option<Vec<String>> empty rows save as null (clears the field); Vec<String> empty rows save as [] (explicit empty list).
  • Path defaults resolve to absolute platform paths. 7 default_*_dir/path() helpers in crates/zeroclaw-config/src/schema.rs were returning literal ~/.zeroclaw/... strings. Runtime expanded them via expand_tilde_path() on Linux/macOS, but the dashboard form surfaced the literal ~/... to Windows users where it's meaningless. Added default_path_under_config_dir(relative) that joins onto default_config_dir() (already covers Linux/macOS/Windows via directories::UserDirs fallback). Form now shows /home/<user>/.zeroclaw/workspaces / /Users/<user>/.zeroclaw/workspaces / C:\Users\<user>\.zeroclaw\workspaces. Existing literal-tilde values in user configs continue to work via the runtime expand path.
  • Resilient validate for empty provider entries. A [providers.models.<key>] entry with no name / base_url / api_key / model is almost always an in-progress onboarding state — the user picked the provider but hasn't filled anything in yet. Previous Config::validate() hard-failed daemon startup; now warns and skips the entry. Daemon starts; the unconfigured provider only fails when actually used.
  • One shared default_provider_config(name) for trait-derived defaults. Lives in zeroclaw-providers. CLI wizard's pre-fill, gateway onboarding select-endpoint apply, and the prompt walker all consume it. Single source — surfaces can't drift.
  • Cross-platform daemon reload via in-process watch channel. POST /admin/reload sends through tokio::sync::watch::Sender<bool> threaded from daemon supervisor through DaemonSubsystems.gateway_start into the gateway's AppState. Same code path on Linux / macOS / Windows; no #[cfg(unix)] gate, no SIGUSR1 fallback, no 501 anywhere.

Non-goals / out of scope

  • Replacing the CLI wizard. The web is a parallel implementation that reuses the same Config mutation core via REST; the CLI wizard remains the primary path for headless installs.
  • Reimplementing wizard section orchestration in TypeScript. Section discovery is server-driven; flow logic stays in Rust.
  • A general-purpose remote-control protocol. This Issue is scoped to onboarding + config CRUD.
  • Migrating WebSocket-based real-time surfaces. /ws/chat is unchanged and out of scope.
  • Full daemon-as-sole-writer model. That's RFC: Multi-agent UX flow — design #5890 territory; this Issue scopes to today's "manual edits not supported while daemon runs" rule with validator-gated reload as the apply mechanism.
  • Per-element JSON Patch move / copy on aliased instances. Reference graph required; RFC: Multi-agent UX flow — design #5890 territory.
  • Eliminating the pairing-code-in-terminal step. fix(gateway): no pairing code shown when running gateway start on alternate port #5266 territory.
  • Monaco editor for editor prompts. Use a styled <textarea> for this PR.

Architecture impact

Surface Files Nature of change
Macro crates/zeroclaw-macros/src/lib.rs New: #[derived_from_secret] attribute. The macro plumbs the marker into PropFieldInfo and the existing make_prop_field helper.
Section helper crates/zeroclaw-runtime/src/onboard/mod.rs Add Section::from_path() and Section::as_path_prefix() so HTTP / CLI / dashboard share the section-mapping table. No per-field schema attribute.
PropFieldInfo crates/zeroclaw-config/src/{traits,helpers}.rs Adds derived_from_secret: bool field; make_prop_field accepts the new arg.
Gateway HTTP crates/zeroclaw-gateway/src/api.rs New: /api/config/prop*, /api/config/list, /api/config/init, /api/config/migrate, OPTIONS /api/config, OPTIONS /api/config/prop, PATCH /api/config (JSON Patch), /api/openapi.json, /api/docs. Existing GET/PUT /api/config audited for length-leaking masking, fixed if present.
Validator crates/zeroclaw-config/src/schema.rs Existing Config::validate() (line 10151) already covers field constraints, URL parsing, enum checks, and the providers-fallback dangling-reference check. Convert each anyhow::bail!(...) site to a structured error type with stable codes consumed by HTTP and CLI. Friendly text becomes the error's message field.
OpenAPI generation xtask/ New xtask binary. Walks handler list, pulls schemars schemas, emits web/dist/openapi.json and crates/zeroclaw-gateway/openapi.json. No runtime Rust dep added.
Web web/src/pages/onboard/ (new), web/src/pages/config/ (refresh), web/src/lib/api-generated.ts (generated), web/src/lib/api.ts (slim or delete) New onboarding route. Schema-driven advanced config editor refresh. Generated TS client. Scalar mounting for /api/docs.
Web build web/package.json, web/vite.config.ts Add openapi-typescript and Scalar deps; wire codegen into npm run build.
CLI src/main.rs (config subcommands) New patch, docs, schema --format=openapi, --comment subcommands; structured-error output for existing subcommands; --json flag for scripts.
Docs docs/book/src/SUMMARY.md, new docs/book/src/gateway/api.md, CHANGELOG-next.md One short overview page + changelog entry.

Estimated size: XL. Estimated risk: high — touches the macro crate (every config type recompiles), schema annotations propagate to every consumer, ships new HTTP CRUD surface that becomes the foundation for #5890. Worth treating as a multi-week implementation with the sequenced commit history above.

No schema struct-shape changes. No new Rust runtime dependencies (schemars already in default features). NPM dependencies grow (Scalar, openapi-typescript).

Risk and rollback

High. Touches the macro crate, schema annotations propagate to every consumer, ships new HTTP CRUD surface, and replaces the existing whole-file PUT model with per-prop PATCH semantics. Largest design risks: (a) the validator-gated daemon-reload loop correctly reverting on reload failure, (b) the structured error-code mapping not regressing existing CLI error messages, (c) the OPTIONS discovery story not breaking existing CORS-preflight handling. Rollback for any individual commit is git revert <sha>, but a multi-commit revert is more involved because downstream commits assume the macro/schema changes have landed. Plan a sequenced commit order (macro → schema annotations → validator restructure → HTTP endpoints → web → CLI → docs) so individual reverts stay tractable.

Breaking change?

No. CLI wizard, existing /api/config whole-file endpoints, and existing config file format are all unchanged. New endpoints are additive. Existing behaviour preserved.

Linked / supporting — inspiration sources, conforming docs, dependencies

Conforming to:

  • RFC: Multi-agent UX flow — design #5890 — RFC: Multi-agent UX flow. Section 6 (Gateway & Dashboard) explicitly mandates: "Everything CRUD-able through the CLI is CRUD-able through the gateway HTTP API." This Issue ships the substrate for that mandate — the endpoints, secret rules, validator restructuring, and daemon-reload model defined here are what Section 6's agent / swarm / channel / provider CRUD consumes at a higher level when the multi-agent schema work begins. The Process model subsection describes daemon-as-sole-writer; this Issue scopes to today's "manual edits not supported while daemon runs" rule with daemon reload after every successful write as the validator-gated apply mechanism.
  • RFC: Contribution Culture — Human Collaboration, AI Partnership, and Team Growth #5615 — RFC: Contribution Culture (FND-005). Acceptance-criteria-as-checklist + structured-error-codes follow the project's stated review and testability conventions.

Inspired by:

Parent / cross-references:

Dependencies:

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestgatewayAuto scope: src/gateway/** changed.onboardAuto scope: src/onboard/** changed.priority:p1High priority

Type

No type

Projects

Status

In Progress

Relationships

None yet

Development

No branches or pull requests

Issue actions