Skip to content

fix(spec,cli): config.storage was never a stack key — stop reading it, and report undeclared top-level keys (#4167) - #4189

Merged
os-zhuang merged 7 commits into
mainfrom
claude/plugin-boot-logs-visibility-12kkwh
Jul 30, 2026
Merged

fix(spec,cli): config.storage was never a stack key — stop reading it, and report undeclared top-level keys (#4167)#4189
os-zhuang merged 7 commits into
mainfrom
claude/plugin-boot-logs-visibility-12kkwh

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #4167.

What the investigation changed

#4167 framed this as a choice: teach the plugin the driver/root dialect, or reject it at authoring. The evidence made it a third thing.

storage was never a stack key at all. ObjectStackDefinitionSchema declares 41 top-level keys and storage is not among them, and the schema is not .strict() — so defineStack strips it, and every documented authoring path goes through defineStack. The compiled-artifact path goes through the same parse.

So os serve's branch could only ever fire on a bare-object config on the config-boot path — where the value then also carried the driver/root spelling the plugin does not read. One authoring key, working on a single unreachable-in-practice path, silently absent on every other. A host writing storage: { driver: 's3', … } believed it configured S3 and got local disk.

That also rules out the alias option: the driver/root dialect has no external author. Repo-wide, nothing in examples/, content/docs/, or any test writes it — only two CLI-internal fallbacks did, and git log -S 'driver?:' -- storage-service-plugin.ts returns nothing, so those keys were never valid. There is no dialect to absorb; there was a typo.

The change

Rebuilt on #4178's walker — and why it is a second pass, not a fold

#4178 landed mid-review and refactored the unknown-key rule from a two-surface check (object/field) into a walker that derives its coverage from PLURAL_TO_SINGULAR and reads each schema's own unknown-key posture. This PR was rebased onto that architecture. lintUnknownAuthoringKeys keeps its signature — the earlier revision changed it, the current one does not.

The top-level pass stays a separate export for two reasons:

  • It is genuinely complementary. That walker iterates metadata collections, so a stack whose only mistake is at the envelope level — no objects, no pages, nothing to iterate — walks clean and reports nothing. A test pins exactly that.
  • The stack schema has to be injected, because stack.zod.ts imports the lint module and importing back would close a cycle. A separate export keeps that requirement visible: a call site either asks for the coverage or does not, and its absence shows up in a diff. An optional parameter would be the same silent-loss shape this rule family exists to report.

It reuses the walker's own keyPosture, so it lints only while the stack schema strips unknown keys, and goes quiet if that schema ever graduates to .strict() (ADR-0049 / #4001) — the lint can never become a second, disagreeing voice next to the parse.

onEnable is exempt — a false positive this PR caught on its own examples

Before merging I pointed the new lint at the apps we ship. examples/app-todo and examples/app-showcase both reported:

⚠ stack.onEnable: 'onEnable' is not a declared stack key, so its value is dropped at load.

That is false. onEnable is a function, so ObjectStackDefinitionSchema cannot declare it and dist/objectstack.json cannot carry it — but it is not lost: AppPlugin reads it straight off the authored bundle and calls it at start(), and the artifact-boot path grafts it back (#4095). It is the documented place to register action handlers. Not declared and dropped at load are different claims, and this is the one surface where they come apart.

New STACK_RUNTIME_MEMBERS names the authored top-level members the runtime honours off the bundle; the lint treats them as declared. The CLI's GRAFTABLE_RUNTIME_MEMBERS is now derived from it rather than restating it — two hand-written copies could disagree, and the disagreement would be silent in exactly the direction this rule family exists to catch.

onDisable is deliberately not exempt: it is declared in the protocol but no kernel, runtime or service calls it, so a value written there really does go nowhere. A test pins both halves of that distinction.

Verified end to end

Authoring path (defineStack):

defineStack: stack.storage: 'storage' is not a declared stack key, so its value is
dropped at load — the file-storage backend is a deployment concern, not an
application declaration. Configure it with the OS_STORAGE_* environment
variables, or per-deployment in Setup → Settings → Storage (which also holds
credentials — a stack definition would commit them to git and to any published
artifact).

Compile path (config that skips defineStack), showing both guidance kinds:

⚠ Undeclared authoring keys (2) — dropped at load (#3786)
  • stack.storage: … — the file-storage backend is a deployment concern…
  • stack.datasource: 'datasource' is not a declared stack key… — did you mean 'datasources'?

Re-harvested after the onEnable fix: all three shipped examples validate with zero undeclared-key warnings — which is also the first real evidence datapoint the #4001 strict-tier programme wanted out of this rule family.

@objectstack/spec 274 files / 7147 tests; @objectstack/cli 89 files / 916 tests; root pnpm lint clean; check:api-surface and all ten repo guard scripts pass locally.

Nothing that worked is being removed

  • not in the schema — stripped by defineStack and by compile;
  • not documented anywhere (all storage docs describe OS_STORAGE_* and the settings namespace);
  • no consumer in objectstack-ai/cloud — I attached and searched that repo directly rather than assuming.

Found in cloud while checking that — filed as cloud#935

The same dead-key mistake is in service-cloud's per-environment local-disk fallback, where the discarded value is the one implementing tenant isolation:

// Isolate uploads per environment so files written by tenant A can't be
// served as tenant B by a path-traversal mishap.
const root = path.join(dataRoot, 'environments', opts.environmentId, 'uploads');
await kernel.use(new StorageServicePlugin({ driver: 'local', root } as any));

root is dropped, so every environment shares one ./storage. The isolation that comment describes is not in effect. It is the fallback branch and OS_STORAGE_REQUIRE_REMOTE=1 avoids it, and I did not establish that two environments can collide on a storage key — so cloud#935 states the missing boundary without claiming a demonstrated leak. Same file gets the s3 path right, which is what makes it a typo rather than a dialect.

If storage should become a real stack key later

That is a separate, legitimate question — and if the answer is yes, the shape to copy is datasources, which solves credentials by referencing sys_secret instead of inlining them. That deserves an ADR, not a resurrected undeclared key. This PR does not foreclose it.


Generated by Claude Code

…, and report undeclared top-level keys (#4167)

`os serve` read `config.storage` and forwarded it to StorageServicePlugin. It
could almost never arrive: `ObjectStackDefinitionSchema` does not declare
`storage` and is not `.strict()`, so `defineStack` — which every documented
authoring path and every compiled artifact goes through — strips the key before
serve runs. The one combination that reached the branch (bare-object config on
the config-boot path) then carried the `driver`/`root` spelling the plugin does
not read either.

So one authoring key worked on a single unreachable-in-practice path and
vanished silently everywhere else. A host writing `storage: { driver: 's3', … }`
believed it had configured S3 and got local disk.

- serve no longer reads it. `resolveStorageCapabilityArg` takes only the env
  root, and the production warning names the two channels that work
  (OS_STORAGE_*, Setup → Settings) instead of advertising the one that does not.
- `lintUnknownAuthoringKeys` now covers TOP-LEVEL stack keys, not just object
  and field keys. `storage` gets a prescriptive entry naming both channels and
  why a stack definition is the wrong home for a credential — it would commit it
  to git and to any published artifact. An ordinary misspelling still gets the
  edit-distance suggestion (`datasource` → `datasources`), and the rule now runs
  on a stack with no `objects` at all, which previously exited early.
- `os migrate files-to-references` shares the resolver. It built the same dead
  `{ driver: 'local', root }`, so its adapter used `./storage` while the server
  writes under `.objectstack/data/uploads` since #4096 — and that command
  reconciles what records claim against what storage holds, so a disagreeing
  root reconciled against the wrong tree.

`lintUnknownAuthoringKeys(rawStack)` becomes `(rawStack, stackSchema)`. Required
rather than optional so a caller that forgets it fails to compile instead of
silently losing the check — the exact failure this rule reports. Injected rather
than imported because stack.zod.ts imports this module and importing back would
close a cycle.

Verified end to end: authoring `storage:` through defineStack warns at load
("defineStack: stack.storage: 'storage' is not a declared stack key…"), and
`os compile` reports it for configs that skip defineStack. spec 273 files /
7150 tests, cli 89 files / 916 tests, root `pnpm lint` clean.

Nothing that worked is being removed: `storage` was never in the schema, is
undocumented, and has no consumer in objectstack-ai/cloud (checked directly).

Closes #4167

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 3:34pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling size/m and removed documentation Improvements or additions to documentation protocol:data tests tooling labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/spec.

112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/cli, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/cli, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 4 commits July 30, 2026 14:40
…tack keys too (#4167)

The one page that documents the #3786 rule described it as object/field only,
which #4167 made inaccurate: `lintUnknownAuthoringKeys` now also reports keys
`ObjectStackDefinitionSchema` does not declare.

- Section title and intro name all three schemas.
- The gate table row becomes "Undeclared stack/object/field keys".
- Adds the top-level case with both guidance kinds, using `storage` as the
  worked example — it is the one where the silence is easiest to miss, because
  the key reads as configuration that took effect, and points at the two
  channels that do configure storage (OS_STORAGE_* and the Settings UI, which is
  also where credentials belong — a stack definition is committed to git and
  compiled into any published artifact).

Docs-only; found by acting on the docs-drift advisory rather than waving it
through. `pnpm check:doc-authoring` clean.

Refs #4167

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh
…4167)

`check:api-surface` failed on the #4167 commit: exporting `STACK_KEY_GUIDANCE`
adds to `@objectstack/spec`'s public surface, and the generated snapshot is the
gate's baseline.

    ./data
      + STACK_KEY_GUIDANCE (const)
    @objectstack/spec public API changed: 0 breaking (removed/narrowed), 1 added.

Regenerated with the command the gate names. The diff is one line — the export
is additive, alongside the FIELD_KEY_GUIDANCE / OBJECT_KEY_GUIDANCE entries it
sits with, and nothing was removed or narrowed.

Also ran the rest of the gates CI runs, so this does not take another round:
doc-authoring, role-word, org-identifier, authz-resolver, route-envelope,
error-code-casing, wildcard-fallthrough, release-notes, node-version, plus the
spec package's authorable-surface, spec-changes, upgrade-guide, liveness and
docs checks — all green.

Refs #4167

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh
…gs-visibility-12kkwh

# Conflicts:
#	content/docs/deployment/validating-metadata.mdx
#	packages/spec/src/data/authoring-key-lint.test.ts
#	packages/spec/src/data/authoring-key-lint.ts
#4178 landed while this was in review and refactored the unknown-authoring-key
rule from a two-surface check (object/field) into a walker that derives its
coverage from `PLURAL_TO_SINGULAR` and reads each schema's own unknown-key
posture. This rebases the #4167 contribution onto that architecture instead of
carrying the version it replaced.

What changes vs. the pre-merge shape:

- `lintUnknownAuthoringKeys` keeps its single-argument signature. The top-level
  pass is a separate export, `lintUnknownStackKeys(rawStack, stackSchema)`.
- It reuses the walker's own `keyPosture`, so it lints only while the stack
  schema STRIPS unknown keys and goes quiet if that schema is ever made strict —
  the lint can never become a second, disagreeing voice next to the parse.
- `STACK_KEY_GUIDANCE` stays in `data/authoring-key-lint.ts` beside the other
  two curated tables, held to the same non-rotting discipline.

The two passes are complementary, not redundant: the walker iterates metadata
COLLECTIONS, so a stack whose only mistake is at the envelope level — no
objects, no pages, nothing to iterate — walks clean and reports nothing. A test
pins exactly that.

Verified end to end after the rebuild: `defineStack` warns at config-load time,
and `os validate` / `os build` report `stack.storage` with its prescription plus
`stack.datasource` with its edit-distance suggestion, alongside the field-level
findings the walker already produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh
@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Jul 30, 2026
claude added 2 commits July 30, 2026 15:20
The top-level coverage moved to `lintUnknownStackKeys` when this was rebuilt on
#4178's walker; the comment still pointed at `lintUnknownAuthoringKeys`, which
no longer does that job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh
Caught by running the new lint against our own shipped examples before
merging: `examples/app-todo` and `examples/app-showcase` both got

  ⚠ stack.onEnable: 'onEnable' is not a declared stack key, so its value is
    dropped at load.

which is false. `onEnable` is a function, so `ObjectStackDefinitionSchema`
cannot declare it and `dist/objectstack.json` cannot carry it — but `AppPlugin`
reads it straight off the authored bundle and calls it at `start()`
(`app-plugin.ts`), and the artifact-boot path grafts it back (#4095). It is the
documented place to register action handlers. "Not declared" and "dropped at
load" are different claims, and this is the one surface where they come apart.

New `STACK_RUNTIME_MEMBERS` names the authored top-level members the runtime
honours off the bundle; `lintUnknownStackKeys` treats them as declared. The
CLI's `GRAFTABLE_RUNTIME_MEMBERS` is now DERIVED from it instead of restating
it — two hand-written copies could disagree, and the disagreement would be
silent in exactly the direction this lint family exists to catch.

`onDisable` is deliberately excluded from the exemption: it is declared in the
protocol but no kernel, runtime or service calls it, so a value written there
really does go nowhere. A test pins both halves of that distinction.

Re-harvested after the fix: all three shipped examples validate with zero
undeclared-key warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh
@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 15:46
@os-zhuang
os-zhuang merged commit 6f23667 into main Jul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/plugin-boot-logs-visibility-12kkwh branch July 30, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

config.storage authored with driver/root is silently ignored — a host asking for S3 gets local disk

2 participants