# 09 · Settings, Secrets, and Runtime State

Read this whenever an extension needs configuration, credentials, onboarding
completion, schedules, durable records, or runtime state.

The wasm server runtime has live `settings` and `secrets` imports
([`07-wasm-server-backends.md`](07-wasm-server-backends.md)):

- `settings.get / set / list-all` — per-extension key/value JSON, granted when
  the manifest declares any `settingsSchemas`.
- `secrets.get / set / delete` — per-extension, **encrypted at rest** and
  isolated; always granted. `get`/`set` require the host to be configured with
  `CEDROS_EXTENSION_SECRETS_PASSPHRASE` (otherwise they fail closed); `delete`
  never touches the passphrase and still removes stored ciphertext.

## Allowed storage

- manifest metadata for static contribution ownership
- Cedros extension management state for install/enable/contribution lifecycle
  and onboarding completion
- Cedros automation settings for runtime schedules
- Cedros host secret storage for credentials
- Cedros site provider configuration for shared providers
- extension-owned records only when a real host content/data model exists
  ([`08-site-database.md`](08-site-database.md))
- site-brain records for durable operator/agent activity
- structured logs for transient diagnostics

## Not allowed

- secrets in manifests
- secrets in bootstrap files
- secrets in ZIP archives
- secrets in job metadata
- authoritative settings in browser `localStorage`
- hidden runtime config inside authored documents
- raw PII in analytics, logs, or site-brain records
- extension-owned provider keys when site-owned providers exist
- private per-extension schedulers, analytics stores, or brain/audit feeds

## Extension settings

Setting keys use lowercase namespaced keys such as
`${extensionId}:provider-mode` or `${extensionId}:sync.enabled`. Keep keys
stable after release.

Document every setting with:

| Field | Notes |
|---|---|
| key | Stable setting key. |
| owner | Cedros host, extension admin page, or operator. |
| storage | Host settings API, extension management state, automation settings, or host-coordinated content model. |
| default | Safe install default. |
| validation | Field constraints and error messages. |
| secret? | Whether the value must be a secret reference. |
| upgrade behavior | Preserve, migrate, reset, or deprecate. |
| disable behavior | Preserve, pause, hide, or fail closed. |
| remove behavior | Preserve by policy, delete by explicit action, or host-coordinated. |

Settings schemas declared in `surfaces.server.settingsSchemas[]` grant the wasm
server runtime `settings.get / set / list-all` for per-extension JSON values.
The ids are a grant, not a schema: intake checks only that they are unique, and
`settings.set` checks only that the value is strict JSON. The "validation" row
above is the extension's own responsibility — validate every value in the Wasm
route or job before calling `set`, and treat values read back as untrusted.
Admin pages can ship their own configuration UI, but persistence must go
through a Wasm route or job that uses the `settings` import: the `cedros:admin`
runtime exports no settings or secrets service (`HOST_SERVICE_IDS` has none).
If the extension has no Wasm backend, mark admin persistence as
host-coordinated instead of storing settings in browser state.

### Autosave over the single-key Wasm seam

`settings.set(key, value)` writes exactly one key. It is non-batching and
provides no multi-key transaction. By contrast, `useCedrosAutosave` passes the
adapter whatever patch `makePatch` returns. That patch may contain several
fields when edits share one debounce window. When `persistenceKey` is enabled,
the hook may also restore a previously queued multi-field snapshot from
IndexedDB and call `save` immediately after mount, before the operator makes a
new edit.

An adapter backed by a route that delegates to `settings.set` must therefore:

1. choose a stable key order and send one setting per route request;
2. reuse stable per-key idempotency identities across retry;
3. stop on the first failed write;
4. return success only for the keys the host actually committed; and
5. throw for the remaining draft so `useCedrosAutosave` retains/retries it
   instead of reporting the whole patch saved.

The route itself must reject a multi-key request. Do not make it appear atomic
by looping inside one HTTP handler: independent `settings.set` calls can still
partially commit. The adapter's canonical response must merge only confirmed
writes and keep unsaved fields dirty. Test at least (a) two different edits in
one debounce window and (b) an IndexedDB-restored multi-field snapshot.

Serialized independent autosave is appropriate only when each key can commit
and retry independently. For an invariant spanning multiple settings, or
settings plus secrets/audit, use the named conditional database operation
`extension.configuration.compare-and-set`. Data 0.1.89 adds registered
Beacon Manager and Beacon client grants, plus the read-only
`extension.configuration.status` operation. The exact access IDs, models,
parameters, and outputs are in
[`reference/conditional-operations.json`](reference/conditional-operations.json).
For Beacon, declare required `cedros-data:extension-config-cas-v1` and minimum
Data version `0.1.89`; send `extensionId` equal to the calling extension.
The host rejects a different owner, access grant, missing audit model, or scope.

Read the opaque revision with a conditional `status` batch operation before
reading the metadata to edit. Submit that revision as `expectedRevision` in the
configuration CAS. Put any accompanying metadata writes in the **same batch**;
any failed write rolls back settings, encrypted secrets, audit, revision, and
receipt. A stale revision fails with the redacted configuration precondition
conflict. Receipts and revisions are isolated by extension (and by the site's
own database). `changedKeys` must exactly match the setting and secret keys;
Beacon may use empty arrays when its configuration is in companion content-model
writes. Audit records include actor, reason, and revision metadata, never secret
values. Mutation IDs are unique per attempt; an exact replay returns the original
result, while reuse with different configuration material is rejected. Keep any
sibling writes identical when replaying a batch. Omitting `extensionId` retains
the original Pay-only contract.

Declare required capability
`cedros-data:extension-config-cas-v1`, a compatible access id/model grant, a
stable `mutationId`, and an expected revision. Do not emulate that atomic
contract with sequential settings or secret imports.

Place extension-owned settings/setup/policy/credential-reference pages in the
admin sidebar with `group: "Extension Settings"`. Do not put extension
configuration pages in the host-owned `Settings` group; Cedros reserves that
group for core site settings and orders `Extension Settings` between `Settings`
and `Tools`.

Safe setting example:

- key: `acme-demo:sync.enabled`
- value: `true`
- reason: boolean owner preference, not a credential

Unsafe setting example:

- key: `acme-demo:api-token`
- value: `sk_live_redacted`
- reason: API tokens must be stored as host secrets, not settings

## Secrets

Secret references are handles, not values. Use `secret://...` only if Cedros
publishes that handle format; otherwise treat secret references as opaque
Cedros-managed ids. Unsafe values look like API keys, bearer tokens,
connection strings, private keys, or passwords and must never appear in
manifests, archives, starter content, logs, or job metadata.

Document every secret with:

- display name
- owning provider or downstream system
- who enters it
- where it is stored
- whether extension code ever receives the raw value
- redaction behavior
- rotation behavior
- deletion behavior
- staging/prod separation
- site-brain/logging records for save/rotate/delete

Rules:

- manifests and bootstrap files may reference required secret categories, never
  raw values
- job metadata may reference a secret handle only if the handle is safe and
  non-sensitive
- logs/site-brain records should say a secret was configured, rotated, or
  missing without showing the value
- host-provider credentials belong to the site provider configuration, not the
  extension
- adding a new required setting or secret after release is a versioning and
  rollout risk; document migration, default, and rollback behavior
  ([`23-release-versioning-and-upgrades.md`](23-release-versioning-and-upgrades.md))

## Runtime API availability

| API | Available today? | Runtime | If unavailable |
|---|---:|---|---|
| read setting | yes via wasm `settings.get/list-all` | server wasm | admin helper handoff if needed |
| write one setting | yes via wasm `settings.set` (single-key, non-batching) | server wasm | serialize independent keys in the admin adapter; use CAS for atomic groups |
| save secret | yes via wasm `secrets.set`, encrypted at rest | server wasm | host secret setup if passphrase is missing |
| rotate/delete secret | yes via wasm `secrets.set/delete` | server wasm | rotation (`set`) needs the passphrase; `delete` works without it |
| atomic settings + secrets + audit CAS | yes in Data 0.1.10 via `extension.configuration.compare-and-set` | `database.batch` named conditional op | require `cedros-data:extension-config-cas-v1`; do not emulate with sequential imports |
| onboarding completion | host-coordinated unless SDK exports it | admin | host state handoff |

## Onboarding completion

Onboarding metadata lives in `onboardingPages[]`
([`15-content-starters-and-onboarding.md`](15-content-starters-and-onboarding.md)).
Completion state lives in Cedros extension management state.

Document:

- onboarding page id
- target module id and section id
- completion criteria
- who marks completion
- whether completion can be reset
- blocked/skipped behavior
- behavior when the target page is removed or renamed
- site-brain record emitted on setup completion

If the SDK method for completion is not available, mark completion persistence
as host-coordinated rather than inventing a browser storage workaround.

## Runtime state

For jobs ([`11-jobs-and-scheduling.md`](11-jobs-and-scheduling.md)):

- durable schedules live in Cedros automation settings
- job run metadata must be safe JSON
- idempotency keys are required for writes, sends, and syncs
- durable run outcomes that matter to operators should emit site-brain records

For server routes:

- request/response state is transient unless stored through a host data model
- external input must be validated before writes or provider calls
- route records require a host content/data model
- **guest globals do not survive**: each invocation gets a fresh instance, so
  caches (e.g. a JWKS cache) must live in settings or the database

For provider calls:

- call attribution includes extension id, optional access id, feature,
  description, and tags
- prompt/input/output details must be redacted before logs or usage metadata
- email sends and storage writes need idempotency keys
- provider-disabled states fail closed

For owner-created content:

- seeded pages, created forms, and created docs are owner content
- disable/remove should not delete owner-created content without explicit owner
  action
- rollback needs renderer/template compatibility or a recoverable missing
  definition state

## Decision table

| Need | Use | Avoid |
|---|---|---|
| Static surface ownership | manifest metadata | runtime-only hidden declarations |
| Operator setup value | host settings API or extension management state | `localStorage` |
| API credential | host secret storage (wasm `secrets`) | manifest/bootstrap/archive |
| Site AI/image/voice/storage/email | host provider service ([`10`](10-host-providers.md)) | extension-owned keys |
| Scheduled work | Cedros automation settings + job hook | private scheduler |
| Durable operational history | site-brain ([`16`](16-analytics-and-site-brain.md)) | console-only logs |
| Short-lived cross-worker extension cache | Data 0.1.10 shared-cache named operations | process globals or durable settings as a cache |
| Diagnostics | structured logs | raw error dumps with secrets |
| Extension records | host data model ([`08`](08-site-database.md)) | hidden JSON in authored docs |
| Customer-visible label | customer tags ([`20`](20-crm-tags-and-source-events.md)) | private CRM-shaped tables |

## Acceptance checks

- every setting has storage, default, validation, and upgrade behavior
- every secret has storage, rotation, deletion, and redaction behavior
- onboarding completion is not stored in browser-only state
- jobs and routes have explicit runtime-state ownership
- disable/remove preserves owner-created content unless explicitly approved
- no secret-like values appear in manifests, bootstrap files, archives, logs,
  or starter content

Next: [`10-host-providers.md`](10-host-providers.md).
