Repository navigation
Commit 5018fc3
fix(plugin-audit): an object definition changed through the metadata door takes effect without a restart (#24676)
Fixes #24565
Clause-②: no
## What changed
`installAuditWriters`
(`packages/plugins/plugin-audit/src/audit-writers.ts`) kept two
process-lifetime copies of `engine.getSchema`: `objectDefCache` behind
`getObjectDef` and `fieldDefsCache` behind `getFieldDefs`. Each object's
FIRST answer was held until a restart. Both copies are gone.
`getObjectDef` now reads the registered definition on every call, and
`getFieldDefs` reads `.fields` off that same read.
That covers every reader at once: the `enable.files` / `enable.feeds`
capability gates, the ledger's credential mask and `internal` omission
(`ledgerView`), the activity mirror's `enable.activities` switch,
`activityMilestones`, the title field, the object label and the
computed-field drop. What the metadata door accepts is unchanged.
## Measured before choosing the route (on `f89d29e38`, real kernel)
Probe: `ObjectKernel` + `ObjectQLPlugin` (its in-house metadata protocol
registered as the `protocol` service) + `AuditPlugin` +
`SqliteWasmDriver`. It saved an object twice through
`protocol.saveMetaItem` (the producer behind `PUT
/api/v1/meta/object/:name`) and listened on every channel.
- **The defect reproduces.** First save, no `enable`, then the
attachment insert answers `FILES_DISABLED 403`. Second save with
`enable: { files: true }`: `success=true`, and
`engine.getSchema('qa_kid').enable.files` reads `true`. The same insert
still answers `FILES_DISABLED 403`.
- **Which signal fires on the door's save (A2):** only
`protocol.onMetadataMutation` (`object/qa_kid active`), and it fires
AFTER the registry holds the new definition (`files=true` at fire time).
`metadata:reloaded` does NOT fire on this save. It fires on per-item
publish, package publish-drafts and dev reload. The metadata service on
this composition has no `subscribe` at all.
- **Cost of reading per call (A3):** `ObjectQL.getSchema` is
`registry.getObject`, which is two map reads over the registry's own
memoized merge (`SchemaRegistry.resolveObject` / `mergedObjectCache`).
It measured 32 to 44 ns per call against 7 ns for a bare `Map.get`
(1,000,000 calls, shared box). The audited writes ask more often now:
one audited insert went from 16 `getSchema` calls to 23, and one audited
update from 9 to 25. That is under 1 microsecond per write, against a
mean audited insert of 3,301 us before and 3,077 us after on the same
harness (300 inserts each, shared-box noise).
## Route: drop the copy, not invalidate it (four axes)
The card's triage text says "invalidates on the metadata change event".
The dispatch offered dropping the copy as the alternative, to be chosen
on the four axes. This PR drops it:
- **Real business need.** Both routes fix the measured door. But the
door announces on a channel `metadata:reloaded` subscribers never see,
and the other doors that change a definition (per-item publish, package
publish, dev reload with `notify: false`, a peer replica's replay, a
delete, a plugin's own `registerObject`) announce on different channels
or not at all. A read per call follows all of them by construction. An
invalidation follows only the channels it subscribes to.
- **Long-term soundness.** The registry is the one owner of an object's
current definition and already memoizes it behind its own invalidation
points. Its own docblock (`resolveOwnerLayer`) refuses a second cache
for this reason. This package already reads per call where the
definition decides: `declaresActivityStampColumns` on every
`sys_activity` insert, and `currentMilestoneWatched` on the read side.
- **Keeping AI-authored metadata honest.** A declaration takes effect on
the next write, whatever door changed it. There is no announce for a
future door to forget.
- **Startup scope.** The smallest change: two maps deleted, no
subscription, no new gate. ⛔ No TTL either: an expiring copy is the same
stale answer for a shorter time.
## Tests
New `audit-writers.definition-follows-metadata.integration.test.ts`, on
the same real composition as the probe. Every definition change goes
through `protocol.saveMetaItem`, and there is no restart:
1. Object saved with no `enable`: attach refused (`FILES_DISABLED` /
403). Saved again with `enable.files: true`: the read-back shows `true`
and the same attach is admitted.
2. Reverse: saved with `files: true`, attach admitted. Saved with
`files: false`: the same attach is refused again.
3. Control (the card's): authored with `enable.files: true` from the
start, never looked up before: admitted.
4. Second reader, the ledger mask: an object audited once, then a
`password` field added through the door, then the record updated. The
`sys_audit_log` row's `new_value` carries the mask (`SECRET_MASK`),
never the value.
**Ablation** (on committed `56218f119`, through
`scripts/ablation-replace.mjs` in wrap mode with a restore trap): the
held copy was put back into `getObjectDef`. The anchor hit once (1 to
0), and the blob went from `e5840a3a6b5b` to `8bfd1f9e7ff2`. Cases 1, 2
and 4 went red; control 3 stayed green:
- 1: `expected { code: 'FILES_DISABLED', status: 403 } to deeply equal {
admitted: true }`
- 2: `expected { admitted: true } to deeply equal { code:
'FILES_DISABLED', status: 403 }`
- 4: red at its masking assertion (the ledger row carried the field's
value, not the mask).
The restore was proven by the tool: blob after restore equals the HEAD
blob, and `git diff HEAD` is empty.
## Local verification (head `30a57ba9d`)
- `pnpm --filter @objectstack/plugin-audit exec vitest run
--maxWorkers=2` (whole package, on `56218f119`): 58 files, 934 tests
passed. The only later commit (`30a57ba9d`) types the new test's option
bags. On that head the new file reran 4/4 green, and `pnpm --filter
@objectstack/plugin-audit typecheck` exited 0, with
`check:test-typecheck: OK` and 0 errors.
- Build: the dependency closure first (`turbo run build
--filter=@objectstack/plugin-audit^...`, 15/15), then the package
itself. All of it ran under `scripts/pm/os-verify-lock.sh`.
- The gate union from `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` (70 commands) was rerun in full on
`30a57ba9d`. All 70 exited 0. `dispatch-gates --ran` reads: `70 derived,
70 run, 0 NOT-MEASURED, 0 UNRUN`.
- In the first pass, on `56218f119`, `check:query-options-erasure` was
red: the test surface grew from 236 to 237 sites because the new test
passed its option bags `as any`. They are now typed with `satisfies`,
and the surface is back at 236.
- Also in that first pass, `check:dual-build-cjs-loads` and `check:i18n`
exited 3 (PREREQUISITE NOT MET: no workspace `dist/`). Both measured
green on the final head.
- The `test:repo` project and the full CI farm are left to CI.
## Acceptance notes
- **Rows written before this fix:** a security note on them is held off
GitHub for the maintainer (#9387). The seat carries it.
- **Same family, other package, not measured through a door:**
`@objectstack/metadata-core`'s `createFieldPresenceProbe` (field-set
cache) and `createResolver` (organization-column cache) also hold a
per-object answer for the process lifetime. Their docblock premise reads
"object schemas are static after registration". In this file they are
asked about `sys_user`, `sys_audit_log` and `sys_activity`, and about
the audited object's organization column. They sit outside this card's
file surface and were left untouched.
- `sys_attachment` in the test is a stand-in under the real name
(service-storage owns the definition). The gate reads only
`parent_object`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0431002 commit 5018fc3
3 files changed
Lines changed: 203 additions & 26 deletions
File tree
- .changeset
- packages/plugins/plugin-audit/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1389 | 1389 | | |
1390 | 1390 | | |
1391 | 1391 | | |
1392 | | - | |
1393 | | - | |
1394 | | - | |
1395 | | - | |
1396 | | - | |
1397 | | - | |
| 1392 | + | |
| 1393 | + | |
| 1394 | + | |
| 1395 | + | |
| 1396 | + | |
| 1397 | + | |
| 1398 | + | |
| 1399 | + | |
| 1400 | + | |
| 1401 | + | |
| 1402 | + | |
| 1403 | + | |
| 1404 | + | |
| 1405 | + | |
| 1406 | + | |
| 1407 | + | |
| 1408 | + | |
| 1409 | + | |
| 1410 | + | |
| 1411 | + | |
| 1412 | + | |
| 1413 | + | |
| 1414 | + | |
| 1415 | + | |
1398 | 1416 | | |
1399 | | - | |
1400 | | - | |
1401 | | - | |
1402 | | - | |
| 1417 | + | |
1403 | 1418 | | |
1404 | | - | |
| 1419 | + | |
1405 | 1420 | | |
1406 | | - | |
1407 | | - | |
1408 | 1421 | | |
1409 | 1422 | | |
1410 | | - | |
1411 | | - | |
1412 | | - | |
1413 | | - | |
1414 | | - | |
1415 | | - | |
1416 | | - | |
1417 | | - | |
1418 | | - | |
1419 | | - | |
1420 | | - | |
1421 | | - | |
1422 | | - | |
| 1423 | + | |
| 1424 | + | |
| 1425 | + | |
| 1426 | + | |
| 1427 | + | |
1423 | 1428 | | |
1424 | 1429 | | |
1425 | 1430 | | |
| |||
0 commit comments