Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
f3af9af
feat(policy): add permissions for vault, secrets, config writes and d…
damusix Jul 29, 2026
6b208ee
fix(identity): reject malformed key material instead of deriving a co…
damusix Jul 29, 2026
640558c
fix(settings): stop persisting env overlay into settings.yml
damusix Jul 29, 2026
8323745
fix(state): stop rewriting state.enc on every load
damusix Jul 29, 2026
2a2c9b7
fix(runner): make createOperation work on MySQL
damusix Jul 29, 2026
bd9c25b
fix(change): restore re-apply after revert and teardown
damusix Jul 29, 2026
9f6efe0
fix(update): validate version before it reaches a URL or shell
damusix Jul 29, 2026
e9e9d64
fix(ci): refuse enrollment when the stored public key differs
damusix Jul 29, 2026
c52d7a1
fix(config): gate import --force behind config:write
damusix Jul 29, 2026
b16f94e
fix(identity): guard init --force behind confirmation and a key backup
damusix Jul 29, 2026
1620fed
fix(logger): reopen write stream after rotation
damusix Jul 29, 2026
0464ba4
fix(runner): dedup templates on the rendered SQL, not raw bytes
damusix Jul 29, 2026
eb4ecd6
fix(change): make rewind honour --dry-run and a count
damusix Jul 29, 2026
cfd86b8
fix(init): write a real ignore entry in the .gitignore block
damusix Jul 29, 2026
93bbe55
chore(config): drop the debug-process test artifact
damusix Jul 29, 2026
467d313
fix(identity): bind executed_by to the enrolled cryptographic identity
damusix Jul 29, 2026
aa23b9d
fix(identity): recompute the identity hash on CLI edit
damusix Jul 29, 2026
39b6bad
fix(settings): type-check the env overlay before use
damusix Jul 29, 2026
83b5b48
fix(tui): gate db transfer through the policy check
damusix Jul 29, 2026
003767e
fix(change): fail a revert whose state cannot be read
damusix Jul 29, 2026
f685f04
fix(worker-bridge): fail requests when a worker dies
damusix Jul 29, 2026
8e092cb
fix(dt): page by primary key and drain on settled promises
damusix Jul 29, 2026
33400c2
fix(debug): gate internal-table operations behind policy
damusix Jul 29, 2026
cfb3613
fix(policy): stop EXPLAIN and quoted identifiers evading the gate
damusix Jul 29, 2026
e4ff888
fix(state): serialize and reconcile concurrent state writes
damusix Jul 29, 2026
8f4ebcc
fix(config): gate export behind secret:read
damusix Jul 29, 2026
fe58dd4
fix(config): honour --json in the add and edit stubs
damusix Jul 29, 2026
9d2d8d9
fix(cli): honour settings.logging in the CLI logger
damusix Jul 29, 2026
a6ce8a6
fix(template): close the unsandboxed-execution paths in context building
damusix Jul 29, 2026
2d51292
test(impersonate): cover the authorization boundary and injection guard
damusix Jul 29, 2026
120c9c1
fix(debug): surface query failures instead of empty results
damusix Jul 29, 2026
80d6f42
fix(change): give change templates their config and secrets
damusix Jul 29, 2026
e83313d
fix(change): keep manifest files in the order listed
damusix Jul 29, 2026
a5a913b
fix(transfer): stop postgres self-copying on same-server path
damusix Jul 29, 2026
4df4704
fix(tui): honour global dry-run in truncate and teardown
damusix Jul 29, 2026
1e95b72
fix(cli): target the active config in sql history and clear
damusix Jul 29, 2026
cd6304a
fix(logger): redact project env vars, DSNs and error messages
damusix Jul 29, 2026
e448176
test(explore): add a Kysely harness that compiles SQL for real
damusix Jul 29, 2026
cb1bf7b
fix(explore): make explore answer with the schema it was asked about
damusix Jul 29, 2026
2fd2b56
fix(state): fail closed on unrecognised config access
damusix Jul 29, 2026
aaf0e6d
fix(sql-terminal): write query history owner-only
damusix Jul 29, 2026
1859413
fix(change): warn when the changes directory is missing
damusix Jul 29, 2026
888500f
fix(sql): keep ungated exec off the barrel, fix two dead examples
damusix Jul 29, 2026
6fdecff
fix(dt): stop trusting the contents of a .dt file
damusix Jul 29, 2026
9d5c781
fix(tui): confirm vault propagate and name recipients
damusix Jul 29, 2026
8306e44
fix(runner): gate preview and inspect on the access policy
damusix Jul 29, 2026
01b2258
fix(change): hide the teardown marker and show orphaned changes
damusix Jul 29, 2026
329cbe0
feat(vault): gate vault operations on the access policy
damusix Jul 29, 2026
c1e95a4
fix(vault): report partial and failed vault propagation
damusix Jul 29, 2026
ba6ff86
fix(vault): show who propagate grants to before granting
damusix Jul 29, 2026
88945cc
fix(vault): confirm destructive vault commands and accept stdin
damusix Jul 29, 2026
e96e989
docs: correct the paths.changes env var name
damusix Jul 29, 2026
85270ce
fix(vault): make vault cp --dry-run run the real preflight
damusix Jul 29, 2026
431a8fa
fix(secret): gate secret commands and honour NOORM_YES
damusix Jul 29, 2026
3c0f40f
feat(sdk): gate vault and secrets namespaces, round out secrets
damusix Jul 29, 2026
d2700d0
merge(f12): update semver validation, log rotation, logging config an…
damusix Jul 29, 2026
5105443
fix(runner): execute every statement of a SQLite file
damusix Jul 29, 2026
93a1bdf
chore(changeset): note change apply/revert and rewind fixes
damusix Jul 29, 2026
19f00b3
fix(lock): store and read lock timestamps in UTC
damusix Jul 29, 2026
3a8194f
test(policy): check classifier verdicts against real databases
damusix Jul 29, 2026
be028ab
fix(runner): write dry-run output owner-only and report its path
damusix Jul 29, 2026
6496a6b
test(policy): assert which classification path each test exercises
damusix Jul 29, 2026
167ae1a
fix(state): reject malformed keys and stamp the derivation used
damusix Jul 29, 2026
cdf0b62
docs(cli): say `sql query` does not record history
damusix Jul 29, 2026
5a8a592
feat(runner): warn when a build.exclude entry matches nothing
damusix Jul 29, 2026
7baa212
fix(db): gate db create on the access policy
damusix Jul 29, 2026
ef8cedc
fix(cli): make --schema on explore list commands actually filter
damusix Jul 29, 2026
cb1b4b1
fix(db): require confirmation for truncate and teardown
damusix Jul 29, 2026
e1bd117
fix(teardown): qualify truncate statements with the table schema
damusix Jul 29, 2026
8f72fa8
fix(connection): stop testServerOnly creating the sqlite target
damusix Jul 29, 2026
5ea9760
docs(execution): record the dedup key and the real per-dialect behaviour
damusix Jul 29, 2026
ac7db88
merge(F13 debug authorization and error surfacing)
damusix Jul 29, 2026
53a7b2e
merge(F09 config and settings env-overlay leak)
damusix Jul 29, 2026
b5e5dad
merge(F04 change re-apply, teardown recovery, rewind dry-run)
damusix Jul 29, 2026
650c385
merge(F08 vault and secrets authorization)
damusix Jul 29, 2026
96f579c
fix(lock): make lock acquisition atomic
damusix Jul 29, 2026
488edd7
merge(F06 state durability, migration and fail-closed access)
damusix Jul 29, 2026
e6dcbe3
fix(lock): gate lock force and report what it evicted
damusix Jul 29, 2026
96b7bd4
fix(ci): back up state before init --force destroys it, and validate …
damusix Jul 29, 2026
58335f5
test(impersonate): drop the test role's grants so teardown is idempotent
damusix Jul 29, 2026
fbf22e8
merge(F02 lock UTC expiry, atomic acquire, force gating)
damusix Jul 29, 2026
c395607
test(teardown): pre-confirm the sdk preserve integration context
damusix Jul 29, 2026
cb1d863
fix(transfer): report rows written, not rows attempted
damusix Jul 29, 2026
0076d30
feat(cli): add `db explore triggers` and show every listable counter
damusix Jul 29, 2026
fa3d8fc
test(explore): guard sqlite trigger parsing against real sqlite_maste…
damusix Jul 29, 2026
97274ce
fix(cli): export all tables when --export omits --tables
damusix Jul 29, 2026
0ae3669
merge(F05 runner sidecar gating, template dedup, mysql tracker, sqlit…
damusix Jul 29, 2026
0f26679
merge(F10 db lifecycle core-seam gating and truncate schema)
damusix Jul 29, 2026
6a13269
merge(F01 SQL classifier EXPLAIN/DDL bypass and dialect-aware splitting)
damusix Jul 29, 2026
d29d06e
fix(transfer): make cross-dialect transfer actually work
damusix Jul 29, 2026
434a351
fix(change): retrieve insert ids on mysql
damusix Jul 29, 2026
96ef43c
fix(transfer): gate plan reads and surface partial outcomes
damusix Jul 29, 2026
dbf4dbc
merge(F07 identity key validation, enroll public-key check, destructi…
damusix Jul 29, 2026
7ccba53
merge(F11 explore correctness, schema scoping and real query-compilin…
damusix Jul 29, 2026
f166ca4
chore(changeset): note the mysql insert-id fix
damusix Jul 29, 2026
934b8fb
merge(F04 mysql change history: createOperation and recordReset)
damusix Jul 29, 2026
a75bd51
fix(tui): add missing hook dependencies
damusix Jul 29, 2026
42e3a03
merge(F03 transfer keyset paging, worker settlement, dt hardening)
damusix Jul 29, 2026
8676dea
fix(policy)!: stop granting MCP admin by default
damusix Jul 29, 2026
d396226
fix(tui): stop clobbering .noorm/.gitignore on re-init
damusix Jul 29, 2026
42db733
merge(F14 TUI confirm-tier gating, dry-run threading, stale-closure d…
damusix Jul 29, 2026
da9f55e
test(integration): confirm destructive db operations explicitly
damusix Jul 29, 2026
0832667
fix(ci): gate identity enroll on vault:propagate
damusix Jul 29, 2026
cd0ff17
fix(sdk): stop a failed or forgotten revert from wedging the connection
damusix Jul 29, 2026
4ec666a
fix(tui): unwrap the vault recipients tuple
damusix Jul 29, 2026
f9662b3
refactor(shared): unify operation-record creation across dialects
damusix Jul 29, 2026
b960d98
test(shared): pin the per-dialect id-retrieval contract
damusix Jul 29, 2026
5d0b617
merge(F16 consolidate dialect-aware operation-record insertion)
damusix Jul 29, 2026
6ffb553
docs(policy): record the agent-channel access default
damusix Jul 29, 2026
f1e214f
fix(tui): consume the vault recipients tuple instead of nesting it
damusix Jul 29, 2026
4704039
docs(policy): drop stale admin/admin references
damusix Jul 29, 2026
e32cb3e
merge(F07 enroll vault gating, sdk scope revert and disconnect)
damusix Jul 29, 2026
10929f9
merge(F15 default mcp channel to viewer instead of admin)
damusix Jul 29, 2026
e300ee1
chore(changeset): describe the v1 readiness fixes
damusix Jul 29, 2026
8082968
feat(cli)!: separate partial, total and usage exit codes
damusix Jul 29, 2026
370fd17
feat(cli)!: pin one --json success envelope across every command
damusix Jul 29, 2026
2d78c12
fix(cli): stop reporting success for targets that were never there
damusix Jul 29, 2026
6e63767
fix(cli): restore run exec under node, and let CI set lock metadata
damusix Jul 29, 2026
f63036b
docs(headless): write down the --json envelope and the exit-code cont…
damusix Jul 29, 2026
5ca233d
merge(F17 unified json envelope, exit-code scheme, no-op-as-success s…
damusix Jul 29, 2026
735d5ea
fix: the access channel names who is driving, not which binary was in…
damusix Jul 30, 2026
e34f937
merge: master (agent skill docs, docs build fix)
damusix Jul 30, 2026
0cd96e5
test(cli): report what the settings layer resolved when the logger as…
damusix Jul 30, 2026
0f2e7c8
ci: isolate the CLI logger settings tests in their own process
damusix Jul 30, 2026
b207ba6
test(ci): make the failure fixture actually contain invalid SQL
damusix Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
38 changes: 38 additions & 0 deletions .changeset/agent-channel-not-transport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
"@noormdev/cli": major
"@noormdev/sdk": major
---

Resolve the access channel from who is driving, not which binary was invoked

`Channel` used to name the transport: `user` meant the CLI/TUI/SDK and `mcp`
meant the MCP server. Those only coincide when a human is at the keyboard. An
agent refused a write over MCP could see `noorm` on the PATH and shell out,
and because the CLI hardcoded `user` at every policy call site, that second
attempt ran with the human's role. On a stock config that turned deny into
allow for `sql:write`, `sql:ddl`, `db:create`, `run:build` and `vault:read`,
and turned `db:destroy` into a confirm that `--yes` satisfied.

Two breaking changes:

**The config fields are renamed.** `Channel` is now `'user' | 'agent'`, and
`ConfigAccess` is `{ user, agent }` instead of `{ user, mcp }`. `agent: false`
hides a config from agents on *both* transports, not just over MCP. Stored
state migrates automatically (state schema v3) and carries every value over
verbatim — `mcp: 'operator'` becomes `agent: 'operator'`, `mcp: false` becomes
`agent: false`. SDK callers passing `channel: 'mcp'` to `createContext` must
pass `'agent'`, and anything reading or writing `config.access.mcp` must use
`config.access.agent`. In the TUI, the "MCP Role" field is now "Agent Role".

**Agents shelling out to the CLI now get the agent role.** The CLI resolves
its channel from provenance via `resolveChannel()`: an allowlist of variables
the agent harnesses (Claude Code, Codex, Cursor, Gemini CLI) set for their own
child processes. A stock config gives agents `viewer`, so commands that used
to succeed inside an agent session are now refused — that is the fix, not a
regression. `TERM_PROGRAM`, `CI` and TTY state are deliberately not consulted;
they describe the terminal or the pipeline, not the caller.

Set `NOORM_CHANNEL=user` to opt out when a human is scripting from inside an
agent session, or `NOORM_CHANNEL=agent` to opt in with no harness present. An
agent can set that variable too; this defends against one routing around a
refusal, not one deliberately evading the check.
11 changes: 11 additions & 0 deletions .changeset/agent-provenance-executed-by.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@noormdev/cli": minor
"@noormdev/sdk": minor
---

## Added

* `feat(identity):` Record the detected agent harness in operation provenance. When a session runs under a recognised agent harness, `executed_by` is suffixed with `(via <harness>)` — so a change applied by an agent is distinguishable from one a human applied. Stamped at the shared insert seam, so change operations, resets and run operations all carry it on every dialect.
* `feat(cli):` `noorm info` reports the detected harness and the environment variables that identified it, so an agent-driven session is visible rather than silent.

Provenance is folded into the existing identity string rather than a new column: the audit question is binary, and a suffix answers it without a four-dialect schema migration. It is not an attestation — `executed_by` is unauthenticated free text and harness detection reads caller-controlled environment variables, so the suffix records what noorm observed, not a proven claim.
16 changes: 16 additions & 0 deletions .changeset/change-reapply-and-rewind-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@noormdev/cli': patch
'@noormdev/sdk': patch
---

Fix change apply/revert recovery, rewind flag handling, and MySQL support

- Changes now run on MySQL at all. `ChangeHistory` retrieved insert ids with a `RETURNING` clause MySQL does not support, so no operation record was ever created and `change run`, `change ff`, `change revert` and `db teardown` were all inoperable.
- A reverted or torn-down change can be applied again. Every file was previously skipped against a prior success, so `apply -> revert -> apply` and `db teardown -> change ff` reported success over an untouched database.
- `ff` and `next` now treat `stale` changes as pending work, so teardown has a supported recovery path.
- `change rewind` honours `--dry-run` and `--force`, and accepts the documented count form (`change rewind 3`). `changes.rewind()` takes an options argument.
- A revert whose history cannot be read fails instead of reporting success over zero files.
- `.sql.tmpl` files inside a change now receive `$.config` and `$.secrets`.
- `.txt` manifests execute in the order they list files, instead of being sorted.
- `ff`/`next` warn when the changes directory is missing rather than reporting a clean run.
- `change list` marks orphaned changes and no longer lists the internal `__reset__` teardown marker.
29 changes: 29 additions & 0 deletions .changeset/headless-envelope-and-exit-codes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@noormdev/cli": major
"@noormdev/sdk": major
---

## Headless contract: one `--json` envelope, four exit codes

### Changed

* `fix(cli)!:` every `--json` payload is now a JSON **object** carrying a top-level boolean `success`, and is never a bare array. `success` is `true` if and only if the process exits `0`, so `jq -e '.success'` is a failure check that works against every command.
* **Commands that returned a top-level array now return a named object.** `change list` → `.changes` (plus `.pending`), `change history` → `.history`, and every `db explore` list — `tables`, `views`, `indexes`, `fks` (→ `.foreignKeys`), `functions`, `procedures`, `types`, `triggers` — under the matching key. A script doing `jq '.[]'` on any of those must now name the key.
* **Every other payload gained `success` and lost nothing.** Existing key reads keep working.
* `config export --json` now emits the envelope (`{success, name, config}`). The bare artifact — the shape `config import` reads — is still what `noorm config export dev > dev.json` produces, unchanged.
* `noorm update --json` no longer prints two JSON documents for one error, and no longer reports `success: true` on a failed install while exiting non-zero.
* `fix(cli)!:` exit codes now distinguish four outcomes: `0` success, `1` total failure, `2` usage error (bad or missing flag, a TTY-only command run non-interactively, or a named target that does not exist), `3` partial failure.
* **`2` no longer means "partial".** It previously meant partial failure on `run build` / `run dir` / `run files` / `run exec` and the five `change` execution commands — but the same `2` was also returned for a *total* failure on those commands, so the two were indistinguishable. Partial now has its own code. **A pipeline testing `[ $? -eq 2 ]` for "partially applied" must now test `-eq 3`.** Checks of the form `if ! noorm …` are unaffected.
* `ci secrets` partial loads move from `2` to `3`. `db transfer`'s total-failure exit moves from `2` to `1` (its partial exit stays `3`). `vault propagate` and `vault rm`, `dev test-workers` and `dev test-helpers` now report partial and not-found instead of collapsing everything to `1`.
* A "you named something that isn't there" failure — missing file, directory, config, change, secret key, table, or a glob matching nothing — moves from `1` to `2` across the CLI. Confirmation and `--force` refusals deliberately stay at `1`.

### Fixed

* `noorm run inspect <missing-file>` reported a fully-populated context and exit `0` for a file that was never on disk — it only ever read the template's *directory*. It now fails with exit `2`. `run preview` gained the same up-front check so both surfaces agree.
* `noorm run dir` reported success over a directory that contained no SQL files, and reported a missing directory as a SQL failure. Both now exit `2` with a message naming the path.
* `noorm run exec` died with `Bun is not defined` under Node — the documented dev entry point — before doing any work. Glob expansion falls back to `node:fs/promises` when the `Bun` global is absent; the compiled binary keeps Bun's own matcher.
* `noorm run preview --json` put a full stack trace, including absolute filesystem paths, into the machine-readable `error` field. `--json` now carries the message only; the stack still reaches an operator on stderr.
* `noorm lock acquire` gained `--timeout` and `--reason`, which the TUI has always collected — CI-acquired locks were permanently default-timeout and reasonless, so whoever they blocked had no way to see what they were waiting on.
* `withVaultContext` never passed `yes` to the SDK context, so `--yes` / `NOORM_YES` was inert for every vault command.
* `db transfer` declared a `--force` flag that nothing read.
* `run exec`, `db reset`, `vault cp`, and `dev test-helpers` wrote some errors straight to stderr, leaving `--json` callers with an empty stdout and no envelope to parse.
15 changes: 15 additions & 0 deletions .changeset/mcp-default-viewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@noormdev/cli": major
"@noormdev/sdk": major
---

## Access default: agents no longer get admin

### Changed

* `fix(policy)!:` a config with no explicit `access` now resolves to `{ user: 'admin', mcp: 'viewer' }` instead of `{ user: 'admin', mcp: 'admin' }`. A stock project writes no `access`, so every config was handing full admin to any connected MCP client — writes, DDL, `change_run`, `run_build`, database drops, and vault/secret reads all went through unchecked.
* **The `user` channel is unchanged.** Nothing about the CLI, TUI, or SDK behaves differently.
* **MCP agents keep read access.** `explore` and read-only SQL (`SELECT`, `EXPLAIN`, `SHOW`, `DESCRIBE`) still work with no setup.
* **MCP agents lose write access by default.** If an agent needs to write, run changes, or run builds, set that config's `access.mcp` to `operator` or `admin` explicitly — via `noorm ui` → Config → Edit, or in the JSON before `config import`.
* **Configs that already store an explicit `access` are untouched**, including ones holding `mcp: 'admin'`. If you created configs on an earlier build, that migration already wrote `admin/admin` to disk and it is preserved — run `noorm config list` and lower any `mcp:admin` you did not intend.
* `noorm config list` now prints the access tag for any config that differs from the default, so an `mcp: admin` escalation is visible. Previously the tag was hidden whenever the user channel was `admin`, which concealed exactly that case.
11 changes: 11 additions & 0 deletions .changeset/mssql-connect-by-ip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@noormdev/cli": patch
"@noormdev/sdk": patch
---

## MSSQL connects by IP address

### Fixed

* `fix(connection):` connecting to MSSQL by IP address works again. Tedious derived the TLS SNI ServerName from the host, and Node rejects an IP literal there (RFC 6066), so every IP connection failed with `Setting the TLS ServerName to an IP address is not permitted`. Hostname connections are unchanged. Encryption stays on — the connection is never silently downgraded to plaintext to work around it.
* `feat(connection):` new optional `connection.tlsServerName` — the hostname the server's TLS certificate is issued for. Required when connecting to an IP address with certificate validation enabled (`ssl` set), since a certificate cannot be validated against an IP. Without it, that combination now fails with an error naming the field instead of an opaque TLS error.
22 changes: 22 additions & 0 deletions .changeset/v1-authorization-seam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@noormdev/cli": major
"@noormdev/sdk": major
---

## Authorization is now enforced at the core seam

Policy was re-implemented per surface, and several cells were simply empty — the CLI would perform operations the TUI correctly refused, and two whole domains had no permission to check against.

### Fixed

* `fix(policy)!:` the SQL classifier no longer routes writes as reads. `EXPLAIN (ANALYZE) DELETE FROM t` classified as `read` and deleted rows under a `viewer` role — through the CLI and over a live MCP session. Two independent causes are fixed: the keyword fallback treated `EXPLAIN` as terminally read-only, and on the parser path `EXPLAIN` inherited nothing from the statement it wraps while the write-signal scan had no DDL node types. An `EXPLAIN` now inherits its wrapped statement's classification.
* `fix(policy)!:` statement splitting is dialect-aware. It tracked only `'`, so an MSSQL bracket-quoted identifier (`SELECT 1 AS [a'b]; DROP TABLE x`) split wrong and the tail ran unclassified. `"`, backticks, `[…]`, `$tag$…$tag$` and all three comment forms are now modelled.
* `fix(policy)!:` a statement with no leading word character used to classify as `read`, so a leading NUL byte carried `DROP TABLE` past the gate. It now fails closed.
* `feat(policy)!:` added the permissions that did not exist — `vault:*`, `secret:*`, `config:write`, `db:truncate`, `db:teardown`, `transfer:plan`, `lock:force`, `debug:*`. Operations in those domains had nothing to gate against and shipped ungated.
* `fix(vault)!:` vault and secret operations are gated at the core seam, so SDK, CLI and TUI inherit one check. A `viewer` config that was denied `run build` could still run `vault set`, `vault rm`, `vault propagate` and `secret set`.
* `fix(db)!:` `db create` is gated. The matrix denied `db:create` to `viewer` and the TUI enforced it; the CLI created the database anyway.
* `fix(db)!:` `db truncate` and `db teardown` require confirmation. `--yes`/`--force` were declared arguments that neither command read, so `noorm db teardown < /dev/null` dropped 63 objects and exited 0 — against documentation promising the CLI refuses.
* `fix(debug)!:` the debug screens' delete paths are gated. They removed rows from `noorm.vault` and `noorm.identities` with no authorization check at all.
* `fix(lock)!:` `lock force` is gated and requires `--yes`, names the holder it evicts, and returns a distinct exit code when there was nothing to release.
* `fix(config)!:` `config import --force` is gated on `config:write`. It rewrote a config's `access` with no check, escalating a `viewer` config to full delete rights in one command.
* `fix(transfer):` `getTransferPlan` is gated, matching the invariant its own comment claimed. `--dry-run` leaked destination table names, row estimates and the FK graph to a denied caller.
15 changes: 15 additions & 0 deletions .changeset/v1-crypto-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@noormdev/cli": major
"@noormdev/sdk": major
---

## Identity and state key handling fail closed

### Fixed

* `fix(identity)!:` malformed key material is rejected instead of deriving a publicly computable key. `Buffer.from(key, 'hex')` never throws, so any non-hex private key collapsed to a zero-length HKDF input and produced a **constant** AES key — an attacker holding no key material could decrypt a real `state.enc`. `isValidKeyHex` existed but was only called on the CI path; it now guards every entry point.
* `fix(ci)!:` `ci identity enroll` verifies the stored public key matches the one presented. It checked only that a row existed, so anyone able to INSERT into `noorm.identities` — no vault access required — could pre-plant their own key and receive the vault key, while the operator's enrollment reported success and echoed back the expected key.
* `fix(identity)!:` `identity init --force` requires `--yes`, backs up the keypair, and names what it destroys. It silently regenerated the keypair and permanently destroyed every project's `state.enc`.
* `fix(ci):` `ci init --force` backs up `state.enc` and refuses on a TTY without confirmation; it destroyed all configs and secrets unprompted. It also now routes through `parseConfig`, so a numeric-looking database name persists as a string.
* `fix(vault)!:` `vault propagate` names every recipient and withholds until confirmed, and reports partial failures instead of returning success with the failed identity silently omitted.
* `fix(sdk):` an explicit identity scope whose revert fails is no longer marked reverted, and `disconnect()` no longer hangs when scopes are still held.
21 changes: 21 additions & 0 deletions .changeset/v1-data-integrity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@noormdev/cli": major
"@noormdev/sdk": major
---

## Operations no longer report success over lost, duplicated or unwritten data

### Fixed

* `fix(transfer)!:` export and db-to-db transfer paginate by primary key instead of bare `LIMIT/OFFSET`. With no `ORDER BY`, a source being written to during a 50,000-row export produced 14,149 missing and 13,387 duplicated rows while reporting `rowsExported: 50000` and exiting 0. Tables with no primary key now take a single-statement snapshot rather than corrupting silently.
* `fix(dt)!:` a malformed row or a dead worker no longer hangs the pipeline forever. The error branch never decremented the in-flight count, `request()` never settled on worker death, and `worker:exit` had no subscribers — a four-byte bad payload wedged `db transfer --import` indefinitely. Three busy-wait loops are replaced with real settlement.
* `fix(transfer)!:` the postgres same-server path no longer copies the destination into itself. It ignored the source database entirely and could only ever emit `INSERT INTO t SELECT ... FROM t`.
* `fix(dt):` decompression is bounded on import. An unbounded `gunzip` inflated 398 KB to 400 MB (1029:1), and because it runs in a compute worker the resulting OOM triggered the hang above.
* `fix(transfer):` reported counts reflect reality — `onConflict: 'skip'` no longer counts no-ops as inserted, and the same-server path reports affected rows instead of a postgres `reltuples` estimate.
* `fix(transfer):` `db transfer --export` with no `--tables` exports every table, as its own flag description promised. It exported nothing, printed `success: true` and exited 0.
* `fix(transfer):` cross-dialect transfer works. The planner probed the destination with the *source* dialect, so every cross-dialect transfer aborted and roughly 400 lines of streaming conversion were unreachable — while the docs claimed the feature worked.
* `fix(teardown)!:` `truncate` qualifies statements with the table's schema. It discarded the schema, emptied 12 tables and then aborted — partial irreversible loss, reported as a failure.
* `fix(state)!:` `state.enc` writes are atomic and reconciled. Ten concurrent `noorm secret set` runs left five secrets, all exiting 0. Writes now stage-and-rename with an exclusive lock, three-way reconcile against a changed file, and keep a `.bak` generation.
* `fix(state):` reads stopped rewriting state. `needsMigration` tested a field the migration never writes, so it was always true and every command — including `config list` — re-encrypted and rewrote `state.enc`, turning reads into writes and amplifying the loss above.
* `fix(runner):` `.sql.tmpl` dedup works. The stored checksum was the raw file hash while the comparison used the rendered hash, so templates re-executed on every build and failed on non-idempotent DDL.
* `fix(runner):` SQLite executes every statement in a multi-statement file instead of silently running the first and reporting success.
14 changes: 14 additions & 0 deletions .changeset/v1-explore-correctness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@noormdev/cli": minor
"@noormdev/sdk": minor
---

## Schema exploration answers about the object you asked for

### Fixed

* `fix(explore):` procedure detail returns parameters. It filtered `information_schema.parameters` on a bare name where the column holds `name_oid`, so the list view reported a parameter count and the detail view always showed none — on every surface.
* `fix(explore):` SQLite quotes identifiers at all raw-SQL sites. A single table named with an embedded quote broke listing, overview and detail for *unrelated* tables.
* `fix(explore):` MySQL table detail reads indexes and foreign keys from the requested schema rather than the connected database, which produced self-contradictory output with the real index missing.
* `fix(explore):` `--schema` is honoured on the list commands, which declared it and ignored it.
* `fix(explore):` the overview counts from the same listings the detail views use, inside one guarded call that surfaces errors, instead of a second implementation that disagreed and hardcoded several counters to zero.
12 changes: 12 additions & 0 deletions .changeset/v1-mysql-dialect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@noormdev/cli": patch
"@noormdev/sdk": patch
---

## MySQL works

### Fixed

* `fix(runner):` `run build` executes on MySQL. `createOperation` emitted a `RETURNING` clause MySQL has no support for, so the operation record was never created and **zero SQL files ran** — the command failed before touching a single file.
* `fix(change):` `recordReset` no longer fails silently on MySQL. It swallowed the same error and returned `0`, so `db teardown` recorded nothing and reported no error at all.
* `refactor(shared):` operation-record insertion is one dialect-aware helper. It existed as three independent copies, which is why fixing the runner left the change module broken and the same defect had to be found twice.
Loading
Loading