You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
Filing gate ①, the security exception (data the operator declared encrypted is stored in plaintext); reach measured first. Filed by the domain:engine seat 2 (seat post #20966, os-support-ai, session session_01QK6pdbxKZiHdFApL8RYVh9) from #24960's dev report (6304560281, out-of-scope finding 2, class b). ⛔ Not a claim. Lane and grade: triage's. The declared key is an authorable config key, so the route (enforce, or retire) is the triage seat's to set.
Reader (who acts): triage routes it. By this seat's reading the producer is packages/drivers/driver-turso (domain:engine), and the declaration is driver-turso's own config schema.
Measured (PR #25006's head 6d3dd6247, TursoDriver from source)
new TursoDriver({ url: 'file:/tmp/…/local.db', encryptionKey: '…' }), then connect, CREATE TABLE, INSERT 'top-secret-24960', disconnect.
Plain better-sqlite3 opened the file read-only, with no key, and read [{"v":"top-secret-24960"}].
NOT MEASURED: replica mode (it needs a remote syncUrl).
Mechanism (read at source on that head)
In local mode (no syncUrl), TursoDriver.connect() (packages/drivers/driver-turso/src/turso-driver.ts) builds no @libsql/client: the libSQL client arm runs only when syncUrl is set. Reads and writes go through Knex on better-sqlite3, which is never handed the key.
The declaration that promises otherwise:
packages/drivers/driver-turso/src/spec/turso.zod.ts: encryptionKey: z.string().optional().describe('Encryption key for local database file (AES-256)');
TursoDriverConfig.encryptionKey: "AES-256 encryption key for the local database file. Only effective in local/replica modes."
Seam: spec:packages/drivers/driver-turso/src/spec/turso.zod.ts#encryptionKey → runtime:TursoDriver.connect() (local mode) · consumer: none in local mode.
Expected
Either the key encrypts the local file (and a file written with it cannot be read without it), or the key is refused where it cannot be honoured. ⛔ Never accepted and silently dropped.
Dedupe (MCP search_issues, objectstack, open and closed)
Why p1: an operator who declares encryptionKey on a local-mode Turso datasource gets a plaintext file. That is a declared-but-not-honoured protection of data at rest.
The fix is the dev's to measure and choose:
either the key encrypts the local file (a file written with it is unreadable without it);
or a local-mode encryptionKey is refused at connect with a sentence naming what does encrypt.
Claim: PM loop round 2 · 2026-10-21T02:46Z · p1, domain:engine, security (triage R551, 6306043862). A local-mode Turso datasource's declared encryptionKey is never applied, so the database file is plaintext.
Session: session_01QK6pdbxKZiHdFApL8RYVh9 (the dispatching seat; the dev is a mode:subagent run of this session, told apart by its branch)
Account: os-support-ai (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-25020-turso-encryption-key-local
Worktree: objectstack-issue-25020
Domain: domain:engine
Seat: domain:engine#2 (seat post #20966)
File surface:
packages/drivers/driver-turso/src/turso-driver.ts, the connect() region (the local and replica arms, near :2255 on 4d991588f). Measured first: what each mode does with encryptionKey today, replica mode included (where createClient receives the key, while the read and write path is Knex on better-sqlite3 over the same file).
packages/drivers/driver-turso/src/spec/turso.zod.tsencryptionKey and TursoDriverConfig.encryptionKey: their text is made true to what the code then does.
Pins in driver-turso's tests, and a changeset.
⛔ Not in scope: driver-sql, any other driver's config, and the remote (libsql://) mode, whose encryption is the server's. No real key and no production path goes on the PR (triage's disclosure line).
Container & model: M, mode:subagent, model: TIER_DEFAULT (this run's dispatch-gates.mjs --tier: "no path-derived mandate"; a security card with a premise-conditional route)
Clause-②: no. If the measured route is the refusal, it is no (narrowing) (a config that connected is refused) and owes a CONTRACT_REVIEW_TIER review before the queue; the dev's PR declares which.
Responsibility: driver-turso's local arm builds no libSQL client, so the key reaches nothing | none: the key's describe text promises AES-256 for the local file | any operator who declares encryptionKey on a local or replica Turso datasource; whether anyone does today is unknown (the dev greps the examples and docs)
Thread-read: 6306043862
Serial constraints cleared: the open PRs' file lists were read at this claim. PR #24893 (os-steve) touches turso-driver.ts near :1092 and :4083, not connect(); no open PR touches turso.zod.ts.
{
"issue": 25020,
"status": "done",
"branch": "claude/issue-25020-turso-encryption-key-local",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25045",
"session": "session_01QK6pdbxKZiHdFApL8RYVh9 — the dispatching seat's id (a mode:subagent run, so the id is the parent's); Claim 6306742426, the card's newest and only Claim, names this branch (checked before the first edit)",
"premise_still_valid": true,
"summary": "The card's premise holds: at BASE 4d991588f a local turso datasource with encryptionKey wrote a plaintext file that plain better-sqlite3 read without the key, and replica mode was no better. The seat's premise P does NOT hold: better-sqlite3 13.0.3 (SQLite 3.53.4), the engine that serves every local and replica read and write, has no encryption codec (no codec compile option, PRAGMA key a silent no-op); only the native libsql binding encrypts, so honouring the key means swapping engines. So, per the ruling's not-P branch, a non-empty encryptionKey in a local or replica mode is refused at all three doors in one message per mode, naming the mode and what does encrypt: new TursoDriver() / createTursoDriver() (VALIDATION_ERROR / 400, before super(), so no file is opened), the spec contract TursoConfigSchema (custom issue on encryptionKey, reaching DatasourceSchema, validateDriverConfig, os validate and the admin save / test doors), and the driver mirror (byte-identical). The producer of the declaration is packages/spec (the authoring contract), so the spec arm, an ADR-0087 semantic migration entry and a BREAKING minor changeset for both packages ship together; describes, TSDoc, README, design doc, drivers.mdx, QA checklist and the generated reference are made true. Remote mode is out of this card's scope and still accepts-and-drops the key (finding below).",
"tests": "All at head d15981a07. driver-turso: tsc --noEmit exit 0 (--listFiles holds all 93 test files incl. the new and changed ones); vitest run: Test Files 93 passed (93), Tests 2497 passed | 37 skipped. spec: typecheck exit 0 (TYPECHECK_EXIT=0, incl. check:test-typecheck); vitest run --project local: Test Files 703 passed (703), Tests 21377 passed | 1 todo. spec check:generated: 1 of 14 stale (check:docs) -> --fix regenerated content/docs/references/data/driver-turso.mdx -> up to date. Consumers authoring the key: metadata-protocol stored-row suites 2 files / 44 passed; service-datasource turso suites 4 files / 91 passed. Ablation (scripts/ablation-replace.mjs on the committed head, each restore proven blob == HEAD and git diff HEAD empty; all three suites resolve the mutated file from source): A constructor refusal call -> no-op: 25 failed | 249 passed | 37 skipped (9 refusal cases + 16 parity cases red, 3 controls and every order row green); B spec arm disabled: 5 failed | 123 passed (the 5 refusal cases red; controls, describe pin, placeholder family green); C mirror arm disabled: 7 failed | 287 passed | 37 skipped (mirror verdict on 6 unforced key rows + the sync+key row red). Not ablated: the parity table's spec half via packages/spec/dist (a rebuild per leg); leg B covers that arm through source.",
"gates": "Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at d15981a07: 119 families. Each run once, exit code captured to a per-command file before any pipe: 113 exit 0 first time; 6 exit 3 PREREQUISITE NOT MET (check:doc-formula-expressions, check:doc-security-posture, spec check:skill-examples, check:docs-transcript-drift, check:dual-build-cjs-loads, check:lean-entry-closure: unbuilt lint / client-react / whole-repo dist), re-run after turbo run build (77 tasks, exit 0): all 6 exit 0. --ran: 'Run reconciliation — 119 derived, 119 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). check:driver-conformance before (BASE) and after: '55 covered cell(s), 0 in the DEBT ledger, 0 exempt' both times. Local-only check-dev-prereqs scan: exit 0. PR check-runs at d15981a07 when this report was written: success — Auto Label, Check Documentation Links, Check PR Size, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter; in_progress — Build Core, Build Docs, Check Changeset, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint (CI release & engine, ESLint, PM dispatch-gates, docs & code guards, repo & tooling, 3 scoped), Spec property liveness, Test Core 1-6/6, Type Check (consumer gates, debt ledger, source gates, workspace); skipped — 18.x upgrade harness, Console Pin Gate, Dogfood shards, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance. in_progress is the honest value; CI convergence is the PM's.",
"dogfood_battery": "d15981a07 · os-verify-lock: VERDICT command-exit 0 (closure per ①, then pnpm --filter @objectstack/dogfood test, no shard, no path) · Test Files 284 passed | 3 skipped (287) · Tests 2361 passed | 18 skipped (2379) · no failure",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "2 — relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/25045/assignees (os-support-ai), read back 13770 of 13770 bytes identical; (2) comment -> POST /repos/objectstack-ai/objectstack/issues/25020/comments (this report). Not REST: git push of the branch. No labels written (the dispatch named none; the changeset exists, so no skip-changeset).",
"files_changed": [
".changeset/25020-turso-encryption-key-local.md",
"content/docs/data-modeling/drivers.mdx",
"content/docs/references/data/driver-turso.mdx",
"docs/design/driver-turso.md",
"docs/qa/platform-checklist/areas/integration-system.json",
"packages/drivers/driver-turso/README.md",
"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/spec/turso.zod.ts",
"packages/drivers/driver-turso/src/turso-driver-local-encryption-key-refusal.test.ts",
"packages/drivers/driver-turso/src/turso-driver.ts",
"packages/spec/src/data/datasource-credential-redaction.ts",
"packages/spec/src/data/driver/driver-placeholder-refusal.test.ts",
"packages/spec/src/data/driver/turso.test.ts",
"packages/spec/src/data/driver/turso.zod.ts",
"packages/spec/src/migrations/entries/semantic/18.turso-config-local-encryption-key-refused.ts"
],
"line_budget": "n/a — no skills/** or governed surface touched; 16 files, +658 / -44 vs origin/main (under the 3000-line threshold)",
"deviations": [
"File surface widened to the producer, as the dispatch allows: packages/spec/src/data/driver/turso.zod.ts (the authoring contract that declares the key; turso-config-constructor-parity.test.ts holds 'authoring refuses what construction refuses', so a constructor-only refusal would reopen the #19977 class), its test, an ADR-0087 semantic entry, a one-comment edit in datasource-credential-redaction.ts, the placeholder-family fixture, and the doc surfaces whose text the refusal made false (README, docs/design/driver-turso.md, content/docs/data-modeling/drivers.mdx, the QA checklist's turso expectation, the regenerated reference).",
"The changeset grades @objectstack/spec minor as well as @objectstack/driver-turso (the dispatch named driver-turso only), because the spec contract narrows too.",
"connect()'s replica arm no longer forwards encryptionKey to createClient: after the refusal a non-empty key cannot reach it, and an empty one was already a no-op there.",
"Replica mode with the key against a REACHABLE sync server: NOT MEASURED (no libSQL sync server in this container; the native binding's default sync path speaks the server's replication protocol). The replica pins need none: the refusal fires at construction.",
"Six derived gates first answered exit 3 (unbuilt prerequisites) and were re-run at the same head after a full turbo build; that build ran under the lock while the unlocked gate runner was mid-way, rebuilding packages/spec/dist from identical source; every gate in that window answered 0.",
"Commit trailers use AGENTS.md's model-free pair, not the harness reminder's model-named Co-Authored-By line; the PR body ends with the session-URL footer form, not the reminder's line."
],
"open_questions": [
{
"question": "After this PR, turso encryptionKey is honoured in no mode: refused for local and replica datasources, and on a remote datasource still parsed and silently dropped (createRemoteClient never forwards it, while @libsql/client's own remote client refuses it with ENCRYPTION_KEY_NOT_SUPPORTED). The claim put remote mode out of scope. What should the follow-up card do?",
"options": [
"A — retire the key outright: a retiredKey() tombstone in the spec contract and z.never() in the mirror, a constructor refusal in every mode, a retired-keys ADR-0087 entry, drop it from STILL_WRITABLE_CREDENTIAL_KEYS and the redaction fixtures that author it (stored rows keep being redacted by the generic credential-shaped check or by a retired-key reader). Cost: one retirement card through the spec-property-retirement kit (about a dozen surfaces, many test fixtures on remote urls); one BREAKING minor.",
"B — refuse it in remote mode too, keeping the key declared: one more arm in tursoTransportIssues and the constructor, same message family. Cost: small (one arm, parity rows); leaves a declared key that every mode refuses, i.e. a retirement without a tombstone.",
"C — leave remote accepting it. Cost: zero now; keeps a declared key that changes nothing."
],
"recommendation": "A. Business need (measured on this tree): zero producers — no example, template, CLI scaffold, published skill or env var authors the key; only test fixtures and docs describing it. Long-term soundness: a key the runtime honours nowhere is the enforce-or-remove case ADR-0049 resolves by removal, with the prescription in the tombstone. AI-error prevention: a tombstone refuses loudly in every mode, while B leaves a declared key whose only purpose is to be refused and C leaves a key an AI will write and the runtime drops. Startup focus: retire immediately, no staged window, no new gate."
}
],
"out_of_scope_findings": [
"class: b · reach: public door measured — new TursoDriver({ url: 'libsql://db.example.invalid', encryptionKey: 'test-key-25020' }) constructs and connects in remote mode with no refusal, and the spec contract (DatasourceSchema / os validate) parses a remote turso datasource carrying encryptionKey, while createRemoteClient never forwards it and @libsql/client's own remote client refuses that key (ENCRYPTION_KEY_NOT_SUPPORTED: Encryption key is not supported by the remote client.) · contract: 'the key is refused where it cannot be honoured. ⛔ Never accepted and silently dropped.' (#25020 Expected; ADR-0049 enforce-or-remove) · evidence: packages/drivers/driver-turso/src/turso-driver.ts createRemoteClient (url, authToken, concurrency, fetch only); measured at BASE 4d991588f and unchanged at d15981a07; contract text now reads 'Not honoured in any mode' · Seam: spec:packages/spec/src/data/driver/turso.zod.ts#encryptionKey → runtime:TursoDriver.createRemoteClient (remote mode) · consumer: none · family: this card's (turso encryptionKey), not a single-point card; the claim scoped remote out, and the open question above carries the route options for the seat to file · dedupe words: turso encryptionKey remote mode ignored · ENCRYPTION_KEY_NOT_SUPPORTED remote client turso driver · retire turso encryptionKey tombstone"
]
}
{
"issue": 25020,
"status": "done",
"branch": "claude/issue-25020-turso-encryption-key-local",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25045",
"session": "session_01QK6pdbxKZiHdFApL8RYVh9 — the dispatching seat's id (a mode:subagent run, so the id is the parent's); patch round 1 on the same Claim 6306742426 and branch; head 4a595ed439b9d2d8d46e5a436a6bf4c568686bc9 equals PR #25045's head (read before finishing)",
"premise_still_valid": true,
"summary": "Patch round 1 (contract review 6308116467 FAIL item 1 + findings 7-9; seat verdict 6308149967). Blocking item fixed: the encryptionKey refusal is now judged by where the database lives, not by the mode. The constructor runs one mode-independent predicate before super() — a non-empty key beside a url namesLocalDatabase() accepts (a file: url in any case, or :memory:) — throwing the existing local / replica text, or, under a forced mode: 'remote', remoteModeLocalEncryptionKeyRefusal(url) (one text for a file, one for an in-memory database; both name the mode, echo no url or key, VALIDATION_ERROR / 400). The spec's tursoTransportIssues gains the same arm last in its remote branch (issue on encryptionKey, after timeoutMs / syncUrl), byte-identical in the mirror (whose copy is unreachable: it declares no mode and refuses the shape through its local arm). The key is not forwarded to createClient; a libsql:// url with a key stays accepted (#25050). Door 6 was measured with the new arm ablated: forced-remote file + key constructed, connected, round-tripped, and plain better-sqlite3 read the row with no key; with the arm it is refused and no file exists. Riders: service-datasource TursoDriverConfigInput.encryptionKey TSDoc (+ a patch changeset entry, accepted by check-changeset-no-major beside Clause-②: no (narrowing)), the redaction module header, OS_ALLOW_DRIVER_CONNECT_FAILURE dropped from the semantic entry and the changeset; describe (both schemas), regenerated reference, drivers.mdx remote sentence, README and design doc made true for the forced-remote file case. The PR body was NOT patched by this run (see deviations): the replacement body is ready for the seat.",
"tests": "All at head 4a595ed43. driver-turso: tsc --noEmit EXIT=0; vitest run: Test Files 93 passed (93), Tests 2538 passed | 47 skipped. spec: typecheck EXIT=0 (incl. check:scripts-typecheck, check:test-typecheck); vitest run --project local: Test Files 703 passed (703), Tests 21383 passed | 1 todo. service-datasource (imports the changed TSDoc file): tsc --noEmit EXIT=0; vitest run: Test Files 51 passed (51), Tests 916 passed. Targeted first (same head): driver-turso refusal + parity + mirror + unrecognised-url suites 4 files / 389 passed | 47 skipped; spec turso + placeholder 2 files / 134 passed. spec check:generated --fix: check:docs was the one stale artifact (content/docs/references/data/driver-turso.mdx regenerated, only the encryptionKey row moved); then gen:schema EXIT=0, gen:docs EXIT=0, check:docs EXIT=0 ('227 generated files in sync with packages/spec'), re-run at the committed head 4a595ed43: EXIT=0. Ablation leg D (scripts/ablation-replace.mjs, at 4a595ed43, predicted before it ran: forced-remote refusal cases red, everything else green): the new predicate narrowed back to the old guard (mode !== 'remote' && ...): 16 failed | 299 passed | 47 skipped — the 6 forced-remote refusal cases (3 file, 2 in-memory, createTursoDriver) and 10 parity cases (5 rows x constructor verdict + byte equality) red; every control, every local / replica case and both forced-remote order rows green; mutation landed (anchor 1 -> 0, blob cb53a3effd5d -> 05b72b8fe788), restore proven (blob == HEAD cb53a3effd5d, git diff HEAD empty). The door-6 probe under the mutation: 'constructed and connected, transportMode remote | read-back [test-secret-25020]', file header 'SQLite format 3', plain better-sqlite3 with no key read the row; at the head: 'refused VALIDATION_ERROR 400 | file exists afterwards: false'. Not ablated this round: the new remote arm in the spec and the mirror (the mirror's copy is unreachable through it).",
"gates": "Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at 4a595ed43: 121 families (round 0's 119 plus the check-tenant-audit-census pair). All 121 run once after a full turbo build (77 tasks, VERDICT command-exit 0), each exit code captured to a per-command file before any pipe: 121 exit 0. --ran at 4a595ed439: 'Run reconciliation — 121 derived, 121 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). check:driver-conformance '55 covered cell(s), 0 in the DEBT ledger, 0 exempt' at BASE, d15981a07 and 4a595ed43. check-changeset-no-major --base origin/main --event (a payload carrying this PR's body): exit 0, service-datasource 'patch ... NOT refused'. Local-only check-dev-prereqs: exit 0. PR check-runs on 4a595ed43, read once: 36 success, 6 skipped (the earlier check:docs red on 0833772a4 / b0236c662 is cleared by 4a595ed43).",
"dogfood_battery": "4a595ed43 · os-verify-lock: VERDICT command-exit 0 (closure per ①, then pnpm --filter @objectstack/dogfood test, no shard, no path) · Test Files 284 passed | 3 skipped (287) · Tests 2361 passed | 18 skipped (2379) · no failure. A first attempt on this head was cut off by the worker restart before its verdict and is not counted.",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "1 this round — one relay stroke, POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: comment -> POST /repos/objectstack-ai/objectstack/issues/25020/comments (this report). Not REST: git push of 0833772a4, b0236c662, 4a595ed43. Round 0's two (pr_create + its assignee leg, and the round-0 report comment) stand. No PR body PATCH, no labels.",
"files_changed": [
".changeset/25020-turso-encryption-key-local.md",
"content/docs/data-modeling/drivers.mdx",
"content/docs/references/data/driver-turso.mdx",
"docs/design/driver-turso.md",
"packages/drivers/driver-turso/README.md",
"packages/drivers/driver-turso/src/spec/turso-config-constructor-parity.test.ts",
"packages/drivers/driver-turso/src/spec/turso.zod.ts",
"packages/drivers/driver-turso/src/turso-driver-local-encryption-key-refusal.test.ts",
"packages/drivers/driver-turso/src/turso-driver.ts",
"packages/services/service-datasource/src/turso-driver-config.ts",
"packages/spec/src/data/datasource-credential-redaction.ts",
"packages/spec/src/data/driver/turso.test.ts",
"packages/spec/src/data/driver/turso.zod.ts",
"packages/spec/src/migrations/entries/semantic/18.turso-config-local-encryption-key-refused.ts"
],
"line_budget": "n/a — no skills/** or governed surface touched; this round 14 files +376 / -45 vs d15981a07; the PR 17 files +996 / -51 vs origin/main (under the 3000-line threshold)",
"deviations": [
"CONFLICT, named rather than chosen silently: the dispatch said 'update the PR body'; the role file says the dev writes the PR body once, at pr_create, never PATCHes it, and names the change for the seat to write (and caps dev writes at push + pr_create + report comment); on conflict the role file governs. So this run did not PATCH. The replacement body (20684 bytes, Clause-② line unchanged as line 2, no angle brackets, session-URL footer) is ready at /tmp/claude-0/-home-user/9fa8eee6-8749-54f5-979b-34f1853aa79d/scratchpad/issue-25020/r1/pr-body-r1.md for the seat's issue_patch on #25045. Until then the stored body reads round 0 (head d15981a07, no forced-remote arm).",
"The constructor arm is written as ONE mode-independent predicate (a non-empty key beside a file: url or :memory:), choosing its text by mode; in a local or replica mode it is the old guard exactly, because localEngineDefect has refused every other url before it.",
"Ablation leg D was taken through source, with no rebuild: the refusal file and the parity table's constructor half import turso-driver.ts directly. The new remote arm in the spec and the mirror was not ablated (one leg was ordered, over the constructor arm).",
"The in-memory variant's text adds no remedy beyond 'Drop encryptionKey' (an in-memory database has no file to put on an encrypted volume).",
"An interrupted dogfood run (worker restart) left no verdict and is not counted; my stale pid files in issue-25020/ were removed by literal path (a first rm with a variable path was refused by the harness safety check and did not run)."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: #25050 (filed by the seat) · the remote-url mode (libsql://, https://, ws(s)://) still accepts and drops encryptionKey; unchanged by this round, and pinned as an accept control in the parity table and the refusal file at the dispatch's order, so #25050's route will turn those controls red on purpose"
]
}
{
"issue": 25020,
"status": "done",
"branch": "claude/issue-25020-turso-encryption-key-local",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25045",
"session": "session_01QK6pdbxKZiHdFApL8RYVh9 — the dispatching seat's id (a mode:subagent run, so the id is the parent's); patch round 2 on the same Claim 6306742426 and branch; head df6803d15923a60a4dc32700d36b563f1b30a4e9 equals PR #25045's head (read after the push and again at the end)",
"premise_still_valid": true,
"summary": "Patch round 2 (seat order 6310662118; contract review 6310634449 PASS on 4a595ed43). One changeset sentence rewritten, nothing else: in .changeset/25020-turso-encryption-key-local.md, 'The message is the same text at all three places, and a test holds the constructor's copy equal to the schema's issue byte for byte.' became 'For a local or replica datasource the message is the same text at all three places; for a `file:` url or `:memory:` under a forced `mode: 'remote'`, the schema and the constructor give the forced-remote text, while the driver mirror, which declares no `mode` and strips it, refuses the same config in the local text. A test holds the constructor's copy equal to the schema's issue byte for byte in every mode, and the mirror's verdict and words equal to the schema's wherever no `mode` is forced.' — which is what the parity table holds (byte equality constructor == spec over every copied-message row, forced-remote rows included; the mirror's verdict compared only on rows with no forced mode). `git diff --name-only 4a595ed43 HEAD` lists only `.changeset/25020-turso-encryption-key-local.md` (count 1), checked before the push, so the PASS record 6310634449 keeps governing; the Regen-provenance line is the seat's.",
"tests": "No source, test or generated file moved this round (the diff is one changeset sentence), so no package suite was re-run; the round-1 runs at 4a595ed43 stand for the identical tree minus .changeset (driver-turso 93 files / 2538 passed | 47 skipped, spec 703 / 21383 passed | 1 todo, service-datasource 51 / 916 passed, all typechecks EXIT=0). At df6803d15: full turbo build 77/77 (75 cached), VERDICT command-exit 0; the full dogfood battery (below). No ablation: the diff changes no tested behaviour (a changeset is prose).",
"gates": "At df6803d15: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 121 families, the same set as round 1. All 121 run once, each exit code captured to a per-command file before any pipe: 121 exit 0 (incl. check-changeset-no-major, check-adr-0087-registration and check-empty-changeset --base origin/main, each 0). --ran: 'Run reconciliation — 121 derived, 121 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). check:driver-conformance '55 covered cell(s), 0 in the DEBT ledger, 0 exempt'. check-changeset-no-major --base origin/main --event (the stored PR body's Clause-② line): exit 0, spec and driver-turso minor, service-datasource patch 'NOT refused'. PR check-runs on df6803d15, read once at the end: 36 success, 6 skipped, none failing or pending.",
"dogfood_battery": "df6803d15 · os-verify-lock: VERDICT command-exit 0 (closure per ①, then pnpm --filter @objectstack/dogfood test, no shard, no path) · Test Files 284 passed | 3 skipped (287) · Tests 2361 passed | 18 skipped (2379) · no failure",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "1 this round — one relay stroke, POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: comment -> POST /repos/objectstack-ai/objectstack/issues/25020/comments (this report). Not REST: git push of df6803d15. No PR body PATCH, no labels.",
"files_changed": [
".changeset/25020-turso-encryption-key-local.md"
],
"line_budget": "n/a — no skills/** or governed surface touched; this round 1 file, +1 / -1 vs 4a595ed43",
"pr_body_delta_for_the_seat": [
{
"old": "**Every reading below was taken at head `4a595ed43`** unless it says otherwise;",
"new": "**Patch round 2** (seat order 6310662118) landed as `df6803d15`: one changeset sentence, `.changeset/**` only. **Every reading below was taken at head `4a595ed43`** unless it says otherwise (the full dogfood battery, the derived gates and the check-runs were re-read at `df6803d15`);"
},
{
"old": "once, on the final head `4a595ed43`, closure built per ① and run under the lock as `pnpm --filter @objectstack/dogfood test` with no shard and no path: `VERDICT command-exit 0`, Test Files 284 passed, 3 skipped (287), Tests 2361 passed, 18 skipped. No failure. (A first attempt on this head was cut off by a worker restart before its verdict and is not counted.)",
"new": "once, on the final head `df6803d15`, closure built per ① and run under the lock as `pnpm --filter @objectstack/dogfood test` with no shard and no path: `VERDICT command-exit 0`, Test Files 284 passed, 3 skipped (287), Tests 2361 passed, 18 skipped. No failure. (At `4a595ed43` the same battery also read `VERDICT command-exit 0`, 284 passed / 3 skipped, 2361 passed / 18 skipped.) At `df6803d15` the 121 derived gate families all exit 0 (`--ran`: 121 derived, 121 run, 0 NOT-MEASURED, 0 UNRUN), and the check-runs read 36 success, 6 skipped."
}
],
"deviations": [
"The rewritten text is two sentences where there was one; no other sentence in the changeset changed (git diff --word-diff shows the one line only).",
"The PR body delta above is for the seat to write (role file: no dev PATCH); the Regen-provenance line is the seat's, per the order.",
"The worktree had been removed after round 1, so it was re-added from origin/claude/issue-25020-turso-encryption-key-local at 4a595ed43 (equal to the PR head) before the edit; a first lock request for the build timed out in the queue (exit 99, never acquired) behind another seat's dogfood and was re-requested under the same slot."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: #25050 · unchanged this round: the remote-url mode still accepts and drops encryptionKey; the seat's noted nit (remote-url lists omit http://) is incomplete, not false, and was not reworked, per the order"
]
}
ACCEPT → PR #25045 comment 6311723823 · contract review PASS 6310634449 on 4a595ed43, carried to df6803d15 by Regen-provenance 6310718106 (prose-only) · ready + auto-merge armed 2026-10-21T08:38Z. The merge closes this card via Fixes. The libsql:// remote mode is #25050.
merged: true, merge commit 28c9a2cb6, through the merge queue on its first entry
origin/main
28c9a2cb6 fix(spec,driver-turso)!: refuse encryptionKey on a local or replica turso datasource instead of writing the file in plaintext (#25045)
queue branch
gh-readonly-queue/main/pr-25045-* gone
card
closed completed by Fixes #25020; #25050 still open (no wrong close)
State: pm:dispatched removed in this act.
A non-empty encryptionKey beside a file: url or :memory: is now refused at the spec, the driver mirror and the constructor, in every mode. That covers a forced mode: 'remote'.
Activity
objectstack-fleet commentedon Oct 21, 2026
Triage (R551): p1 ·
domain:engine·area:access·bug+security·pm:queue. ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-21T01:52ZencryptionKeyon a local-mode Turso datasource gets a plaintext file. That is a declared-but-not-honoured protection of data at rest.encryptionKeyis refused at connect with a sentence naming what does encrypt.6303865025), a refusal ships in 18.x as a BREAKING minor. No 19.0 step.Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
Claim: PM loop round 2 · 2026-10-21T02:46Z · p1,
domain:engine,security(triage R551, 6306043862). A local-mode Turso datasource's declaredencryptionKeyis never applied, so the database file is plaintext.Session:
session_01QK6pdbxKZiHdFApL8RYVh9(the dispatching seat; the dev is amode:subagentrun of this session, told apart by its branch)Account:
os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-25020-turso-encryption-key-localWorktree:
objectstack-issue-25020Domain:
domain:engineSeat:
domain:engine#2(seat post #20966)File surface:
packages/drivers/driver-turso/src/turso-driver.ts, theconnect()region (the local and replica arms, near:2255on4d991588f). Measured first: what each mode does withencryptionKeytoday, replica mode included (wherecreateClientreceives the key, while the read and write path is Knex on better-sqlite3 over the same file).packages/drivers/driver-turso/src/spec/turso.zod.tsencryptionKeyandTursoDriverConfig.encryptionKey: their text is made true to what the code then does.driver-turso's tests, and a changeset.⛔ Not in scope:
driver-sql, any other driver's config, and the remote (libsql://) mode, whose encryption is the server's. No real key and no production path goes on the PR (triage's disclosure line).Container & model:
M,mode:subagent,model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate"; a security card with a premise-conditional route)Clause-②: no. If the measured route is the refusal, it is
no (narrowing)(a config that connected is refused) and owes aCONTRACT_REVIEW_TIERreview before the queue; the dev's PR declares which.Responsibility:
driver-turso's local arm builds no libSQL client, so the key reaches nothing | none: the key's describe text promises AES-256 for the local file | any operator who declaresencryptionKeyon a local or replica Turso datasource; whether anyone does today is unknown (the dev greps the examples and docs)Thread-read: 6306043862
Serial constraints cleared: the open PRs' file lists were read at this claim. PR #24893 (
os-steve) touchesturso-driver.tsnear:1092and:4083, notconnect(); no open PR touchesturso.zod.ts.Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
os-dev-report
{ "issue": 25020, "status": "done", "branch": "claude/issue-25020-turso-encryption-key-local", "pr": "https://github.com/objectstack-ai/objectstack/pull/25045", "session": "session_01QK6pdbxKZiHdFApL8RYVh9 — the dispatching seat's id (a mode:subagent run, so the id is the parent's); Claim 6306742426, the card's newest and only Claim, names this branch (checked before the first edit)", "premise_still_valid": true, "summary": "The card's premise holds: at BASE 4d991588f a local turso datasource with encryptionKey wrote a plaintext file that plain better-sqlite3 read without the key, and replica mode was no better. The seat's premise P does NOT hold: better-sqlite3 13.0.3 (SQLite 3.53.4), the engine that serves every local and replica read and write, has no encryption codec (no codec compile option, PRAGMA key a silent no-op); only the native libsql binding encrypts, so honouring the key means swapping engines. So, per the ruling's not-P branch, a non-empty encryptionKey in a local or replica mode is refused at all three doors in one message per mode, naming the mode and what does encrypt: new TursoDriver() / createTursoDriver() (VALIDATION_ERROR / 400, before super(), so no file is opened), the spec contract TursoConfigSchema (custom issue on encryptionKey, reaching DatasourceSchema, validateDriverConfig, os validate and the admin save / test doors), and the driver mirror (byte-identical). The producer of the declaration is packages/spec (the authoring contract), so the spec arm, an ADR-0087 semantic migration entry and a BREAKING minor changeset for both packages ship together; describes, TSDoc, README, design doc, drivers.mdx, QA checklist and the generated reference are made true. Remote mode is out of this card's scope and still accepts-and-drops the key (finding below).", "tests": "All at head d15981a07. driver-turso: tsc --noEmit exit 0 (--listFiles holds all 93 test files incl. the new and changed ones); vitest run: Test Files 93 passed (93), Tests 2497 passed | 37 skipped. spec: typecheck exit 0 (TYPECHECK_EXIT=0, incl. check:test-typecheck); vitest run --project local: Test Files 703 passed (703), Tests 21377 passed | 1 todo. spec check:generated: 1 of 14 stale (check:docs) -> --fix regenerated content/docs/references/data/driver-turso.mdx -> up to date. Consumers authoring the key: metadata-protocol stored-row suites 2 files / 44 passed; service-datasource turso suites 4 files / 91 passed. Ablation (scripts/ablation-replace.mjs on the committed head, each restore proven blob == HEAD and git diff HEAD empty; all three suites resolve the mutated file from source): A constructor refusal call -> no-op: 25 failed | 249 passed | 37 skipped (9 refusal cases + 16 parity cases red, 3 controls and every order row green); B spec arm disabled: 5 failed | 123 passed (the 5 refusal cases red; controls, describe pin, placeholder family green); C mirror arm disabled: 7 failed | 287 passed | 37 skipped (mirror verdict on 6 unforced key rows + the sync+key row red). Not ablated: the parity table's spec half via packages/spec/dist (a rebuild per leg); leg B covers that arm through source.", "gates": "Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at d15981a07: 119 families. Each run once, exit code captured to a per-command file before any pipe: 113 exit 0 first time; 6 exit 3 PREREQUISITE NOT MET (check:doc-formula-expressions, check:doc-security-posture, spec check:skill-examples, check:docs-transcript-drift, check:dual-build-cjs-loads, check:lean-entry-closure: unbuilt lint / client-react / whole-repo dist), re-run after turbo run build (77 tasks, exit 0): all 6 exit 0. --ran: 'Run reconciliation — 119 derived, 119 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). check:driver-conformance before (BASE) and after: '55 covered cell(s), 0 in the DEBT ledger, 0 exempt' both times. Local-only check-dev-prereqs scan: exit 0. PR check-runs at d15981a07 when this report was written: success — Auto Label, Check Documentation Links, Check PR Size, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter; in_progress — Build Core, Build Docs, Check Changeset, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint (CI release & engine, ESLint, PM dispatch-gates, docs & code guards, repo & tooling, 3 scoped), Spec property liveness, Test Core 1-6/6, Type Check (consumer gates, debt ledger, source gates, workspace); skipped — 18.x upgrade harness, Console Pin Gate, Dogfood shards, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance. in_progress is the honest value; CI convergence is the PM's.", "dogfood_battery": "d15981a07 · os-verify-lock: VERDICT command-exit 0 (closure per ①, then pnpm --filter @objectstack/dogfood test, no shard, no path) · Test Files 284 passed | 3 skipped (287) · Tests 2361 passed | 18 skipped (2379) · no failure", "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "2 — relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/25045/assignees (os-support-ai), read back 13770 of 13770 bytes identical; (2) comment -> POST /repos/objectstack-ai/objectstack/issues/25020/comments (this report). Not REST: git push of the branch. No labels written (the dispatch named none; the changeset exists, so no skip-changeset).", "files_changed": [ ".changeset/25020-turso-encryption-key-local.md", "content/docs/data-modeling/drivers.mdx", "content/docs/references/data/driver-turso.mdx", "docs/design/driver-turso.md", "docs/qa/platform-checklist/areas/integration-system.json", "packages/drivers/driver-turso/README.md", "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/spec/turso.zod.ts", "packages/drivers/driver-turso/src/turso-driver-local-encryption-key-refusal.test.ts", "packages/drivers/driver-turso/src/turso-driver.ts", "packages/spec/src/data/datasource-credential-redaction.ts", "packages/spec/src/data/driver/driver-placeholder-refusal.test.ts", "packages/spec/src/data/driver/turso.test.ts", "packages/spec/src/data/driver/turso.zod.ts", "packages/spec/src/migrations/entries/semantic/18.turso-config-local-encryption-key-refused.ts" ], "line_budget": "n/a — no skills/** or governed surface touched; 16 files, +658 / -44 vs origin/main (under the 3000-line threshold)", "deviations": [ "File surface widened to the producer, as the dispatch allows: packages/spec/src/data/driver/turso.zod.ts (the authoring contract that declares the key; turso-config-constructor-parity.test.ts holds 'authoring refuses what construction refuses', so a constructor-only refusal would reopen the #19977 class), its test, an ADR-0087 semantic entry, a one-comment edit in datasource-credential-redaction.ts, the placeholder-family fixture, and the doc surfaces whose text the refusal made false (README, docs/design/driver-turso.md, content/docs/data-modeling/drivers.mdx, the QA checklist's turso expectation, the regenerated reference).", "The changeset grades @objectstack/spec minor as well as @objectstack/driver-turso (the dispatch named driver-turso only), because the spec contract narrows too.", "connect()'s replica arm no longer forwards encryptionKey to createClient: after the refusal a non-empty key cannot reach it, and an empty one was already a no-op there.", "Replica mode with the key against a REACHABLE sync server: NOT MEASURED (no libSQL sync server in this container; the native binding's default sync path speaks the server's replication protocol). The replica pins need none: the refusal fires at construction.", "Six derived gates first answered exit 3 (unbuilt prerequisites) and were re-run at the same head after a full turbo build; that build ran under the lock while the unlocked gate runner was mid-way, rebuilding packages/spec/dist from identical source; every gate in that window answered 0.", "Commit trailers use AGENTS.md's model-free pair, not the harness reminder's model-named Co-Authored-By line; the PR body ends with the session-URL footer form, not the reminder's line." ], "open_questions": [ { "question": "After this PR, turso encryptionKey is honoured in no mode: refused for local and replica datasources, and on a remote datasource still parsed and silently dropped (createRemoteClient never forwards it, while @libsql/client's own remote client refuses it with ENCRYPTION_KEY_NOT_SUPPORTED). The claim put remote mode out of scope. What should the follow-up card do?", "options": [ "A — retire the key outright: a retiredKey() tombstone in the spec contract and z.never() in the mirror, a constructor refusal in every mode, a retired-keys ADR-0087 entry, drop it from STILL_WRITABLE_CREDENTIAL_KEYS and the redaction fixtures that author it (stored rows keep being redacted by the generic credential-shaped check or by a retired-key reader). Cost: one retirement card through the spec-property-retirement kit (about a dozen surfaces, many test fixtures on remote urls); one BREAKING minor.", "B — refuse it in remote mode too, keeping the key declared: one more arm in tursoTransportIssues and the constructor, same message family. Cost: small (one arm, parity rows); leaves a declared key that every mode refuses, i.e. a retirement without a tombstone.", "C — leave remote accepting it. Cost: zero now; keeps a declared key that changes nothing." ], "recommendation": "A. Business need (measured on this tree): zero producers — no example, template, CLI scaffold, published skill or env var authors the key; only test fixtures and docs describing it. Long-term soundness: a key the runtime honours nowhere is the enforce-or-remove case ADR-0049 resolves by removal, with the prescription in the tombstone. AI-error prevention: a tombstone refuses loudly in every mode, while B leaves a declared key whose only purpose is to be refused and C leaves a key an AI will write and the runtime drops. Startup focus: retire immediately, no staged window, no new gate." } ], "out_of_scope_findings": [ "class: b · reach: public door measured — new TursoDriver({ url: 'libsql://db.example.invalid', encryptionKey: 'test-key-25020' }) constructs and connects in remote mode with no refusal, and the spec contract (DatasourceSchema / os validate) parses a remote turso datasource carrying encryptionKey, while createRemoteClient never forwards it and @libsql/client's own remote client refuses that key (ENCRYPTION_KEY_NOT_SUPPORTED: Encryption key is not supported by the remote client.) · contract: 'the key is refused where it cannot be honoured. ⛔ Never accepted and silently dropped.' (#25020 Expected; ADR-0049 enforce-or-remove) · evidence: packages/drivers/driver-turso/src/turso-driver.ts createRemoteClient (url, authToken, concurrency, fetch only); measured at BASE 4d991588f and unchanged at d15981a07; contract text now reads 'Not honoured in any mode' · Seam: spec:packages/spec/src/data/driver/turso.zod.ts#encryptionKey → runtime:TursoDriver.createRemoteClient (remote mode) · consumer: none · family: this card's (turso encryptionKey), not a single-point card; the claim scoped remote out, and the open question above carries the route options for the seat to file · dedupe words: turso encryptionKey remote mode ignored · ENCRYPTION_KEY_NOT_SUPPORTED remote client turso driver · retire turso encryptionKey tombstone" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
os-dev-report
{ "issue": 25020, "status": "done", "branch": "claude/issue-25020-turso-encryption-key-local", "pr": "https://github.com/objectstack-ai/objectstack/pull/25045", "session": "session_01QK6pdbxKZiHdFApL8RYVh9 — the dispatching seat's id (a mode:subagent run, so the id is the parent's); patch round 1 on the same Claim 6306742426 and branch; head 4a595ed439b9d2d8d46e5a436a6bf4c568686bc9 equals PR #25045's head (read before finishing)", "premise_still_valid": true, "summary": "Patch round 1 (contract review 6308116467 FAIL item 1 + findings 7-9; seat verdict 6308149967). Blocking item fixed: the encryptionKey refusal is now judged by where the database lives, not by the mode. The constructor runs one mode-independent predicate before super() — a non-empty key beside a url namesLocalDatabase() accepts (a file: url in any case, or :memory:) — throwing the existing local / replica text, or, under a forced mode: 'remote', remoteModeLocalEncryptionKeyRefusal(url) (one text for a file, one for an in-memory database; both name the mode, echo no url or key, VALIDATION_ERROR / 400). The spec's tursoTransportIssues gains the same arm last in its remote branch (issue on encryptionKey, after timeoutMs / syncUrl), byte-identical in the mirror (whose copy is unreachable: it declares no mode and refuses the shape through its local arm). The key is not forwarded to createClient; a libsql:// url with a key stays accepted (#25050). Door 6 was measured with the new arm ablated: forced-remote file + key constructed, connected, round-tripped, and plain better-sqlite3 read the row with no key; with the arm it is refused and no file exists. Riders: service-datasource TursoDriverConfigInput.encryptionKey TSDoc (+ a patch changeset entry, accepted by check-changeset-no-major beside Clause-②: no (narrowing)), the redaction module header, OS_ALLOW_DRIVER_CONNECT_FAILURE dropped from the semantic entry and the changeset; describe (both schemas), regenerated reference, drivers.mdx remote sentence, README and design doc made true for the forced-remote file case. The PR body was NOT patched by this run (see deviations): the replacement body is ready for the seat.", "tests": "All at head 4a595ed43. driver-turso: tsc --noEmit EXIT=0; vitest run: Test Files 93 passed (93), Tests 2538 passed | 47 skipped. spec: typecheck EXIT=0 (incl. check:scripts-typecheck, check:test-typecheck); vitest run --project local: Test Files 703 passed (703), Tests 21383 passed | 1 todo. service-datasource (imports the changed TSDoc file): tsc --noEmit EXIT=0; vitest run: Test Files 51 passed (51), Tests 916 passed. Targeted first (same head): driver-turso refusal + parity + mirror + unrecognised-url suites 4 files / 389 passed | 47 skipped; spec turso + placeholder 2 files / 134 passed. spec check:generated --fix: check:docs was the one stale artifact (content/docs/references/data/driver-turso.mdx regenerated, only the encryptionKey row moved); then gen:schema EXIT=0, gen:docs EXIT=0, check:docs EXIT=0 ('227 generated files in sync with packages/spec'), re-run at the committed head 4a595ed43: EXIT=0. Ablation leg D (scripts/ablation-replace.mjs, at 4a595ed43, predicted before it ran: forced-remote refusal cases red, everything else green): the new predicate narrowed back to the old guard (mode !== 'remote' && ...): 16 failed | 299 passed | 47 skipped — the 6 forced-remote refusal cases (3 file, 2 in-memory, createTursoDriver) and 10 parity cases (5 rows x constructor verdict + byte equality) red; every control, every local / replica case and both forced-remote order rows green; mutation landed (anchor 1 -> 0, blob cb53a3effd5d -> 05b72b8fe788), restore proven (blob == HEAD cb53a3effd5d, git diff HEAD empty). The door-6 probe under the mutation: 'constructed and connected, transportMode remote | read-back [test-secret-25020]', file header 'SQLite format 3', plain better-sqlite3 with no key read the row; at the head: 'refused VALIDATION_ERROR 400 | file exists afterwards: false'. Not ablated this round: the new remote arm in the spec and the mirror (the mirror's copy is unreachable through it).", "gates": "Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at 4a595ed43: 121 families (round 0's 119 plus the check-tenant-audit-census pair). All 121 run once after a full turbo build (77 tasks, VERDICT command-exit 0), each exit code captured to a per-command file before any pipe: 121 exit 0. --ran at 4a595ed439: 'Run reconciliation — 121 derived, 121 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). check:driver-conformance '55 covered cell(s), 0 in the DEBT ledger, 0 exempt' at BASE, d15981a07 and 4a595ed43. check-changeset-no-major --base origin/main --event (a payload carrying this PR's body): exit 0, service-datasource 'patch ... NOT refused'. Local-only check-dev-prereqs: exit 0. PR check-runs on 4a595ed43, read once: 36 success, 6 skipped (the earlier check:docs red on 0833772a4 / b0236c662 is cleared by 4a595ed43).", "dogfood_battery": "4a595ed43 · os-verify-lock: VERDICT command-exit 0 (closure per ①, then pnpm --filter @objectstack/dogfood test, no shard, no path) · Test Files 284 passed | 3 skipped (287) · Tests 2361 passed | 18 skipped (2379) · no failure. A first attempt on this head was cut off by the worker restart before its verdict and is not counted.", "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "1 this round — one relay stroke, POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: comment -> POST /repos/objectstack-ai/objectstack/issues/25020/comments (this report). Not REST: git push of 0833772a4, b0236c662, 4a595ed43. Round 0's two (pr_create + its assignee leg, and the round-0 report comment) stand. No PR body PATCH, no labels.", "files_changed": [ ".changeset/25020-turso-encryption-key-local.md", "content/docs/data-modeling/drivers.mdx", "content/docs/references/data/driver-turso.mdx", "docs/design/driver-turso.md", "packages/drivers/driver-turso/README.md", "packages/drivers/driver-turso/src/spec/turso-config-constructor-parity.test.ts", "packages/drivers/driver-turso/src/spec/turso.zod.ts", "packages/drivers/driver-turso/src/turso-driver-local-encryption-key-refusal.test.ts", "packages/drivers/driver-turso/src/turso-driver.ts", "packages/services/service-datasource/src/turso-driver-config.ts", "packages/spec/src/data/datasource-credential-redaction.ts", "packages/spec/src/data/driver/turso.test.ts", "packages/spec/src/data/driver/turso.zod.ts", "packages/spec/src/migrations/entries/semantic/18.turso-config-local-encryption-key-refused.ts" ], "line_budget": "n/a — no skills/** or governed surface touched; this round 14 files +376 / -45 vs d15981a07; the PR 17 files +996 / -51 vs origin/main (under the 3000-line threshold)", "deviations": [ "CONFLICT, named rather than chosen silently: the dispatch said 'update the PR body'; the role file says the dev writes the PR body once, at pr_create, never PATCHes it, and names the change for the seat to write (and caps dev writes at push + pr_create + report comment); on conflict the role file governs. So this run did not PATCH. The replacement body (20684 bytes, Clause-② line unchanged as line 2, no angle brackets, session-URL footer) is ready at /tmp/claude-0/-home-user/9fa8eee6-8749-54f5-979b-34f1853aa79d/scratchpad/issue-25020/r1/pr-body-r1.md for the seat's issue_patch on #25045. Until then the stored body reads round 0 (head d15981a07, no forced-remote arm).", "The constructor arm is written as ONE mode-independent predicate (a non-empty key beside a file: url or :memory:), choosing its text by mode; in a local or replica mode it is the old guard exactly, because localEngineDefect has refused every other url before it.", "Ablation leg D was taken through source, with no rebuild: the refusal file and the parity table's constructor half import turso-driver.ts directly. The new remote arm in the spec and the mirror was not ablated (one leg was ordered, over the constructor arm).", "The in-memory variant's text adds no remedy beyond 'Drop encryptionKey' (an in-memory database has no file to put on an encrypted volume).", "An interrupted dogfood run (worker restart) left no verdict and is not counted; my stale pid files in issue-25020/ were removed by literal path (a first rm with a variable path was refused by the harness safety check and did not run)." ], "open_questions": [], "out_of_scope_findings": [ "carrier: #25050 (filed by the seat) · the remote-url mode (libsql://, https://, ws(s)://) still accepts and drops encryptionKey; unchanged by this round, and pinned as an accept control in the parity table and the refusal file at the dispatch's order, so #25050's route will turn those controls red on purpose" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
os-dev-report
{ "issue": 25020, "status": "done", "branch": "claude/issue-25020-turso-encryption-key-local", "pr": "https://github.com/objectstack-ai/objectstack/pull/25045", "session": "session_01QK6pdbxKZiHdFApL8RYVh9 — the dispatching seat's id (a mode:subagent run, so the id is the parent's); patch round 2 on the same Claim 6306742426 and branch; head df6803d15923a60a4dc32700d36b563f1b30a4e9 equals PR #25045's head (read after the push and again at the end)", "premise_still_valid": true, "summary": "Patch round 2 (seat order 6310662118; contract review 6310634449 PASS on 4a595ed43). One changeset sentence rewritten, nothing else: in .changeset/25020-turso-encryption-key-local.md, 'The message is the same text at all three places, and a test holds the constructor's copy equal to the schema's issue byte for byte.' became 'For a local or replica datasource the message is the same text at all three places; for a `file:` url or `:memory:` under a forced `mode: 'remote'`, the schema and the constructor give the forced-remote text, while the driver mirror, which declares no `mode` and strips it, refuses the same config in the local text. A test holds the constructor's copy equal to the schema's issue byte for byte in every mode, and the mirror's verdict and words equal to the schema's wherever no `mode` is forced.' — which is what the parity table holds (byte equality constructor == spec over every copied-message row, forced-remote rows included; the mirror's verdict compared only on rows with no forced mode). `git diff --name-only 4a595ed43 HEAD` lists only `.changeset/25020-turso-encryption-key-local.md` (count 1), checked before the push, so the PASS record 6310634449 keeps governing; the Regen-provenance line is the seat's.", "tests": "No source, test or generated file moved this round (the diff is one changeset sentence), so no package suite was re-run; the round-1 runs at 4a595ed43 stand for the identical tree minus .changeset (driver-turso 93 files / 2538 passed | 47 skipped, spec 703 / 21383 passed | 1 todo, service-datasource 51 / 916 passed, all typechecks EXIT=0). At df6803d15: full turbo build 77/77 (75 cached), VERDICT command-exit 0; the full dogfood battery (below). No ablation: the diff changes no tested behaviour (a changeset is prose).", "gates": "At df6803d15: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 121 families, the same set as round 1. All 121 run once, each exit code captured to a per-command file before any pipe: 121 exit 0 (incl. check-changeset-no-major, check-adr-0087-registration and check-empty-changeset --base origin/main, each 0). --ran: 'Run reconciliation — 121 derived, 121 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). check:driver-conformance '55 covered cell(s), 0 in the DEBT ledger, 0 exempt'. check-changeset-no-major --base origin/main --event (the stored PR body's Clause-② line): exit 0, spec and driver-turso minor, service-datasource patch 'NOT refused'. PR check-runs on df6803d15, read once at the end: 36 success, 6 skipped, none failing or pending.", "dogfood_battery": "df6803d15 · os-verify-lock: VERDICT command-exit 0 (closure per ①, then pnpm --filter @objectstack/dogfood test, no shard, no path) · Test Files 284 passed | 3 skipped (287) · Tests 2361 passed | 18 skipped (2379) · no failure", "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "1 this round — one relay stroke, POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: comment -> POST /repos/objectstack-ai/objectstack/issues/25020/comments (this report). Not REST: git push of df6803d15. No PR body PATCH, no labels.", "files_changed": [ ".changeset/25020-turso-encryption-key-local.md" ], "line_budget": "n/a — no skills/** or governed surface touched; this round 1 file, +1 / -1 vs 4a595ed43", "pr_body_delta_for_the_seat": [ { "old": "**Every reading below was taken at head `4a595ed43`** unless it says otherwise;", "new": "**Patch round 2** (seat order 6310662118) landed as `df6803d15`: one changeset sentence, `.changeset/**` only. **Every reading below was taken at head `4a595ed43`** unless it says otherwise (the full dogfood battery, the derived gates and the check-runs were re-read at `df6803d15`);" }, { "old": "once, on the final head `4a595ed43`, closure built per ① and run under the lock as `pnpm --filter @objectstack/dogfood test` with no shard and no path: `VERDICT command-exit 0`, Test Files 284 passed, 3 skipped (287), Tests 2361 passed, 18 skipped. No failure. (A first attempt on this head was cut off by a worker restart before its verdict and is not counted.)", "new": "once, on the final head `df6803d15`, closure built per ① and run under the lock as `pnpm --filter @objectstack/dogfood test` with no shard and no path: `VERDICT command-exit 0`, Test Files 284 passed, 3 skipped (287), Tests 2361 passed, 18 skipped. No failure. (At `4a595ed43` the same battery also read `VERDICT command-exit 0`, 284 passed / 3 skipped, 2361 passed / 18 skipped.) At `df6803d15` the 121 derived gate families all exit 0 (`--ran`: 121 derived, 121 run, 0 NOT-MEASURED, 0 UNRUN), and the check-runs read 36 success, 6 skipped." } ], "deviations": [ "The rewritten text is two sentences where there was one; no other sentence in the changeset changed (git diff --word-diff shows the one line only).", "The PR body delta above is for the seat to write (role file: no dev PATCH); the Regen-provenance line is the seat's, per the order.", "The worktree had been removed after round 1, so it was re-added from origin/claude/issue-25020-turso-encryption-key-local at 4a595ed43 (equal to the PR head) before the edit; a first lock request for the build timed out in the queue (exit 99, never acquired) behind another seat's dogfood and was re-requested under the same slot." ], "open_questions": [], "out_of_scope_findings": [ "carrier: #25050 · unchanged this round: the remote-url mode still accepts and drops encryptionKey; the seat's noted nit (remote-url lists omit http://) is incomplete, not false, and was not reworked, per the order" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
ACCEPT → PR #25045 comment 6311723823 · contract review PASS 6310634449 on
4a595ed43, carried todf6803d15by Regen-provenance 6310718106 (prose-only) · ready + auto-merge armed 2026-10-21T08:38Z. The merge closes this card viaFixes. Thelibsql://remote mode is #25050.Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
Landed ·
domain:engineseat 2 ·session_01QK6pdbxKZiHdFApL8RYVh9· 2026-10-21T09:14Zmerged: true, merge commit28c9a2cb6, through the merge queue on its first entryorigin/main28c9a2cb6 fix(spec,driver-turso)!: refuse encryptionKey on a local or replica turso datasource instead of writing the file in plaintext (#25045)gh-readonly-queue/main/pr-25045-*gonecompletedbyFixes #25020; #25050 still open (no wrong close)State:
pm:dispatchedremoved in this act.encryptionKeybeside afile:url or:memory:is now refused at the spec, the driver mirror and the constructor, in every mode. That covers a forcedmode: 'remote'.libsql://remote mode, which still accepts and drops the key, is turso: after #25020 a remote datasource's encryptionKey is still parsed and silently dropped, so the key is honoured in no mode; retire it (ADR-0049 enforce-or-remove) #25050. Its route (retire the key, A) is triage's to set, and itsBlocked-by: #25020is now cleared.Generated by Claude Code