Skip to content

feat(spec,driver-turso)!: retire turso config.encryptionKey, which never encrypted anything - #25153

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-7afd2e
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-7afd2e

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

Fixes #25050

Clause-②: no (narrowing)

Metadata-protocol: narrowed

What changes

BREAKING. Turso config.encryptionKey is removed (Route A, retire the key, ADR-0049 enforce-or-remove). It never encrypted anything, in any mode:

After #25020 the key still parsed on a remote datasource, did nothing there, and was the last credential-shaped key a datasource could write in plain text.

Surface Before After
@objectstack/spec TursoConfigSchema.encryptionKey accepted beside a remote url; refused beside a local url (#25020) retiredKey() tombstone: a tsc error, and a parse error at config.encryptionKey in every mode with any value ('' included), carrying the prescription. It is the only issue reported on that config. encryption_key / encryptionkey get the same prescription
@objectstack/driver-turso mirror TursoConfigSchema same split z.never() with the same prescription text, byte for byte
new TursoDriver() / createTursoDriver() refused local / replica / forced-remote-on-a-file only TursoDriverConfig.encryptionKey?: never. A defined value is refused with VALIDATION_ERROR / 400 as the constructor's first check, in every mode
@objectstack/service-datasource TursoDriverConfigInput.encryptionKey string never. The shared loader still forwards a declared value, so a legacy stored row reaches the constructor's refusal and does not boot in silence
redaction (datasource-credential-redaction.ts) STILL_WRITABLE_CREDENTIAL_KEYS = { turso: ['encryptionKey'] } RETIRED_CREDENTIAL_KEYS, same content: a stored legacy value is still withheld on read and named in redactedConfigKeys

FROM → TO

You wrote Write instead
url: 'libsql://my-db.turso.io', encryptionKey: '…' (also https://, ws(s)://) delete encryptionKey. A remote database's storage encryption is configured on the Turso server
encryptionKey: '' (or any unset value) delete encryptionKey
url: 'file:./data/app.db', encryptionKey: '…' (also :memory:, or a replica beside syncUrl) delete encryptionKey, and put the file on an encrypted disk or volume if it must be encrypted at rest

The one-line fix: delete encryptionKey from every turso datasource.

ADR-0087 disposition

  • D2 turso-config-encryption-key-removed (retiredFromLoadPath: true, retiredAfter: '18.0.0') deletes the key when its value is unset (any non-string, or '') or when it sits beside a remote url (libsql://, https://, http://, ws://, wss://, in any case, url trimmed). It emits one notice per key.
  • A non-empty key beside a local url is left for the author. The row keeps the turso: a local-mode datasource's declared encryptionKey is never applied, so the database file is written in plaintext that any SQLite reader opens without the key #25020 posture: it fails at driver construction with the prescription.
  • D3 turso-config-encryption-key-retired. RETIRED_KEYS_BY_MAJOR[18] gains data/TursoConfig:encryptionKey. The STEP18_RATIONALE fragment has order 107.
  • The changeset .changeset/25050-retire-turso-encryption-key.md carries the one marker, both declaration lines, BREAKING, FROM → TO and the one-line fix. Grades: @objectstack/spec minor, @objectstack/driver-turso minor, @objectstack/service-datasource minor.

Decision: the partial conversion (four axes)

The question was whether the D2 conversion deletes every stored encryptionKey, or only the ones it can prove were inert.

  • Real business need: an operator who wrote a key beside a local file believed the file was encrypted. A conversion that silently deletes that key lets the datasource boot in plaintext with no signal, which is the defect turso: a local-mode datasource's declared encryptionKey is never applied, so the database file is written in plaintext that any SQLite reader opens without the key #25020 made loud. Beside a remote url, or when the value is unset, nothing was ever believed and nothing changes.
  • Long-term soundness: one rule ("a key the driver never read, with nothing believed behind it") is mechanical and lossless. The local case needs a human decision (an encrypted volume, or a remote database), so it stays a refusal that names the fix.
  • Preventing AI metadata errors: the tombstone, the guidance spellings, and the constructor refusal all carry the same prescription. An agent that writes the key in any spelling or mode is told to delete it. There is no accepted-but-inert state left to learn from.
  • Startup no-sprawl: one conversion, one D3 entry, one retired-key entry, one renamed internal constant. There are no new options or modes, and the loader keeps its single forwarding reader.

Measured assumptions (one disproved)

  • Disproved: "an existing path still redacts a stored encryptionKey once it is tombstoned." It does not. The schema-derived credential list excludes retiredKey() tombstones by their [REMOVED] prefix ([#14478 stack 5/6] data/ · ui/ · ai/ · integration/: the 7 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers #15680), and the derived turso list (authToken) is non-empty, so the canonical fallback never applies.
    • Without a fix, a legacy row's value would have been served on read.
    • Fixed with the retired-key reader RETIRED_CREDENTIAL_KEYS (the renamed STILL_WRITABLE_CREDENTIAL_KEYS).
    • Proven by datasource-credential-redaction.test.ts ("a RETIRED credential tombstone … is excluded from the derivation, yet still redacted") and by the service-datasource admin-door test on a local-url legacy row.
  • Held: zero in-repo producers. No example, template, scaffold, skill, host default or env var authors the key. A tree-scoped absence pin (turso-encryption-key-retirement.test.ts, radius packages examples skills content scripts) now holds it, with every exclusion named.
  • Re-homed fixtures: 15 stored-metadata redaction suites used encryptionKey as their sentinel credential. 14 now use turso authToken, which is refused inline and redacted. The runtime reader-contexts pin seeds through the production writer, which refuses both primary slots, so it now uses mongodb options.proxyPassword, which is writable and redacted. The metadata-protocol legacy-row redaction test keeps encryptionKey on purpose, as a real retired-key row.

Reverse verification (once, no permanent test)

Run with scripts/ablation-replace.mjs on the committed head. Each mutation landed (anchor 1 → 0, blob moved) and was restored (blob equals HEAD, git diff HEAD empty).

  • Constructor refusal removed (refuseIgnoredSyncKey(ENCRYPTION_KEY_RETIRED_REFUSAL)): driver-turso turso-driver-encryption-key-retired.test.ts and turso-config-constructor-parity.test.ts went red, 36 failed across both files.
  • Retired-key reader emptied (turso: []): spec datasource-credential-redaction.test.ts and metadata-type-redaction.test.ts went red, 7 failed.

Verification

  • @objectstack/spec: build 0, pnpm test (after fixing the ledger total, which the first run caught) 0, typecheck 0. The repo-project absence pin passes, 10 tests.
  • @objectstack/driver-turso: build 0, pnpm test 0 (after measuring the SYNC_KEY_REFUSALS floor at 20), typecheck 0.
  • @objectstack/service-datasource: build 0, pnpm test 0, typecheck 0.
  • @objectstack/cli: typecheck 0. The three touched test files pass. One storage-driver.test.ts case timed out at 5 s under a shared-box batch, then passed in isolation (225 passed).
  • Touched suites in runtime, objectql, mcp, metadata-protocol, plugin-audit and service-automation: all pass.
  • dispatch-gates --commands on this diff (head a15132cc4): all 127 derived commands run, exit codes recorded before any pipe. --ran reports 127 run, 0 NOT-MEASURED, 0 UNRUN.
    • 5 of the 127 first refused on environment prerequisites, not findings: a shallow clone (check-engine-split-ratio exit 2, check-plugin-teardown-shape --self-test exit 3) and dist output that was not built (check:skill-examples, check:dual-build-cjs-loads, check:i18n-coverage, each exit 3).
    • Each passed (exit 0) after git fetch --shallow-since and a full pnpm build.
  • Dogfood pnpm --filter @objectstack/dogfood test on a15132cc4, lock-wrapped: lock VERDICT command-exit 0. Test Files 287 passed, 3 skipped (290). Tests 2395 passed, 18 skipped (2413).

The contract-review-tier review belongs to the seat. This PR stays a draft.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-turso, @objectstack/service-datasource, @objectstack/spec, touching 33 documentable anchor(s). ⚠️ 8 changed file(s) yielded no anchor (packages/drivers/driver-turso/README.md, packages/spec/authorable-surface/data.json, packages/spec/dropped-refinements.baseline.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/automation/hook-bodies.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/automation/jobs.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/automation/webhooks.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/data-modeling/drivers.mdx (via encryptionKey (symbol, a field of const object TURSO_CONFIG_READERS; a field of interface TursoDriverConfig; a field of interface TursoDriverConfigInput; a field of interface TursoTransportKeys), encryptionKey (literal, a string literal in TursoConfigSchema; a string literal in TursoTransportIssue; a string literal in apply; a string literal in tursoTransportIssues))
  • content/docs/deployment/environment-variables.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/protocol/kernel/lifecycle.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))

⛔ 8 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/releases/v17/17-0.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/releases/v17/17-1.mdx (via encryptionKey (symbol, a field of const object TURSO_CONFIG_READERS; a field of interface TursoDriverConfig; a field of interface TursoDriverConfigInput; a field of interface TursoTransportKeys), encryptionKey (literal, a string literal in TursoConfigSchema; a string literal in TursoTransportIssue; a string literal in apply; a string literal in tursoTransportIssues))
  • content/docs/releases/v17/17-4.mdx (via TursoDriverConfig (symbol, a top-level interface), timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/releases/v17/17-5.mdx (via retiredAfter (symbol, a field of const object tursoConfigEncryptionKeyRemoved), retiredFromLoadPath (symbol, a field of const object tursoConfigEncryptionKeyRemoved), syncUrl (literal, a string literal in TursoTransportIssue), timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/releases/v17/17-6.mdx (via syncUrl (literal, a string literal in TursoTransportIssue))
  • content/docs/releases/v17/17-7.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/releases/v18/18-0.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 8 changed file(s) yielded no anchor (packages/drivers/driver-turso/README.md, packages/spec/authorable-surface/data.json, packages/spec/dropped-refinements.baseline.json, …) — pages documenting those are invisible to this run
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 147 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 37ccd5f2cf1daf02d16772e10182698da0bbeb4d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7be84dfaf4d46e640076cb1664d9ad732d35a574 — the merge of head a15132cc4c85f93cd298cf9b1daba5bf95cdb092 into base 37ccd5f2cf1daf02d16772e10182698da0bbeb4d, 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 7be84dfaf4d46e640076cb1664d9ad732d35a574 && git checkout 7be84dfaf4d46e640076cb1664d9ad732d35a574
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 37ccd5f2cf1daf02d16772e10182698da0bbeb4d a15132cc4c85f93cd298cf9b1daba5bf95cdb092 && git checkout -B drift-repro 37ccd5f2cf1daf02d16772e10182698da0bbeb4d && git merge --no-ff a15132cc4c85f93cd298cf9b1daba5bf95cdb092

node scripts/docs-audit/affected-docs.mjs --json 37ccd5f2cf1daf02d16772e10182698da0bbeb4d

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 37ccd5f2cf1daf02d16772e10182698da0bbeb4d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a15132cc4c85f93cd298cf9b1daba5bf95cdb092
Local-runs: none
Scope: packages/spec/src/conversions/registry.ts, packages/spec/src/data/datasource-credential-redaction.ts, packages/spec/src/data/driver/common.zod.ts, packages/spec/src/data/driver/turso.zod.ts, packages/spec/src/kernel/metadata-type-redaction.ts, packages/spec/src/migrations/entries/retired-keys/18.data__TursoConfig__encryptionKey.ts, packages/spec/src/migrations/entries/semantic/18.turso-config-encryption-key-retired.ts, packages/spec/src/migrations/registry.ts.template + every other path of the diff outside the prose faces (the narrowing face's floor): packages/spec/authorable-surface/data.json, packages/spec/dropped-refinements.baseline.json, packages/spec/vitest.repo-tests.json, packages/spec/src/data/datasource-credential-redaction.test.ts, packages/spec/src/data/driver/driver-placeholder-refusal.test.ts, packages/spec/src/data/driver/turso-encryption-key-retirement.test.ts, packages/spec/src/data/driver/turso.test.ts, packages/spec/src/kernel/metadata-type-redaction.test.ts, packages/spec/src/shared/retired-key-removal-version.pin.test.ts, packages/drivers/driver-turso/src/turso-driver.ts, packages/drivers/driver-turso/src/spec/turso.zod.ts, packages/drivers/driver-turso/src/spec/turso-config-constructor-parity.test.ts, packages/drivers/driver-turso/src/spec/turso.test.ts, packages/drivers/driver-turso/src/turso-driver-encryption-key-retired.test.ts, packages/drivers/driver-turso/src/turso-driver-local-encryption-key-refusal.test.ts (deleted), packages/drivers/driver-turso/src/turso-driver-forced-local-with-sync-url-refusal.test.ts, packages/drivers/driver-turso/src/turso-driver-forced-replica-without-sync-url-refusal.test.ts, packages/services/service-datasource/src/turso-driver-config.ts, packages/services/service-datasource/src/datasource-credential-migration.ts, packages/services/service-datasource/src/tests/.test.ts (4), packages/cli/src/**/.test.ts (3), packages/mcp/src/stdio-data-bridge.stored-metadata-body.test.ts, packages/metadata-protocol/src/.test.ts (3), packages/objectql/src/.test.ts (3), packages/plugins/plugin-audit/src/.test.ts (2), packages/runtime/src/.test.ts (4), packages/services/service-automation/src/builtin/.integration.test.ts (2); cited code read at the head: packages/spec/src/shared/retired-key.ts, packages/spec/src/shared/retired-key-migrate-sentence.test.ts, packages/spec/src/kernel/protocol-version.ts, packages/spec/src/conversions/retired-after.census.json, packages/spec/src/migrations/entries/README.md, packages/spec/authorable-surface.base.json, packages/spec/liveness/.json, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs. The prose faces (.changeset/** body prose, content/docs/**, docs/**, packages/drivers/driver-turso/README.md) are the lane seat's ACCEPT at TIER_DEFAULT; the changeset's declaration lines and marker are judged in ② below.

Inputs: card #25050 (body, the triage unlock 6313284617, the claim 6328120971, the dev report 6330083449), PR #25153 (body, 55-file list, the net diff against main), and the check-runs on the head. Not the dispatching seat's conclusions.

① Derived judgments

Every accept-set and public-surface change the diff implies, named right or wrong:

  1. @objectstack/spec TursoConfigSchema.encryptionKey: placeholderFree(z.string()).optional() ⇒ retiredKey(TURSO_ENCRYPTION_KEY_RETIRED). RIGHT. Accept set narrows to absence in every mode and for every value ('', null, 0, {} included); z.input<typeof TursoConfigSchema> (TursoConfig) narrows the key to the tombstone mark, so a typed authoring site is a tsc error; the parse raises the prescription at encryptionKey as the only issue, ahead of every transport arm (the strict shape fails before the refinement runs, matching the constructor's "first" posture). Pinned in turso.test.ts (nine mode/url rows, seven values, five compound-defect rows, both authoring doors, the describe, the tsc channel).
  2. The turso: a local-mode datasource's declared encryptionKey is never applied, so the database file is written in plaintext that any SQLite reader opens without the key #25020 refusal arms leave tursoTransportIssues (spec and mirror) and TursoTransportKeys.encryptionKey goes. RIGHT: subsumed by the tombstone; the sibling transport arms are byte-untouched.
  3. Guidance rows encryption_key / encryptionkey carry the same prescription. RIGHT: a tombstoned key leaves the unknown-key probe's candidate list (acceptsNothing), exactly the auth_token/authtoken precedent on the same shape; pinned (unrecognized_keys with the prescription).
  4. Prescription text. RIGHT on every house convention: backticked fully-qualified key first; was removed in @objectstack/spec 18 and @objectstack/driver-turso 18 (ADR-0049); the inertness clause; the imperative fix with the real mechanism (encrypted disk or volume); the MIXED two-clause os migrate meta --from 17 sentence, correct because the D2 conversion is partial. The release is derived right: spec label 18.0.0 (stable), retiredAfter: '18.0.0' ≥ newest recorded stable ⇒ PROTOCOL_MAJOR = 18 (protocol-version.ts), and --from 17 reaches the toMajor: 18 step; the new retired-key-removal-version.pin.test.ts row joins it. No issue number and no url in the text.
  5. D2 conversion turso-config-encryption-key-removed (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '18.0.0', order: 74, unique in MAJOR_18_CONVERSIONS). RIGHT. Driver-aware through resolveDriverId (catches libsql), url trimmed and prefix-matched case-insensitively against the same five remote prefixes turso.zod.ts carries, strips only an unset value (non-string or '') or a key beside a remote url, one notice per key, idempotent; fixture has six rows and expectedNotices: 3 = the three keys stripped. The partial posture is the right one on the four axes the PR argues: a non-empty key beside a local url is intent no artifact records, and stripping it would turn turso: a local-mode datasource's declared encryptionKey is never applied, so the database file is written in plaintext that any SQLite reader opens without the key #25020's loud construction refusal back into a plaintext boot in silence. Retired from the load path ⇒ a live author meets the tombstone, never a rewrite; stored rows replay it at loadDatasourceRows / loadDatasourceRow (ADR-0087 addendum).
  6. D3 turso-config-encryption-key-retired, RETIRED_KEYS_BY_MAJOR[18] += data/TursoConfig:encryptionKey (entry file named per entries/README.md, generated into the gitignored registry.ts), STEP18_RATIONALE fragment order: 107 (unique). RIGHT; surface backtick-free as the template requires; the D3 reason names the D2 id and the local-url residue, pinned.
  7. authorable-surface/data.json: data/TursoConfig:encryptionKey ⇒ … [RETIRED]; dropped-refinements.baseline.json: the encryptionKey site leaves data/TursoConfig and the total 812 ⇒ 811. RIGHT: the placeholderFree refinement on the key is gone with the key; publishedSchemasWithDroppedRefinements stays 240 because the shape keeps "", syncUrl, url. authorable-surface.base.json is correctly untouched (a baseRev snapshot written only by gen:schema, never by a PR). The four ratchets (api-surface, api-surface-signatures, json-schema.manifest, authorable-surface) carry no property-level row for this key, so their zero change is the expected reading for a tombstone route (a property retirement, not a def removal).
  8. STILL_WRITABLE_CREDENTIAL_KEYS ⇒ RETIRED_CREDENTIAL_KEYS, same content, module-local (never exported, before or after). RIGHT, and necessary: refusedCredentialKeys excludes a z.never() whose description starts [REMOVED] (the [#14478 stack 5/6] data/ · ui/ · ai/ · integration/: the 7 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers #15680 exclusion, which retiredKey() stamps), and turso's derived list is ['authToken'] (non-empty), so the CANONICAL_CREDENTIAL_KEYS fallback never applies — without the renamed list a legacy stored value would be served on read. The public reading redactableConfigKeys('turso' | 'libsql') still contains encryptionKey; redactDatasourceConfig still withholds and names it; pinned in spec (datasource-credential-redaction.test.ts, the new tombstone-yet-redacted case) and at the admin door (datasource-config-redaction.test.ts, moved to a local-url row because the load-path conversion strips a remote-url one first — the right fixture). The PM assumption the dev disproved is disproved correctly.
  9. @objectstack/driver-turso mirror TursoConfigSchema.encryptionKey ⇒ z.never({ error: () => ENCRYPTION_KEY_RETIRED }).optional().describe('[REMOVED] ' + …). RIGHT: structurally what retiredKey() returns, text byte-equal to the spec's; turso-config-constructor-parity.test.ts holds the three copies equal across 16 RETIRED_KEY_ROWS (five remote-url rows floored), and SYNC_KEY_REFUSALS floor re-measured 38 ⇒ 20 after the rows moved, with the "zero inert rows" assertion kept.
  10. TursoDriverConfig.encryptionKey?: string ⇒ ?: never (exported type, BREAKING); constructor refuses encryptionKey !== undefined as its first check, before detectMode and before super(), as VALIDATION_ERROR / 400 with the spec text; localEncryptionKeyRefusal, remoteModeLocalEncryptionKeyRefusal, namesLocalDatabase deleted. RIGHT. Refuses null and '' too, matching the tombstone; message carries no url or key; no file is opened. The replica arm's createClient still forwards no key. The deleted turso-driver-local-encryption-key-refusal.test.ts is replaced by turso-driver-encryption-key-retired.test.ts (every mode incl. a pre-built client, every value, FIRST ordering, createTursoDriver, the tsc channel, round-trip and remote-mode controls).
  11. @objectstack/service-datasource TursoDriverConfigInput.encryptionKey ⇒ never; the reader forwards any defined value (the ''-is-unset arm dropped); TURSO_DRIVER_CONFIG_KEYS unchanged (still lists the key). RIGHT. Forwarding is the one correct seam: the schema refuses the key at the door and the load path strips the unset and remote cases, so what can still arrive is a non-empty key beside a local url, which must reach the constructor's refusal rather than be dropped (the turso: a local-mode datasource's declared encryptionKey is never applied, so the database file is written in plaintext that any SQLite reader opens without the key #25020 defect). It is a refusal hand-off, not a consumer dialect (Prime Directive Add comprehensive test suite for Zod schema validation #12); the #16023 retired-spelling pin names it as the one exception and holds it to "forwarded unchanged, never read into another key". The two key-set pins (storage-driver.test.ts, turso-driver-factory.convergence.test.ts) filter the key with a stated reason. datasource-credential-migration.ts: wording only; the unbindable set stays derived from redactableConfigKeys minus the bindable slot, so the retired key still lands there.
  12. metadata-type-redaction.ts, common.zod.ts: comment-only. RIGHT.
  13. Fixture re-homing (cli, mcp, metadata-protocol, objectql, plugin-audit, runtime, service-automation): sentinel encryptionKey ⇒ authToken on libsql:// rows; the runtime reader-contexts pin ⇒ mongodb options.proxyPassword. RIGHT in each case: authToken is in the derived redaction set for turso and libsql, so every redaction assertion keeps its meaning; the runtime pin seeds through the production writer, which refuses authToken inline, and options.proxyPassword is in MONGO_OPTIONS_CREDENTIAL_PATHS (writable, redacted). protocol.data-door-stored-metadata-redaction.test.ts keeps encryptionKey beside a file: url on purpose as the real legacy-row case. driver-placeholder-refusal.test.ts drops its turso encryptionKey row with a stated reason (no literal left to judge).
  14. Tree-scoped absence pin (turso-encryption-key-retirement.test.ts, registered in vitest.repo-tests.json). RIGHT: key-position matcher with string/comment masking, YAML and fence handling, anti-vacuity on the matcher and on the walk (visited > 1000, example sources > 50, driver files > 10), every exclusion a file whose job is to spell the key, prefixes limited to the conversion fixtures, release-owned prose and gitignored output. The zero-producer claim the card measured now has a pin.
  15. Liveness ledger. No packages/spec/liveness/*.json row names data/TursoConfig:encryptionKey at the head or before it (driver config shapes carry no ledger rows), so the skill's "keep the tombstone's dead row" step has no row to keep; Spec property liveness is green on this head. No action.

Check-runs on a15132cc4 as read for this record: success — Check Changeset, Governed Surface Queue Guard, Spec property liveness, Build Docs, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Lint & Repo Gates and every Lint · job (ESLint, PM dispatch-gates, docs & code guards, repo & tooling gates, CI/release/engine gates, the three scoped jobs), Type Check · source gates, Dogfood Regression Gate, Auto Label, the four PR-claim guards, filter. Skipped (opt-in / path-filtered) — Console Pin Gate, Temporal Conformance, Dogfood Verify CLI, the sharded Dogfood matrix, 18.x upgrade harness, Packed-tarball smoke. PENDING, not waited for — Build Core, Test Core 1/6 to 6/6, Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace. The dev report records spec, driver-turso, service-datasource build/test/typecheck at 0 and all 127 dispatch-gate commands at exit 0 on this head; the pending runs are the mechanical confirmation of what this record read.

② Semver level

  • Changeset .changeset/25050-retire-turso-encryption-key.md: @objectstack/spec minor, @objectstack/driver-turso minor, @objectstack/service-datasource minor. RIGHT. The diff moves published source in exactly those three packages (every other touched package moves tests only); the kind is Metadata-protocol: narrowed (a key tombstoned, a value refused), which takes at least minor on each moved package; major is refused by the launch-window guard, under which a breaking change ships minor with its BREAKING banner, and the body says so by name.
  • Clause-②: no (narrowing) at line start in the PR body and the changeset; Metadata-protocol: narrowed beside it in both. RIGHT and not malformed; narrowed beside the (narrowing) arm is consistent.
  • Exactly one ADR-0087 marker, registered turso-config-encryption-key-removed, turso-config-encryption-key-retired: both ids are new in this diff and resolve in the registries (the D2 conversion and the D3 semantic entry). RIGHT.
  • BREAKING banner, FROM → TO table (remote url, unset value, local url), the one-line fix, and the stored-rows-stay-redacted section all present; blast radius stated with its limit (out-of-repo deployments not claimed zero). Check Changeset is green on the head.
  • Clause-②: no (narrowing) — the published accept set of TursoConfigSchema, the driver mirror, TursoDriverConfig and TursoDriverConfigInput narrows; nothing widens.

③ Boundary flags

Dev report 6330083449: open_questions: [], out_of_scope_findings: []. Deviations, each answered:

  1. Branch claude/project-thread-7afd2e, not claude/issue-25050-*: named in the claim 6328120971; the card must claim this branch guard is green. Answered.
  2. Model-free commit trailers (Claude-Session + Co-authored-by: Claude): what AGENTS.md and the pre-push hook require. Answered.
  3. Out-of-radius doc edits (docs/qa/platform-checklist/areas/integration-system.json, docs/design/driver-turso.md, content/docs/data-modeling/drivers.mdx): prose faces, stale on the still-writable posture, read consistent with the code (the QA knownGap now expects a refusal everywhere and a planted file:-url legacy row; the five-key writable table drops the turso row). Lane ACCEPT. Answered.
  4. Clone deepened for two history gates: local only, no tree change. Answered.

Card and rulings: Route A (triage's 6313284617 reads it as an 18.x retirement with no staged window; the claim fixed Route A) is carried out item by item — tombstone, z.never() mirror, constructor refusal in every mode, ADR-0087 D2/D3 and retired-key entry, the key out of STILL_WRITABLE_CREDENTIAL_KEYS with a retired-key reader keeping a stored row redacted, fixtures re-homed. The claim's "one changeset (stop on breach)" holds: one changeset. The PM assumption the dev disproved (an existing path redacts a tombstoned stored key) is disproved correctly and the fix is the minimal one. Nothing escalated.

Implemented-by: claude/project-thread-7afd2e
Reviewed-by: session_01B2EeooonSXrANpkpgUSfFZ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:spec seat 2 (#18549) · os-bill · session session_01B2EeooonSXrANpkpgUSfFZ · 2026-10-22T01:28Z · head a15132cc4c85f93cd298cf9b1daba5bf95cdb092

  1. Shape: draft, base main, one closing keyword (Fixes #25050, line 4); no other card number sits beside a closing verb. check-governed-merges.mjs --pr 25153: 0 of 55 paths governed, 2382 changed lines ≤ 3000, so ordinary queue landing. mergeable_state: clean.
  2. Premise measured: the dev re-measured zero producers on main and pinned it with a tree-scoped absence test. It disproved the seat's assumption that an existing path keeps a tombstoned stored key redacted: the tombstone's [REMOVED] prefix excludes it from the derived list, and turso's derived list is non-empty. The rename STILL_WRITABLE_CREDENTIAL_KEYS → RETIRED_CREDENTIAL_KEYS is the minimal reader, proven by the spec and service-datasource redaction tests and by ablation (B). The seat's assumption was wrong; this is the correction of record.
  3. Gates (check-runs on this head): 36 success, 6 skipped, 0 failed or pending. Lint & Repo Gates and TypeScript Type Check are both success. The skipped six are the opt-in or path-scheduled jobs (Console Pin Gate, Temporal Conformance, Dogfood Verify CLI, the Dogfood Regression matrix placeholder, the 18.x upgrade harness, the packed-tarball smoke). Local: 127 of 127 dispatch-gates commands run (--ran exit 0); dogfood on this head, 2395 passed, 18 skipped, 0 failed.
  4. Contract review: PASS at CONTRACT_REVIEW_TIER on this head, record 6330200104 (narrowing face, Local-runs: none).
  5. Accepted deviations: the branch is claude/project-thread-7afd2e as the claim names it; commit trailers are model-free, as the pre-push hook requires; three stale prose docs were corrected (below).
  6. Prose faces (the seat's own read, sentence by sentence): the changeset's BREAKING banner, FROM → TO table, one-line fix, ADR-0087 marker and both declaration lines; drivers.mdx (the writable table drops the turso row, so "six" → "five", and the turso callout states the removal); the regenerated driver-turso.mdx row; docs/design/driver-turso.md; the QA knownGap and step in integration-system.json; the driver README. Each matches the code at this head.
  7. Out of scope: none reported, none found.
  8. Landing: pr_ready + automerge_enable through the relay now. The seat 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 protocol:data size/xl tests tooling

Projects

None yet

2 participants