Skip to content

fix(runtime): an explicit sqlite / sqlite-wasm selection refuses a URL that is not a file path, on both hosts - #23896

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23863-explicit-sqlite-refuses-non-file
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23863-explicit-sqlite-refuses-non-file

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23863

Clause-②: no

Under an explicit sqlite or sqlite-wasm driver selection, the one SQLite file-name rule both hosts call (sqliteFilenameFromUrl in @objectstack/runtime) now refuses every value that is not a file path. It refuses an opaque scheme (jdbc:postgresql://…, redis:h) on both kinds, and on the sqlite-wasm kind it also refuses a SCHEME:// URL (postgres://…, redis://h/x.db), as the sqlite kind already did. The refusal names the selected kind and the scheme only, never the URL, and the boot stops before it creates anything on disk.

Why

An explicit selection skips the URL classification (resolveStandaloneDatabase: explicitDriver || detectDriverFromUrl(url)), so the opaque-scheme refusal that PR #23861 added to that classification for #23765 (merged) never sees it. The file-name rule was the only check left, and it recognised only SCHEME://, and only for the sqlite kind. Measured at the public door on base 34b740f2d1 (each boot in a fresh directory outside the repo, OS_HOME pinned inside it):

boot (os serve --dev, plain config) base 34b740f2d1 head c80f0ceaa
OS_DATABASE_DRIVER=sqlite OS_DATABASE_URL=jdbc:postgresql://127.0.0.1:5999/db ✓ Server is ready, Driver: SqlDriver(better-sqlite3) → jdbc:postgresql://127.0.0.1:5999/db, created ./jdbc:postgresql:/127.0.0.1:5999/db plus its .telemetry.db, -shm and -wal siblings exit 1, ✗ [StandaloneStack] sqlite driver was selected but the URL does not look like a file path (its scheme is "jdbc:"). Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly., directory empty
OS_DATABASE_DRIVER=sqlite-wasm OS_DATABASE_URL=postgres://PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:5999/db ✓ Server is ready, created ./postgres:/PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:5999/db (the credentials in a directory name) exit 1, ✗ [StandaloneStack] sqlite-wasm driver was selected but the URL does not look like a file path (its scheme is "postgres://"). …, directory empty, 0 placeholder hits in the log
CLI legacy path, OS_MODE=off OS_DATABASE_DRIVER=sqlite-wasm OS_DATABASE_URL=jdbc:postgresql://127.0.0.1:5999/db not run on base exit 1, ✗ sqlite-wasm driver was selected but the URL does not look like a file path (its scheme is "jdbc:"). … (the typed UnsupportedDriverError, untagged), directory empty

Changes

  • packages/runtime/src/standalone-stack.ts
    • schemeOf (new, module-private, no new export): the opaque-scheme expression PR fix(runtime): the standalone host refuses an opaque-scheme OS_DATABASE_URL, and a .wasm.db ending no longer rescues an unrecognised scheme #23861 added inline to detectDriverFromUrl (urlSchemeOf(dbUrl) ?? /^[a-z][a-z0-9+.-]+:/i.exec(dbUrl)?.[0]), now one helper. detectDriverFromUrl calls it in place of the inline expression; nothing else in that function changed.
    • sqliteFilenameFromUrl: both kinds strip their prefixes as before, then ask schemeOf of a value the strip left as given, and urlSchemeOf of what a file: (or wasm-sqlite:) strip left. So file:backup:2026.db still names backup:2026.db. One sentence for both kinds, naming the kind; the sqlite kind's words are unchanged. An empty name is still :memory: on the WASM kind.
    • Its docblock now states the refusal, and it corrects the claim that every caller passes a normalized URL (see Acceptance notes).
  • packages/cli/src/utils/storage-driver.ts: sqliteFileFor's docblock only. Its "Two inputs reach that" sentence was no longer true. No CLI code change: the CLI host already calls the runtime rule and re-raises its refusal as UnsupportedDriverError.
  • packages/runtime/src/standalone-stack-explicit-sqlite-non-file-refusal.test.ts (new, 80 cases): both kinds through both selection doors (databaseDriver + databaseUrl, OS_DATABASE_DRIVER + OS_DATABASE_URL) and through the exported rule itself. The refused rows assert a plain Error naming the kind and the scheme, and that no own property of the error carries PLACEHOLDER-USER / PLACEHOLDER-SECRET / PLACEHOLDER-TOKEN, with a positive control that the serialisation holds the message. The boot (createStandaloneStack) is refused in an empty cwd and leaves it empty. CONTROL rows, the one changed file name and the un-normalized rungs (Acceptance notes) are pinned too.
  • packages/cli/src/utils/driver-vocabulary-parity.test.ts: explicit-driver rows under both kinds. Both hosts refuse the same values in dev AND prod, with the CLI host's message equal to the runtime host's minus its [StandaloneStack] tag, and driverType equal to the kind. Placeholder credentials are absent from every own property of both errors. Every CONTROL value names the same file on both hosts. The rows of the classification path (PR fix(runtime): the standalone host refuses an opaque-scheme OS_DATABASE_URL, and a .wasm.db ending no longer rescues an unrecognised scheme #23861) are untouched.
  • .changeset/23863-explicit-sqlite-non-file-refusal.md: @objectstack/runtime patch, Clause-②: no. It names the one file name that changes answer under an explicit selection: backup:2026.db is now refused, and ./backup:2026.db and file:backup:2026.db still name it.

Verification (head c80f0ceaa)

  • Build: pnpm turbo run build --filter=@objectstack/cli... --concurrency=2 → 61/61 tasks, exit 0 (the CLI's dependency closure plus the CLI itself).
  • Both hosts through the built dist, under each kind, in dev and prod (resolveStorageDefinition vs resolveStandaloneDatabase). Both refuse jdbc:postgresql://127.0.0.1:5999/db (jdbc:), redis:h (redis:), redis://h/x.db (redis://), postgres://PLACEHOLDER-USER:PLACEHOLDER-SECRET@… (postgres://) and backup:2026.db (backup:). Both open file:./x.db, ./x.db, C:\data\x.db, C:/data/x.db, :memory:, file:backup:2026.db, ./backup:2026.db, sqlite:./x.db and sqlite:///abs/x.db as the same file. Under sqlite-wasm, both open wasm-sqlite:///tmp/x.db as /tmp/x.db. On base, both hosts opened every refused value as a file under sqlite-wasm, and every opaque one under sqlite too.
  • pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 with the new file, standalone-stack-opaque-scheme-refusal.test.ts and standalone-stack-url-refusal-credentials.test.ts → 3 files, 157 passed, exit 0.
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 with driver-vocabulary-parity.test.ts and storage-driver.test.ts → 2 files, 204 passed, exit 0.
  • pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 (the runtime's whole local project) → 402 files, 6121 passed, 19 skipped, exit 0.
  • pnpm --filter @objectstack/runtime typecheck → exit 0. pnpm --filter @objectstack/cli typecheck → exit 0.
  • pnpm exec eslint --no-inline-config over the four changed source and test files → exit 0.
  • Gate families: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 65 commands. 63 exited 0. pnpm check:dual-build-cjs-loads and pnpm check:i18n-coverage exited 3 (PREREQUISITE NOT MET: packages outside the CLI closure, such as connector-slack, have no dist/ in this worktree), so they are NOT MEASURED locally and left to CI. --ran reconciliation: 65 derived, 63 run, 2 NOT-MEASURED, 0 UNRUN. The repo-wide lint run is CI's Lint & Repo Gates.

Ablation (each new refusal once; the CLI suite resolves @objectstack/runtime through dist/, so every leg rebuilt it)

Each mutation went through scripts/ablation-replace.mjs: the anchor hit once, the blob changed, and the restore was proven by blob == HEAD with an empty git diff HEAD. Each leg rebuilt the runtime and ran scripts/ablation-dist-preflight.mjs.

leg mutation dist/ new runtime file (80) CLI parity (130)
A1, opaque scheme filename === dbUrl ? schemeOf(dbUrl) : urlSchemeOf(filename) → urlSchemeOf(filename) marker absent (was 1) 25 red: every opaque row on both kinds and both doors, the exported-rule rows, backup:2026.db, the sqlite: boundary rows, and the sqlite boot over jdbc:. SCHEME:// rows and CONTROL green 8 red: the opaque rows on both kinds. The rest green
A1 restore — marker present, 1 80 green 130 green
A2, WASM kind refuses if (!filename || scheme) → if (!filename || (scheme && kind.length === 6)) marker present, 1 20 red: every sqlite-wasm refusal row (opaque and SCHEME://), the sqlite-wasm boot, and the sqlite-wasm boundary rows. All sqlite rows and CONTROL green 6 red: the sqlite-wasm rows. The rest green
A2 restore — marker absent, 0 80 green 130 green

After both legs: file blob f3f84355f733 == HEAD, git status --porcelain empty.

Acceptance notes

  • Mechanism assumption 1 (every caller passes a normalized URL) is false for two rungs, and this PR does not normalize them. resolveProjectDatabaseUrl applies normalizeDatabaseUrl on the explicit and OS_DATABASE_URL / DATABASE_URL rungs. A declared default datasource's url (non-SQLite drivers only; a declared sqlite datasource is always expressed as file:) and TURSO_DATABASE_URL reach the rule as written. Under an explicit selection there, sqlite:./x.db is now refused as the scheme sqlite:. On base it opened a file named sqlite:./x.db, which meant a sqlite:. directory. sqlite:///… was already refused there under sqlite, and is now refused under sqlite-wasm too. The one-line alternative, calling normalizeDatabaseUrl inside sqliteFilenameFromUrl, would have made sqlite:///abs/x.db open on those rungs where it is refused today. That widens the accept set, against this card's ruled Clause-②: no, so the boundary is pinned instead (the boundary: rungs that do not normalize the URL). The classification path answers these values the same way since PR fix(runtime): the standalone host refuses an opaque-scheme OS_DATABASE_URL, and a .wasm.db ending no longer rescues an unrecognised scheme #23861, whose own Acceptance notes record the un-normalized declared-datasource tier ("No in-repo producer writes it"). No producer was found here either.
  • Mechanism assumptions 2 and 3 held. The CLI host follows the export in dev and prod (table above), and both hosts refuse before any directory is made (door rows above; the boot pin runs createStandaloneStack in an empty cwd).
  • The refusal's remedy clause, unchanged by ruling, reads oddly under an explicit selection. It says "or set OS_DATABASE_DRIVER explicitly" to an operator who just did. Reported to the seat, not changed here.
  • main moved after the merge. Six commits landed past c5cf546a6 (spec, plugin-audit, docs, CI header). None touches standalone-stack.ts, resolve-project-database.ts or packages/cli/src/utils/. PR CI runs on the merge ref.

Generated by Claude Code

… that is not a file path

The rows go red on 34b740f, where an explicit selection opens
`jdbc:postgresql://…`, `redis:h` and (on sqlite-wasm) a `postgres://…`
server URL as a SQLite file at a path made of the URL.

Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT
Co-authored-by: Claude <noreply@anthropic.com>
…L that is not a file path

`sqliteFilenameFromUrl` is the only check an explicit selection's URL meets,
and it refused only a `SCHEME://` URL on the `sqlite` kind. Both kinds now
refuse an opaque scheme (`jdbc:postgresql://…`, `redis:h`) on the value as
given, and the `sqlite-wasm` kind refuses a `SCHEME://` URL too. A value whose
`file:` (or `wasm-sqlite:`) prefix the rule strips stays a file, so
`file:backup:2026.db` still names `backup:2026.db`. One sentence for both
kinds, naming the kind and the scheme only.

The opaque-scheme expression `detectDriverFromUrl` gained in the merged
opaque-scheme fix is now one module-private helper, `schemeOf`, that both
functions call.

Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT
Co-authored-by: Claude <noreply@anthropic.com>
…te / sqlite-wasm selection; changeset

Explicit-driver rows in the driver vocabulary parity pin: an opaque scheme,
an unrecognised SCHEME:// and a server URL are refused by both hosts under
both kinds with one sentence (the CLI host's typed and untagged), no own
property of either error carries the placeholder credentials, and every file
value names the same file on both hosts.

Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/cli/src/utils/storage-driver.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/cli/src/utils/storage-driver.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 45 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 497518c3f67d8cc022fc6663d2e08f97117298a4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7d9853680114d6a9a6e0742ec503a0b7f26fdfa9 — the merge of head c80f0ceaaa8d9f7d1fb3982a386d97fd48dc23eb into base 497518c3f67d8cc022fc6663d2e08f97117298a4, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7d9853680114d6a9a6e0742ec503a0b7f26fdfa9 && git checkout 7d9853680114d6a9a6e0742ec503a0b7f26fdfa9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 497518c3f67d8cc022fc6663d2e08f97117298a4 c80f0ceaaa8d9f7d1fb3982a386d97fd48dc23eb && git checkout -B drift-repro 497518c3f67d8cc022fc6663d2e08f97117298a4 && git merge --no-ff c80f0ceaaa8d9f7d1fb3982a386d97fd48dc23eb

node scripts/docs-audit/affected-docs.mjs --json 497518c3f67d8cc022fc6663d2e08f97117298a4

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #23863 · domain:cli seat 2 · os-tesla · session session_018t6a5rvqPRvEDMCVVW9kYT · 2026-10-16T12:24Z · reviewed head c80f0ceaaa8d9f7d1fb3982a386d97fd48dc23eb, against GitHub and the branch, not against the report's prose. Holder of claim 6213923568; report 6215053592 on #23863.

  1. Shape. Draft, base main, assignee os-tesla. Line 1 is Fixes #23863, the one closing keyword; line 3 is Clause-②: no. 5 files, +447 / −22. check-governed-merges --pr 23896: NOT governed (0 of 5 paths). git merge-tree is clean against origin/main. PR fix(runtime): the standalone host refuses an opaque-scheme OS_DATABASE_URL, and a .wasm.db ending no longer rescues an unrecognised scheme #23861 merged mid-run; origin/main was merged in with a real merge commit (d57f61537), as the dispatch's merged branch ordered.
  2. The rule, read in the diff.
    • One module-private helper, schemeOf (a SCHEME:// or an opaque scheme of two or more characters), now answers for both detectDriverFromUrl (its inline expression from PR fix(runtime): the standalone host refuses an opaque-scheme OS_DATABASE_URL, and a .wasm.db ending no longer rescues an unrecognised scheme #23861, replaced by the call) and sqliteFilenameFromUrl. No new export.
    • sqliteFilenameFromUrl strips file: (and wasm-sqlite: on the WASM kind) on both kinds, then refuses: an opaque scheme on the value as given (filename === dbUrl), or a SCHEME:// left after the strip. So file:backup:2026.db still names backup:2026.db.
    • An empty WASM name is still :memory:. On the sqlite kind, an empty file: is still refused.
    • The sentence now names the selected kind. For the sqlite kind its words are unchanged.
    • The classification path gains no refusal: detectDriverFromUrl hands this rule only file: values and values with no scheme.
  3. Measured at the door, base 34b740f2d1 against head c80f0ceaa, in a scratch directory outside the repo.
    • Base: OS_DATABASE_DRIVER=sqlite beside jdbc:postgresql://… booted ready and created ./jdbc:postgresql:/…. sqlite-wasm beside postgres://PLACEHOLDER-USER:PLACEHOLDER-SECRET@… created a directory carrying the placeholders.
    • Head: both exit 1 with the scheme-only sentence and create nothing.
    • The built dist of both hosts agrees on every pinned value, in dev and prod.
  4. Pins.
    • The new runtime file holds 80 cases over both kinds and the three doors, the CONTROL rows included (file:./x.db, ./x.db, C:\data\x.db, C:/data/x.db, :memory:, file:backup:2026.db, ./backup:2026.db, wasm-sqlite:///tmp/x.db, an empty WASM name).
    • The parity file's explicit-driver block holds both hosts to one sentence. The CLI host's error is UnsupportedDriverError, recognized: true, with the runtime's sentence minus its tag. A placeholder credential is in no own property of either error, and a positive control proves the serialisation holds the message.
    • Two ablations, each on a rebuilt dist. Removing the opaque refusal turned 25 runtime and 8 parity cases red. Removing the WASM refusal turned 20 runtime and 6 parity cases red. CONTROL stayed green in both, and the restore was proven by blob.
  5. Changeset, sentence by sentence.
    • @objectstack/runtime patch, with Clause-②: no. The before / after bullets match the diff and the door readings.
    • The backup:2026.db bullet names the one file name that changes answer, and both spellings that still reach it, as the claim's Basis line promised.
    • The sqlite:-alias bullet states the boundary the dev pinned instead of normalizing (see 7).
  6. Contract review: none is owed. The paths decide no face, and the changeset's Clause-②: no carries no arm, so record-recognisers --paths reads no face; this seat's read is the review. The Basis line on the claim cites the published text these refusals already state (the module header's "Unknown URL schemes throw — we never silently fall back to sqlite", the sqlite kind's "does not look like a file path"), the reading PR fix(runtime): the standalone host refuses an opaque-scheme OS_DATABASE_URL, and a .wasm.db ending no longer rescues an unrecognised scheme #23861 landed on.
  7. Accepted deviations.
    • schemeOf and the one changed line in detectDriverFromUrl (the dispatch's merged branch).
    • The parity file's stale "Two inputs reach it" comment was reworded.
    • Mechanism assumption 1 was falsified in part, and the dev did not normalize. A declared datasource's url and TURSO_DATABASE_URL reach the rule as written. There, under an explicit selection, sqlite:./x.db is now refused as the scheme sqlite:; before, it opened a file named sqlite:./x.db. Normalizing inside the rule would have opened sqlite:///… where it is refused today, a widening against the ruled Clause-②: no. So the boundary is pinned and named in the changeset, which this seat accepts. No in-repo producer writes that spelling (PR fix(runtime): the standalone host refuses an opaque-scheme OS_DATABASE_URL, and a .wasm.db ending no longer rescues an unrecognised scheme #23861's reading).
    • Three pushes, and one lock queue-timeout re-queued.
  8. Gates, pinned to the head. Check-runs on c80f0ceaa read at this stamp, latest run per name: 40 names, 34 success, 6 skipped, 0 failure, nothing running. The successes include Build Core, Test Core 1–6/6, TypeScript Type Check and the four Type Check · lanes, all eight Lint · lanes (which run check:dual-build-cjs-loads and check:i18n-coverage), Lint & Repo Gates, Check Changeset, Spec property liveness and Governed Surface Queue Guard. The 6 skips are roster names: Build Docs, Console Pin Gate, Dogfood Regression Gate (matrix), Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance. The dev's local subset at this head: 63 of 65 derived gate commands exit 0, and 2 are NOT MEASURED (check:dual-build-cjs-loads, check:i18n-coverage, prerequisite: packages outside the CLI closure without dist; CI's Lint lanes run both). @objectstack/runtime local 402 files / 6121 tests. The CLI's parity and storage-driver unit files: 204 tests. Both typechecks exit 0.

Out of scope, filed by this seat: the refusal ends "…, or set OS_DATABASE_DRIVER explicitly." to an operator who did set it (finding 1, measured at the door). It is older than this card on the sqlite kind, and this card's ruling kept those words. It is filed as #23898, ⛔ not a claim, for triage to grade.

Landing: once every check on the head is green or an expected skip, this seat flips the PR ready and arms auto-merge through the relay, then follows it to MERGED.


Generated by Claude Code

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 size/m tests tooling

Projects

None yet

2 participants