Repository navigation
feat(plugin-audit): sys_activity gains writer-stamped read-rule columns with an idempotent backfill, and the withheld-row rule is a predicate with no SYSTEM pre-scan (ADR-0139 E6) - #23814
Conversation
Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy Co-authored-by: Claude <noreply@anthropic.com>
…atabase families Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy Co-authored-by: Claude <noreply@anthropic.com>
… no leaf Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy Co-authored-by: Claude <noreply@anthropic.com>
… door Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy Co-authored-by: Claude <noreply@anthropic.com>
… backfill's write site; system-context page Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 39 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 33 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 1a058c00c3aa2ff0b3007b8940a254a4b38a6ea8 && git checkout 1a058c00c3aa2ff0b3007b8940a254a4b38a6ea8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d795719926a2dacf7a394e23b717f36961343a26 d75ae9784332b0ba3d0eeb48efb8d1ee978eed5c && git checkout -B drift-repro d795719926a2dacf7a394e23b717f36961343a26 && git merge --no-ff d75ae9784332b0ba3d0eeb48efb8d1ee978eed5c
node scripts/docs-audit/affected-docs.mjs --json d795719926a2dacf7a394e23b717f36961343a26
|
Contract reviewServed-tier: Rendered at ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL FAIL reasons:
Generated by Claude Code |
席位核对 · REWORK (patch round 1) ·
|
…at carry those roles; pin membership over a prefix computeWithheldUpdateFilter keeps its name as the withheld-row predicate builder, and isWithheldOnlyUpdate is the update half over changed_fields, called by keptRowsOfParent. A served field whose name is a prefix of a withheld one keeps no withheld-only update row. Claude-Session: https://claude.ai/code/session_01TyhsxCqpTkk1U5GLD1JETy Co-authored-by: Claude <noreply@anthropic.com>
Fixes #23731
Clause-②: yes
ADR-0139 E6, ruled letter B (ruling record 6205262047, maintainer 「B (推荐)」; Q2 = b confirmed). The activity stream's withheld-row rule becomes a predicate over two writer-stamped
sys_activitycolumns. The SYSTEM pre-scan and its 2,000-row window are gone, for both halves.Rollout (the changeset's first paragraph)
Run
os migrate activity-changed-fields --applyat once after upgrading. Until it runs, stored activity rows carry no stamp and are withheld from every non-system reader. The door's name and flags areos migrate audit-metadata-bodies's shape, one for one (--database-url,--apply,--yes,--force,--json); nothing is renamed, so objectstack-ai/cloud#2900 reads it as declared. It is a dry run by default, idempotent, and covers both columns in one run. One measured difference, stated in the changeset: the columns arrive with the upgraded deployment's schema sync, which--applyperforms itself, so a dry run against a database no upgraded process has opened cannot read them and exits 1.What changed
sys_activitygainschanged_fields(tags) andmilestone_key(text), bothreadonly. They are stamped by ONE planner,planActivityStamp(activity-changed-fields.ts):changed_fieldsis the key union of the recordedold/newfor an update. It is[]for a create, a delete, a note, or an update that touched onlyinternalfields.milestone_keyis the sorted, comma-joined watched-field list (text_sources.kind) of a fired milestone the mirror declared. It isnullotherwise.Writers. The CRUD mirror stamps its own rows. A
sys_activitybefore-insert hook (Q2 = b) stamps every inserted row, over whatever value a writer sent: the engine's create-sidereadonlystrip skipssys_objects, so the hook is the guard, ascomment-access-hooks.tsis foris_edited. Both stamp only when the registeredsys_activitydeclares the columns, theorganization_idprecedent.The rule (
activity-field-redaction.ts). It is nowcomputeWithheldRowFilter→keptRowsOfParent: one branch per parent object the read gate enumerates, or the one parent a read pins onobject_name. A row is kept when all of these hold:changed_fieldsis empty, or contains a served field (one$containsper served field);milestone_keyis null, or equals the key of P's CURRENTactivityMilestonesdeclaration while the reader is served every field that declaration watches.It reads no row and composes no leaf. A parent the service gives no answer for is kept whole, as before. An engine with no registry view is denied, and that is said once.
The redaction narrows
changed_fieldsto the served members and never servesmilestone_key.The query guard (
parent-field-query-guard.ts) covers both columns, and the pin against the redaction's fail-closed strip covers them too.The backfill,
backfillActivityChangedFields, sits besidestored-metadata-body-migration.tsand is exported fromsrc/index.ts. It reads unstamped rows a page at a time (keyset onid) and updates both columns in one write, as the platform.The door,
os migrate activity-changed-fields, is registered in the one-shot family table, the JSON-purity family and the absent-database roster.Docs and generated files.
deployment/cli.mdx(a section, and the data-migrations table) and the platform checklist variants (16 → 17 subcommands). The plugin-audit i18n bundles, regenerated and translated for zh-CN / ja-JP / es-ES (the provenance hashes dropped by themselves). The tenant-audit census, regenerated: the backfill's update is one more write call site (225 → 226), and the hand-written prose figures follow.permissions/system-context.mdxrow 45 no longer names a pre-scan of the activity redaction.Landing places: everything is in
plugin-audit, the CLI door, dogfood, docs and the generated files the gates regenerate. None ofpackages/spec,objectql,core, any driver,parent-record-read-gate.ts,comment-access-hooks.ts, ormigrate/apply.tsis touched.Evidence
All readings are at head 70ff93d (merge base 609d07a), on the shared box, under
scripts/pm/os-verify-lock.sh.pnpm --filter @objectstack/plugin-audit test: 51 files / 832 tests passed.typecheckexit 0, test-typecheck ledger included.@objectstack/clitypecheck exit 0. Unit project: 297 files / 4,379 tests passed.CLI integration, run locally because the diff touches it:
data-commands.absent-databaseandschema-migrate.one-shot-family: 135 / 135;json-stdout-purity.e2e: 59 / 59 (nightly tier).@objectstack/dogfoodtypecheck exit 0.Real stack (dogfood), with the real
SecurityPlugin+AuditPluginand a store of more than 2 × 2,000 rows:ReadableIdsleaf. The only SYSTEM read left is the read gate's D5 bounded probe of the parent pair.The seven activity dogfood suites, the org-peer sign-in case (
activity-withheld-update.dogfood) among them: 7 files / 65 tests passed.The upgrade path, measured. On a database turned back into the previous version's shape (both columns dropped from the rotation shard, the view re-created without them, rows planted unstamped):
--apply's plain boot adds both columns to the shard and re-creates the view;Ablations. Each negative pin was run red, then restored. Every mutation went through
scripts/ablation-replace.mjs; restore was proven blob == HEAD withgit diff HEADempty.changed_fieldsserved unnarrowedmilestone_keyservedPERMISSION_DENIED/ 403 probe)id, object_name, metadatacarrying aReadableIdsleafA8 resolves through
dist/. Its marker was present indist/(ablation-dist-preflight.mjs), and after the restore and rebuild it was absent. Two attempts were no-ops and are void:dist/unchanged;Acceptance notes
object_namepin builds one branch per enumerated parent, and$containsleaves per served field. The security service is asked about each enumerated parent, where it used to be asked about each parent present in the 2,000-row window. That is the query guard's established cost on an unpinned query.parent-record-read-gate.ts'sno-object-admissiontext, and ADR-0139's consumer table (row 3,#computeWithheldUpdateFilter), still describe the withheld-update rule's SYSTEM re-read. The file is ADR-0139 E7: the comment-thread, comment-reaction and attachment read gates emit one ReadableIds leaf per parent object and do no pre-scan; D5's three classes keep the probe (D4, D5) #23732's and the ADR is Tier H, so neither is touched here. Carrier: ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788.Generated by Claude Code