Repository navigation
Commit a3d47c2
fix(metadata-fs, runtime): an actor-less filesystem write records null, never the label fs; the dispatcher metadata PUT records the user (#23109)
Part of #23086
Clause-②: no
This PR covers positions 2 and 3 of the `PutOptions.actor` producer
family. The contract (`packages/metadata-core/src/types.ts`, #4556) is
the acting user's id, or `null`, and never a label. It follows the
triage direction in comment 6131584326: positions 2 and 3 in one PR, one
pin per position, no second resolver, and no new repo-wide check.
Position 1 (plugin-security's `'system'`) is not edited here. The card
stays open so the seat can verify position 1 when #22975's 7a and 7b
land.
## Measured first, on `origin/main` `6662e3f590`
- **Position 1, confirmed and not edited.** Both modules still pass the
label. `permission-set-projection.ts:1658` has `actor: 'system'`, and so
does `position-environment-backfill.ts:299`. The same two hits are there
at `9956048b4c` and at `origin/main` `8464380248`.
- **Position 2: where `'fs'` lands.** `FileSystemRepository`'s
constructor set `this.fsActor = opts.fsActor ?? 'fs'`, and `fsActor`
reached four places:
- the event `actor` of an edit seen on disk (`handleFsChange`,
`publishExternalDelete`);
- `authoredBy` in `get()`, in the `list()` header, and in a no-op
`put()`, each spelled `meta?.actor ?? this.fsActor`.
`??` treats a recorded `null` as absent. So a `put()` with `actor: null`
read back from `get()` / `list()` as authored by `'fs'`, even though the
write and its log record held `null`. The new pin read exactly that on
the unfixed source: `expected { get: 'fs', list: [ 'fs' ], … } to deeply
equal { get: null, list: [ null ], … }`.
- **`MetadataPlugin` never passes `fsActor`.**
`packages/metadata/src/plugin.ts` constructs the repository with `root`,
`org` and `disableWatch` only, so every host that boots the plugin
inherited the label.
- **Readers of `authoredBy` and the event actor from this repository:
none key on `'fs'`.**
- `MetadataManager.applyRepoEvent` reads `op`, `ref`, `ts` and `seq`,
and never `actor`.
- `MetadataManager.getRepository()` has no non-test caller.
- No HTTP route exposes this repository's history or items, and the HMR
legacy events carry no actor, so no Studio provenance display can read
it.
- `LayeredRepository` is never constructed over it outside tests.
- In non-test `packages/**/*.ts`, a `git grep` finds no comparison of an
actor or `authoredBy` with `'fs'`.
- The only readers keyed on the label were nine assertions in
`metadata-fs`'s own watcher suites (`expect(… .actor).toBe('fs')`). They
are updated below.
- `fsActor` itself has no producer anywhere in this tree.
- **Position 3.** `HttpDispatcher`'s `PUT /metadata/:type/:name` branch
called `saveMetaItem` with no `actor`, although the execution context it
had just admitted carries `userId`. A signed-in author's save through
the `@objectstack/hono` catch-all therefore recorded `null` in
`sys_metadata.updated_by` / `created_by` and
`sys_metadata_history.recorded_by`. The new pin read exactly that on the
unfixed branch: `expected { created_by: null, … } to deeply equal {
created_by: 'u_author', … }`. `RestServer`'s twin records the user
through `resolveMetaWriteActor`.
## Change
- `packages/metadata-fs/src/repository.ts`:
- `fsActor` defaults to `null`, and its exported type (`fsActor?:
string`) is unchanged. Its doc now states the rule: a user id for a
change the repository did not write itself, `null` when omitted, never a
label. `source: 'fs'` already names the origin on every event.
- The three `authoredBy` reads go through one private
`authoredByOf(meta)`. When a log record exists, it answers with that
record's actor, and a recorded `null` stays `null`. Only when no record
exists (a file placed on disk outside the repository) does it answer
with `fsActor`.
- `packages/metadata/src/plugin.ts`: a comment at the attach states the
actor rule, as the explicit-composition rule asks. The code is
unchanged. With the default gone, the attach gives `null`.
- `packages/runtime/src/domains/meta.ts`: the `PUT` branch passes
`executionContext.userId` when it is a non-empty string, and nothing
otherwise. That is the same inline reading the `_migrate-stored` branch
got in `8c10776152`, with no new resolver function. The actor is read
off the admitted identity only, never off `item` or a header.
- `.changeset/23086-actor-family.md`: `patch` for
`@objectstack/metadata-fs` and `@objectstack/runtime`, written for an
operator. `@objectstack/metadata` is not listed because its only change
is a `//` comment, and no comment ships. Built `packages/metadata/dist`
holds zero hits for the new comment and zero for the pre-existing
comment above it, in js, cjs and maps. The positive control, the runtime
string `FileSystemRepository attached`, hits `dist/index.js` once.
## Pins (one per position, plus the control)
- **Position 2, the repository:**
`packages/metadata-fs/test/actor-never-a-label.test.ts` (new). It checks
the `put()` receipt, `get()`, the `list()` header and the `history()`
event:
- a write with no actor records `null` on every surface, a no-op rewrite
included;
- a write by `usr_alice` records `usr_alice` on every surface;
- a file no recorded write produced reports `null`;
- a named `fsActor` answers only where no write is recorded, and a
recorded `null` stays `null`.
The nine watcher-suite assertions now read `actor` as `null` for an edit
seen on disk (`source` still `'fs'`).
- **Position 2, the `MetadataPlugin` door:**
`packages/metadata/src/plugin-fs-repository-actor.test.ts` (new). A real
`LiteKernel` boots a real `MetadataPlugin`, and the test reads the
repository handle it attached. A file placed under
`.objectstack/metadata` before boot reports `null`, and so does an
actor-less `put()`. The positive control is that the placed file was
indexed. If a future attach passes a label as `fsActor`, this test turns
red.
- **Position 3, the dispatcher door:**
`packages/runtime/src/domains/meta-save-actor.test.ts` (new). Each door
gets its own real store: better-sqlite3 `:memory:`, the real
`sys_metadata*` objects, and a real `ObjectStackProtocolImplementation`.
The test reads the rows back after every save:
- a signed-in `u_author` save records `u_author` in `created_by`,
`updated_by` and `recorded_by`;
- a later save by `u_editor` moves `updated_by` to `u_editor`, keeps
`created_by`, and appends `u_editor` to `recorded_by`;
- an `isSystem` save with no user records `null` in all three.
An anonymous caller never reaches the save, because the anonymous deny
answers first, so "nobody" is the `isSystem` caller.
- **CONTROL:** the same two cases run against `RestServer`'s `PUT` twin
(`resolveMetaWriteActor`), which is unchanged and records the same rows.
- `packages/runtime/src/http-dispatcher.test.ts`: the deliberate
exact-match pin of the dispatcher's `saveMetaItem` request now includes
`actor: 'u1'`. It was the one red in the full runtime run before that
update.
## Ablation
The fix and its pins were committed first. Each leg went through
`scripts/ablation-replace.mjs` in WRAP mode, which carries its own
restore trap. The legs ran inside one script that also carried an `EXIT
INT TERM` trap restoring both files from `HEAD` by absolute path. The
run was at `1f9c123dcf`.
- **Leg A1, the `'fs'` default restored** (`opts.fsActor ?? null` back
to `opts.fsActor ?? 'fs'`):
- The anchor fell from 1 to 0 and the blob went from `f76f6dee4f48` to
`bfde62909356`.
- `metadata-fs` was rebuilt, and `ablation-dist-preflight.mjs` found the
marker in 2 built files. The pristine witness had found the fixed
spelling in dist before the mutation.
- Result: `metadata-fs` 11 failed and 63 passed. The failures were the
new pin's file-with-no-record case and the ten watcher tests behind the
nine updated assertions (`expected 'fs' to be null`). The new pin's
other three cases stayed green.
- The plugin door pin failed 1 of 1 (`expected 'fs' to be null`). The
runtime pin passed 4 of 4.
- Restore leg: rebuilt, and the preflight found the marker absent from
all 6 built files with the tree clean.
- **Leg A2, the `meta?.actor ?? this.fsActor` fallback restored, with
the default left `null`:**
- The anchor fell from 1 to 0 and the blob went from `f76f6dee4f48` to
`c50311af3a10`.
- Result: `metadata-fs` 1 failed and 73 passed. The failure was the
named-`fsActor` case, `expected { get: 'usr_operator', … } to deeply
equal { get: null, … }`. The runtime pin passed 4 of 4.
- The plugin pin was not run in this leg: its subject resolves through
`metadata-fs`'s `dist/`, which this leg did not rebuild, so its reading
would have proved nothing.
- **Leg B, the dispatcher's `userId` dropped** (`const actor: string |
undefined = undefined;`):
- The anchor fell from 1 to 0 and the blob went from `84a24c33019f` to
`5632faa44578`.
- Result: the runtime pin had 1 failed and 3 passed. The failure was the
dispatcher's signed-in case, `expected { created_by: null, … } to deeply
equal { created_by: 'u_author', … }`. The dispatcher's `isSystem` case
and both `RestServer` control cases stayed green.
- The `metadata-fs` pin (4/4) and the plugin pin (1/1) stayed green.
- **Restore:** both blobs equal `HEAD` (`f76f6dee4f48`, `84a24c33019f`).
`git diff HEAD` is empty and `git status --porcelain` is empty.
## Verification
`origin/main` `55828c5145` was merged once, at `9956048b4c`. The runtime
and metadata-fs changes are disjoint from it. After the merge I ran
`pnpm install --frozen-lockfile` and `turbo run build
--filter=!@objectstack/docs` (73/73 tasks).
- At `9956048b4c`:
- `@objectstack/metadata-fs` `pnpm test`: 11 files, 74 tests, exit 0.
- `@objectstack/metadata` `pnpm test`: 61 files, 880 tests, exit 0.
- `@objectstack/runtime` `--project local` over
`src/http-dispatcher.test.ts` and `src/domains/`: 110 files, 2299 tests,
exit 0.
- `typecheck` for `metadata-fs`, `metadata` and `runtime`: exit 0.
Runtime's test-typecheck ledger is unchanged (27 files, 190 errors, 68
pinned signatures).
- Before the merge, at `f489836584`, `@objectstack/runtime`'s full
`--project local` suite ran 362 files: 361 passed, and 1 failed. The
failure was the exact-match pin in `http-dispatcher.test.ts`, updated in
`1f9c123dcf`. 5166 tests passed and 19 were skipped. The full runtime
suite was not re-run after the merge. The incoming commits touch
`metadata-protocol`'s envelope and install-lock paths, not the save
actor path, so only the meta domain, the dispatcher file and the pins
were re-run. The merge ref's full run is CI's.
- **Gates, at the final head `9956048b4c`:** `dispatch-gates --commands`
derived 66 commands on the real diff. That is the 55-command lead list
plus 11 changeset-family commands. Each exit code was captured before
any pipe, and all 66 exited 0. `dispatch-gates --ran` reports 66
derived, 66 run, 0 NOT-MEASURED and 0 UNRUN.
- `check:dual-build-cjs-loads` read real built output after a full build
(107 entries, 66 packages).
- `check:query-options-erasure` is back at its 236-site test ceiling.
- The derivation warns that `origin/main` has since moved by 2 commits,
which changed three engine-double roster baselines. This diff adds no
engine double.
- **Lint, a proven narrowing:** `eslint --no-inline-config --format
json` over the 14 changed `.ts` files gives 14 files, 0 errors, 0
warnings and 0 ignore notices, so all 14 were linted.
`eslint.config.mjs` never enables type-aware linting (its own comment at
line 327: no `parserOptions.project`, no typed `@typescript-eslint`
rules), so this diff cannot change the verdict on any file it does not
touch. The repository-wide `pnpm lint` is left to CI.
## Acceptance notes
- Entries written before this release keep what they recorded: `'fs'` in
an existing `.objectstack/.log/main.jsonl`, and `null` in
dispatcher-saved rows. This is per the card's "Not here".
- `fsActor` has no producer in this tree. Retiring it would narrow an
exported options type (Clause-② narrowing). It is left in place with its
type unchanged, and only its default moved. No card is filed: per the
family rule, this is an observation and not a defect.
- `RestServer.resolveMetaWriteActor`'s docblock
(`packages/rest/src/rest-server.ts`) still says "the protocol's
`'system'` / `NULL` defaults still apply". The `'system'` half has been
stale since #4556. The file is outside this claim, and nobody is
carrying it.
- The dispatcher `PUT`'s fallback, `metadata.saveItem(type, name,
item)`, carries no actor either. No metadata service in this tree
implements `saveItem` (the only hits are the client SDK's
`meta.saveItem`), so the branch is unreachable with the in-tree
`MetadataManager`. This is a dead-code observation, not filed.
- A dispatcher save by a signed-in user now also gets that user in
`sys_metadata_audit.actor` (through `recordMetadataAudit`) instead of
that column's `'system'` default, the same as on the REST door.
- Comments in `metadata-fs`'s `self-write-suppression.test.ts` that
quote the pre-fix symptom `{op:'update', actor:'fs'}` are left as the
historical record of what was measured.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 240890c commit a3d47c2
15 files changed
Lines changed: 456 additions & 15 deletions
File tree
- .changeset
- packages
- metadata-fs
- src
- test
- metadata/src
- runtime/src
- domains
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
66 | | - | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
67 | 77 | | |
68 | 78 | | |
69 | 79 | | |
| |||
112 | 122 | | |
113 | 123 | | |
114 | 124 | | |
115 | | - | |
| 125 | + | |
| 126 | + | |
116 | 127 | | |
117 | 128 | | |
118 | 129 | | |
| |||
147 | 158 | | |
148 | 159 | | |
149 | 160 | | |
150 | | - | |
| 161 | + | |
| 162 | + | |
151 | 163 | | |
152 | 164 | | |
153 | 165 | | |
| |||
269 | 281 | | |
270 | 282 | | |
271 | 283 | | |
272 | | - | |
| 284 | + | |
273 | 285 | | |
274 | 286 | | |
275 | 287 | | |
| |||
298 | 310 | | |
299 | 311 | | |
300 | 312 | | |
301 | | - | |
| 313 | + | |
302 | 314 | | |
303 | 315 | | |
304 | 316 | | |
| |||
372 | 384 | | |
373 | 385 | | |
374 | 386 | | |
375 | | - | |
| 387 | + | |
376 | 388 | | |
377 | 389 | | |
378 | 390 | | |
| |||
534 | 546 | | |
535 | 547 | | |
536 | 548 | | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
537 | 563 | | |
538 | 564 | | |
539 | 565 | | |
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
167 | 167 | | |
168 | 168 | | |
169 | 169 | | |
170 | | - | |
| 170 | + | |
171 | 171 | | |
172 | 172 | | |
173 | 173 | | |
| |||
238 | 238 | | |
239 | 239 | | |
240 | 240 | | |
241 | | - | |
| 241 | + | |
242 | 242 | | |
243 | 243 | | |
244 | 244 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
171 | | - | |
| 171 | + | |
172 | 172 | | |
173 | 173 | | |
174 | 174 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
128 | 128 | | |
129 | 129 | | |
130 | 130 | | |
131 | | - | |
| 131 | + | |
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
196 | 196 | | |
197 | 197 | | |
198 | 198 | | |
199 | | - | |
| 199 | + | |
200 | 200 | | |
201 | 201 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
170 | 170 | | |
171 | 171 | | |
172 | 172 | | |
173 | | - | |
| 173 | + | |
174 | 174 | | |
175 | 175 | | |
176 | 176 | | |
| |||
306 | 306 | | |
307 | 307 | | |
308 | 308 | | |
309 | | - | |
| 309 | + | |
310 | 310 | | |
311 | 311 | | |
312 | 312 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
268 | 268 | | |
269 | 269 | | |
270 | 270 | | |
271 | | - | |
| 271 | + | |
272 | 272 | | |
273 | 273 | | |
274 | 274 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
203 | 203 | | |
204 | 204 | | |
205 | 205 | | |
206 | | - | |
| 206 | + | |
207 | 207 | | |
208 | 208 | | |
0 commit comments