Repository navigation
fix(metadata-fs, runtime): an actor-less filesystem write records null, never the label fs; the dispatcher metadata PUT records the user - #23109
Conversation
…'; the dispatcher's metadata PUT records the user FileSystemRepository no longer defaults fsActor to the label 'fs', and an item's authoredBy reports the actor its log record holds, a recorded null included. HttpDispatcher's PUT /metadata/:type/:name passes the execution context's user id to saveMetaItem, or nothing. Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
… attach, runtime dispatcher PUT) Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
…ersion Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
… actor Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 36 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b3281ceed8175cbf0a749acdab921f6422cf93cd && git checkout b3281ceed8175cbf0a749acdab921f6422cf93cd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2808438e9dad471383140b286f8d970afa59559d 9956048b4c3fa854dd92e828a8907edebd939263 && git checkout -B drift-repro 2808438e9dad471383140b286f8d970afa59559d && git merge --no-ff 9956048b4c3fa854dd92e828a8907edebd939263
node scripts/docs-audit/affected-docs.mjs --json 2808438e9dad471383140b286f8d970afa59559d
|
Part of #23086
Clause-②: no
This PR covers positions 2 and 3 of the
PutOptions.actorproducer family. The contract (packages/metadata-core/src/types.ts, #4556) is the acting user's id, ornull, 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/main6662e3f590Position 1, confirmed and not edited. Both modules still pass the label.
permission-set-projection.ts:1658hasactor: 'system', and so doesposition-environment-backfill.ts:299. The same two hits are there at9956048b4cand atorigin/main8464380248.Position 2: where
'fs'lands.FileSystemRepository's constructor setthis.fsActor = opts.fsActor ?? 'fs', andfsActorreached four places:actorof an edit seen on disk (handleFsChange,publishExternalDelete);authoredByinget(), in thelist()header, and in a no-opput(), each spelledmeta?.actor ?? this.fsActor.??treats a recordednullas absent. So aput()withactor: nullread back fromget()/list()as authored by'fs', even though the write and its log record heldnull. The new pin read exactly that on the unfixed source:expected { get: 'fs', list: [ 'fs' ], … } to deeply equal { get: null, list: [ null ], … }.MetadataPluginnever passesfsActor.packages/metadata/src/plugin.tsconstructs the repository withroot,organddisableWatchonly, so every host that boots the plugin inherited the label.Readers of
authoredByand the event actor from this repository: none key on'fs'.MetadataManager.applyRepoEventreadsop,ref,tsandseq, and neveractor.MetadataManager.getRepository()has no non-test caller.LayeredRepositoryis never constructed over it outside tests.packages/**/*.ts, agit grepfinds no comparison of an actor orauthoredBywith'fs'.metadata-fs's own watcher suites (expect(… .actor).toBe('fs')). They are updated below.fsActoritself has no producer anywhere in this tree.Position 3.
HttpDispatcher'sPUT /metadata/:type/:namebranch calledsaveMetaItemwith noactor, although the execution context it had just admitted carriesuserId. A signed-in author's save through the@objectstack/honocatch-all therefore recordednullinsys_metadata.updated_by/created_byandsys_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 throughresolveMetaWriteActor.Change
packages/metadata-fs/src/repository.ts:fsActordefaults tonull, 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,nullwhen omitted, never a label.source: 'fs'already names the origin on every event.authoredByreads go through one privateauthoredByOf(meta). When a log record exists, it answers with that record's actor, and a recordednullstaysnull. Only when no record exists (a file placed on disk outside the repository) does it answer withfsActor.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 givesnull.packages/runtime/src/domains/meta.ts: thePUTbranch passesexecutionContext.userIdwhen it is a non-empty string, and nothing otherwise. That is the same inline reading the_migrate-storedbranch got in8c10776152, with no new resolver function. The actor is read off the admitted identity only, never offitemor a header..changeset/23086-actor-family.md:patchfor@objectstack/metadata-fsand@objectstack/runtime, written for an operator.@objectstack/metadatais not listed because its only change is a//comment, and no comment ships. Builtpackages/metadata/distholds 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 stringFileSystemRepository attached, hitsdist/index.jsonce.Pins (one per position, plus the control)
Position 2, the repository:
packages/metadata-fs/test/actor-never-a-label.test.ts(new). It checks theput()receipt,get(), thelist()header and thehistory()event:nullon every surface, a no-op rewrite included;usr_alicerecordsusr_aliceon every surface;null;fsActoranswers only where no write is recorded, and a recordednullstaysnull.The nine watcher-suite assertions now read
actorasnullfor an edit seen on disk (sourcestill'fs').Position 2, the
MetadataPlugindoor:packages/metadata/src/plugin-fs-repository-actor.test.ts(new). A realLiteKernelboots a realMetadataPlugin, and the test reads the repository handle it attached. A file placed under.objectstack/metadatabefore boot reportsnull, and so does an actor-lessput(). The positive control is that the placed file was indexed. If a future attach passes a label asfsActor, 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 realsys_metadata*objects, and a realObjectStackProtocolImplementation. The test reads the rows back after every save:u_authorsave recordsu_authorincreated_by,updated_byandrecorded_by;u_editormovesupdated_bytou_editor, keepscreated_by, and appendsu_editortorecorded_by;isSystemsave with no user recordsnullin all three.An anonymous caller never reaches the save, because the anonymous deny answers first, so "nobody" is the
isSystemcaller.CONTROL: the same two cases run against
RestServer'sPUTtwin (resolveMetaWriteActor), which is unchanged and records the same rows.packages/runtime/src/http-dispatcher.test.ts: the deliberate exact-match pin of the dispatcher'ssaveMetaItemrequest now includesactor: '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.mjsin WRAP mode, which carries its own restore trap. The legs ran inside one script that also carried anEXIT INT TERMtrap restoring both files fromHEADby absolute path. The run was at1f9c123dcf.'fs'default restored (opts.fsActor ?? nullback toopts.fsActor ?? 'fs'):f76f6dee4f48tobfde62909356.metadata-fswas rebuilt, andablation-dist-preflight.mjsfound the marker in 2 built files. The pristine witness had found the fixed spelling in dist before the mutation.metadata-fs11 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.expected 'fs' to be null). The runtime pin passed 4 of 4.meta?.actor ?? this.fsActorfallback restored, with the default leftnull:f76f6dee4f48toc50311af3a10.metadata-fs1 failed and 73 passed. The failure was the named-fsActorcase,expected { get: 'usr_operator', … } to deeply equal { get: null, … }. The runtime pin passed 4 of 4.metadata-fs'sdist/, which this leg did not rebuild, so its reading would have proved nothing.userIddropped (const actor: string | undefined = undefined;):84a24c33019fto5632faa44578.expected { created_by: null, … } to deeply equal { created_by: 'u_author', … }. The dispatcher'sisSystemcase and bothRestServercontrol cases stayed green.metadata-fspin (4/4) and the plugin pin (1/1) stayed green.HEAD(f76f6dee4f48,84a24c33019f).git diff HEADis empty andgit status --porcelainis empty.Verification
origin/main55828c5145was merged once, at9956048b4c. The runtime and metadata-fs changes are disjoint from it. After the merge I ranpnpm install --frozen-lockfileandturbo run build --filter=!@objectstack/docs(73/73 tasks).9956048b4c:@objectstack/metadata-fspnpm test: 11 files, 74 tests, exit 0.@objectstack/metadatapnpm test: 61 files, 880 tests, exit 0.@objectstack/runtime--project localoversrc/http-dispatcher.test.tsandsrc/domains/: 110 files, 2299 tests, exit 0.typecheckformetadata-fs,metadataandruntime: exit 0. Runtime's test-typecheck ledger is unchanged (27 files, 190 errors, 68 pinned signatures).f489836584,@objectstack/runtime's full--project localsuite ran 362 files: 361 passed, and 1 failed. The failure was the exact-match pin inhttp-dispatcher.test.ts, updated in1f9c123dcf. 5166 tests passed and 19 were skipped. The full runtime suite was not re-run after the merge. The incoming commits touchmetadata-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.9956048b4c:dispatch-gates --commandsderived 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 --ranreports 66 derived, 66 run, 0 NOT-MEASURED and 0 UNRUN.check:dual-build-cjs-loadsread real built output after a full build (107 entries, 66 packages).check:query-options-erasureis back at its 236-site test ceiling.origin/mainhas since moved by 2 commits, which changed three engine-double roster baselines. This diff adds no engine double.eslint --no-inline-config --format jsonover the 14 changed.tsfiles gives 14 files, 0 errors, 0 warnings and 0 ignore notices, so all 14 were linted.eslint.config.mjsnever enables type-aware linting (its own comment at line 327: noparserOptions.project, no typed@typescript-eslintrules), so this diff cannot change the verdict on any file it does not touch. The repository-widepnpm lintis left to CI.Acceptance notes
'fs'in an existing.objectstack/.log/main.jsonl, andnullin dispatcher-saved rows. This is per the card's "Not here".fsActorhas 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'/NULLdefaults still apply". The'system'half has been stale sincesys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556. The file is outside this claim, and nobody is carrying it.PUT's fallback,metadata.saveItem(type, name, item), carries no actor either. No metadata service in this tree implementssaveItem(the only hits are the client SDK'smeta.saveItem), so the branch is unreachable with the in-treeMetadataManager. This is a dead-code observation, not filed.sys_metadata_audit.actor(throughrecordMetadataAudit) instead of that column's'system'default, the same as on the REST door.metadata-fs'sself-write-suppression.test.tsthat quote the pre-fix symptom{op:'update', actor:'fs'}are left as the historical record of what was measured.Generated by Claude Code