| title | System context (isSystem) — bypass rules |
|---|---|
| navTitle | System Context (isSystem) |
| description | Every platform behaviour that ExecutionContext.isSystem changes — what an elevated write gets, what it loses, and what the flag deliberately does not do. |
ExecutionContext.isSystem is the platform's one elevation flag. Setting it on a
write or read means "this operation is the engine acting on its own behalf" —
the seed loader replaying package fixtures, a plugin's boot reconciler, a
service self-write, a migration.
This page is the authority for what that flag actually does. It exists
because the flag is not one concept: it is a single boolean read at over a
hundred distinct sites across some twenty packages, and knowing three of those
behaviours gives no hint that the rest exist. Every documented app-side bug
traced to isSystem had the same shape — the metadata was complete and correct,
and the gap was observable only by querying the resulting rows.
path#symbol citation. Nothing encodes a
position, so an unrelated edit above a site cannot rot one, and a repair is
never mechanical: there are no numbers to renumber. The price is granularity.
Several reads inside one function collapse onto one anchor, so deleting a
whole symbol reds this page, and adding or deleting one of several reads inside
a symbol that keeps at least one does not. The gap is real, it is priced
below, and it is left open deliberately rather than hidden — see
Maintaining this table.
Three unrelated declarations share the identifier. This page documents only the
first. The others are metadata keys on a stored document (one of them now
retired) and have nothing to do with elevation. (A fourth, Environment.isSystem — a
platform-infrastructure environment, not user data — left this repo with the
@objectstack/spec/cloud subpath in #16325 and is declared in the cloud repo.)
| Declaration | What it is | This page? |
|---|---|---|
ExecutionContext.isSystem — packages/spec/src/kernel/execution-context.zod.ts#isSystem |
The elevation flag on an operation's context | ✅ |
Object.isSystem — packages/spec/src/data/object.zod.ts#isSystem |
Marks a system object (protected from deletion; defaults its org-wide sharing to public when no sharingModel is set) |
❌ |
EmailTemplate.isSystem — packages/spec/src/system/email-template.zod.ts#isSystem |
A retired key: a retiredKey() tombstone, which the parse refuses with a migration note |
❌ |
The collision is a genuine hazard rather than a naming nit: Object.isSystem
changes an object's default sharing, and ExecutionContext.isSystem changes
whether sharing grants are materialised — so a search for "isSystem sharing"
returns both, and they are unrelated decisions.
A fifth, closely-spelled family — isSystemObjectName() /
isSystemObject() in packages/runtime/src/action-execution.ts#isSystemObjectName,
packages/mcp/src/mcp-http-tools.ts#isSystemObject — keys on the sys_ name prefix,
not on any flag.
isSystem is server-constructed and never client-supplied. Inbound HTTP
cannot set it (packages/rest/src/rest-server.ts#enforceAuth), and neither
can an action body (packages/runtime/src/domains/actions.ts#handleActionsRequest). It is
written by internal callers only, as an option on the engine call:
await engine.insert('crm_account', row, { context: { isSystem: true } });Its parse-time default is false (packages/spec/src/kernel/execution-context.zod.ts#isSystem), so an absent
context is never elevated.
Grouped by lane. Every row cites the site that reads the flag. "What you lose" is the part that costs app-side bugs — it is the protection or the side effect that silently does not happen.
| # | Behaviour when isSystem |
Package | What you get / what you lose | Anchor |
|---|---|---|---|---|
| 1 | The whole security middleware short-circuits before any gate runs | plugin-security | Get: every CRUD/FLS/tenant/owner gate below skipped in one branch. Lose: every gate under it in this lane at once, down to and including the write-bypass row that widens the effective write scope to org (row 7) — this is the single largest behaviour on the page |
packages/plugins/plugin-security/src/security-plugin.ts#start |
| 2 | owner_id is not auto-stamped on INSERT (the step 3.5 anchor guard is inside the block row 1 skips) |
plugin-security | Lose: the row lands owner_id = NULL, so the default owner_only_writes policy hides it from its own creator. Get: nothing — this is a gap, not a capability |
the step 3.5 guard block and the short-circuit that skips it are both inside packages/plugins/plugin-security/src/security-plugin.ts#start |
| 3 | Row-level read filter resolves to "no filter" | plugin-security | Get: unscoped reads. Lose: row-level scoping entirely | packages/plugins/plugin-security/src/security-plugin.ts#getReadFilter |
| 4 | Field-level security returns all fields | plugin-security | Get: every column readable (getReadableFields) and writable (getWritableFields). Lose: field masking |
packages/plugins/plugin-security/src/security-plugin.ts#resolveProjectionFieldMask |
| 5 | Export permission granted unconditionally | plugin-security | Get: canExport is true |
packages/plugins/plugin-security/src/security-plugin.ts#canExport |
| 6 | Object-level read admission granted unconditionally | plugin-security | Get: canReadObject is true. This is the OBJECT-level half of a read — "may this caller read this object at all" — which the doors that bypass this middleware ask before they compile a statement of their own; getReadFilter is its row-level half, and the two are not interchangeable |
packages/plugins/plugin-security/src/security-plugin.ts#canReadObject |
| 6b | WRITE admission granted unconditionally | plugin-security | Get: canWriteObject is true. The WRITE twin of the object-level read admission above, asked by the write PREVIEW (ObjectQL.validate()), which runs no middleware for its target object and so has no gate of its own. It exists because a validation rule that reads one hop through a reference field is evaluated against a related row fetched under system authority, and that elevation is bounded to the arms the question RUNS — named, ⛔ never a category of the write decision, and ⛔ not a promise the write would succeed. The arms: the ADR-0103 engine-owned affordance gate and the ADR-0090 D12 delegated-admin gate (both ahead of every resolution), the fail-closed postures (#3545's unresolvable posture and the D10 dangling delegator), the ADR-0066 D3 capability arm, the CRUD grant, the ADR-0090 D10 delegator arm that a bare CRUD-grant check misses, the field-level-security write gate over the caller's payload when one is supplied, and the ADR-0123 D2 no-active-organization wall |
packages/plugins/plugin-security/src/security-plugin.ts#canWriteObject |
| 7 | Write bypass = true, effective write scope = org |
plugin-security | Get: widest write scope without holding any capability | packages/plugins/plugin-security/src/security-plugin.ts#start |
| 8 | Metadata-plane schema masking exempt (ADR-0106 D4) | metadata-core | Get: unmasked object schema. Note: the exemption is a caller property — it short-circuits before the security service is consulted | packages/metadata-core/src/object-schema-fls.ts#isObjectSchemaMaskExempt |
| 8b | Authoring-trail reads admitted | rest, runtime | Get: a metadata item's version history, audit rows and stored-version diff (GET /meta/:type/:name/history, /audit, /diff) without studio.access or manage_metadata, and a package's commit timeline (GET /packages/:id/commits) without studio.access. Note: narrower than the schema-mask exemption the draft switches ask (isObjectSchemaMaskExempt): neither door admits setup.access, which organization admins hold |
packages/rest/src/rest-server.ts#mayReadAuthoringTrail, packages/runtime/src/domains/packages.ts#requireCommitTrailReadCapability |
| 9 | explain() may target a principal other than the caller |
plugin-security | Get: no manage_users / delegated-admin check |
packages/plugins/plugin-security/src/security-plugin.ts#explainAccessForCaller |
| 9b | The master-detail write check the security service serves (checkControlledByParentWrite, ADR-0055) answers allow |
plugin-security | Get: allow for any record, with no master judged — the write path's own first exit, so a gate composing it admits exactly what the record's own system-context update would. Lose: nothing a caller reaches today: its two consumers, the sys_attachment and sys_comment parent gates, return before they ask for a system session |
packages/plugins/plugin-security/src/security-plugin.ts#checkControlledByParentWrite |
| 10 | Anonymous-deny treats the caller as authenticated | core | Get: passes the 401 seam with no userId |
packages/core/src/security/anonymous-deny.ts#shouldDenyAnonymous |
| 12 | Session-resolution middleware skipped | plugin-auth | Get: no session lookup attempted | packages/plugins/plugin-auth/src/auth-plugin.ts#start |
| 13 | Per-request performance timings disclosed | observability | Get: timing headers a normal caller cannot pull | packages/observability/src/perf-timing.ts#isPerfDisclosurePrincipal |
| 15 | MCP stdio bridge skips the object API-exposure gate | mcp | Get: the bridge reaches objects whose apiEnabled / apiMethods would refuse an external caller |
packages/mcp/src/stdio-data-bridge.ts#enforceApiExposure |
| 16 | Read-audit rows are not written | plugin-audit | Lose: the "a person opened this record" trail. sudo() keeps the caller's userId, so this flag is the only thing separating a human read from a platform one |
packages/plugins/plugin-audit/src/read-audit.ts#installReadAuditWriter |
| 17 | Approval snapshot payload redaction skipped, and so is the snapshot query guard | plugin-approvals | Get: the whole snapshot on find / findOne — the audit/replay channel — and a filter, sort or grouping by it on any read. Lose: field-visibility redaction over approval payloads, and the refusal of a query over the snapshot for a reader withheld a field of the objects it can reach |
packages/plugins/plugin-approvals/src/payload-redaction-middleware.ts#bindSnapshotRedactionMiddleware, packages/plugins/plugin-approvals/src/payload-predicate-guard.ts#bindSnapshotPredicateGuard |
| 18 | REST anonymous-deny seam satisfied | rest | Get: enforceAuth passes with no userId. Not reachable from the wire — isSystem is never set on an inbound request |
packages/rest/src/rest-server.ts#enforceAuth |
| 18b | Settings namespace read scope not applied | service-settings | Get: a read of sys_setting, sys_setting_audit or sys_platform_setting across every namespace, whatever each namespace's readPermission names — the settings service's own reads of its stores take this path. Lose: the per-namespace read gate the generic data API applies to every other caller, the same rule the settings routes enforce |
packages/services/service-settings/src/settings-read-door.ts#settingsReadDoorMiddleware |
| # | Behaviour when isSystem |
Package | What you get / what you lose | Anchor |
|---|---|---|---|---|
| 19 | readonly strip bypassed — UPDATE, single row |
objectql | Get: a readonly field CAN be written. Lose: the protection that stops a caller seeding e.g. approval_status |
packages/objectql/src/engine.ts#update |
| 20 | readonly strip bypassed — UPDATE, bulk/predicate |
objectql | Same, on the multi-row path | packages/objectql/src/engine.ts#update |
| 21 | readonly strip bypassed — INSERT |
objectql | Same, on create — one gate over BOTH create-side passes since the 2026-09-03 ruling moved the static-readonly strip in beside the runtime-owned one and deleted the DataProtocol ingress copy. isSystem is the only exemption on this path: preserveAudit is deliberately not read on create, so a non-system historical import is still stripped. A second read in the same symbol gates the pass that withholds those caller values from beforeInsert before the strip, the same exemption, so a system caller's hook still sees what it sent |
packages/objectql/src/engine.ts#insert |
| 21b | readonly strip bypassed — the write PREVIEW (ObjectQL.validate) |
objectql | Same as 19–21, in the preview: it runs the write's caller-write strips under the write's own gate, so a system preview reports no readonly drop and judges the value a system write would store. The computed strip of a formula value is NOT gated here or on either write verb — no driver has a column for it, so it runs for every caller |
packages/objectql/src/engine.ts#validate |
| 22 | Strict-drop refusal never fires | objectql | Lose: a caller that opted into loud refusal gets silence — strict refuses exactly what the strip would have taken, and the strip took nothing. One exception: a formula value, which the computed strip takes from every caller, so strict still refuses it |
packages/objectql/src/engine.ts#insert, packages/objectql/src/readonly-strict-errors.ts#READONLY_CLASS_REASONS |
| 23 | Referential-integrity check skipped | objectql | Get: writes proceed against unreachable/unresolvable targets. Lose: an isSystem caller can write a dangling reference |
packages/objectql/src/engine.ts#assertReferencesResolve |
| 23b | Position-catalog check skipped — a sys_user_position write whose position names no position in the security catalog is not refused |
plugin-security | Get: a system writer can store an assignment naming no position — the seed loader writes stack.data from AppPlugin.start() under a system context, as invitation acceptance and the platform's own bootstraps do. Lose: such a row grants nothing and nothing says so. A non-system insert, or a non-system update that changes position, is refused 400 VALIDATION_FAILED / reference_not_found instead when the security catalog holds no position of that name: the environment registry (ADR-0131 D3), one per deployment, which takes no organization, so a name only a sys_position row carries — any organization's — is refused like one nobody carries. It is the same stand-down as the engine's referential-integrity check for a lookup column |
packages/plugins/plugin-security/src/position-catalog-refusal.ts#assertPositionNamesCatalogRow |
| 23c | Grant-name catalog check skipped — a sys_user_permission_set write whose permission_set names no permission set in the security catalog is not refused |
plugin-security | Get: a system writer can store a grant naming a set not yet registered — seed replay and boot provisioning may write the grant before the definition it names is registered. Lose: such a row grants nothing and nothing says so. A non-system insert, or a non-system update that changes permission_set, is refused 400 VALIDATION_FAILED / reference_not_found instead when the security catalog holds no set by that name; a grant that names no set at all is refused required for every caller. The grant names its set by name alone, and nothing derives the name from permission_set_id (ADR-0131 D4, D10) |
packages/plugins/plugin-security/src/grant-permission-set-name.ts#onInsert, #onUpdate |
| 23d | Grant-holder membership check skipped — a sys_user_position or sys_user_permission_set write naming a user with no sys_member row in the row's organization is not refused |
plugin-security | Get: a system writer can store such a grant — invitation acceptance writes the membership and its placement together, and seed replay, the organization-admin reconcile and the platform bootstraps write grants as the system. Lose: such a row takes effect once its user is admitted to the organization. A non-system insert, or a non-system update that moves user_id or organization_id, is refused 400 VALIDATION_FAILED / reference_not_found at user_id instead, for every caller, a platform administrator included; the membership is read as { isSystem: true, tenantId } with tenantId the row's own organization, never another one. A row stored with no organization is not judged. Under single, 18.0 stamps a new grant with the Default Organization, so such a row is one an earlier release wrote |
packages/plugins/plugin-security/src/grant-holder-membership-refusal.ts#onInsert, #onUpdate |
| 24 | Tenant-audit warning silenced; bypassTenantAudit threaded to the driver |
objectql | Get: unscoped system writes stop warning. Lose: the signal that would flag a genuine user-path scoping bug | packages/objectql/src/engine.ts#buildDriverOptions |
| 25 | Engine-owned / append-only write guard bypassed | plugin-security | Get: generic writes to managedBy engine-owned objects |
packages/plugins/plugin-security/src/system-write-guard.ts#isUserContextWrite, #assertEngineOwnedWriteAllowed |
| 26 | Identity write guard bypassed (ADR-0092) | plugin-auth | Get: direct writes to identity tables through the generic data path | packages/plugins/plugin-auth/src/identity-write-guard.ts#isUserContextWrite |
| 26b | API key restore guard does not refuse | plugin-auth | Get: a write setting revoked: false on a sys_api_key row an administrator's forced password reset revoked is admitted without the platform-administrator check a user-context restore must pass; the reason is still cleared. Lose: nothing a caller reaches: no system writer restores a key, and the mint, the last-used stamp and the forced reset itself never set revoked: false |
packages/plugins/plugin-auth/src/api-key-restore-guard.ts#isUserContextWrite |
| 27 | Search-companion column kept in a read's rows when it was explicitly requested | objectql | Get: the internal companion column is readable. Lose: nothing for app code — this is the engine reading its own index | packages/objectql/src/engine.ts#stripSearchCompanionFromRead |
| 28 | Dependent-count disclosure on a blocked delete | objectql | Get: the count of blocking children. Nothing was elevated past the caller, so nothing is withheld | packages/objectql/src/engine.ts#dependentCountIsDisclosable |
| 29 | Reference-cleanup log attributes the write to 'system' |
objectql | Get: an honest actor label instead of anonymous when the context carries neither userId nor actor |
packages/objectql/src/engine.ts#recordReferenceCheckElevation |
| 29b | A traversing validation rule's related read skips the caller's bounds | objectql | Get: no row bound from the caller's own read on a related object no organization wall scopes, no read door on a header the rule reads through a master_detail field, and a related read even with no organization under a walled posture (a tenantId on the context still scopes it). Every caller that is not system is held to all three: a header its read door does not return is unavailable to the rule exactly as a header that exists nowhere is, whatever it holds, and a header it can read is judged on its stored values. A plain lookup to an object an organization wall scopes asks no read door, system caller or not |
packages/objectql/src/engine.ts#resolveOneHopRelated |
| 29c | A detail write's master-detail header read asks no read door | objectql | Get: the write binds the header a parent-scoped requiredWhen / readonlyWhen reads as parent from the header's stored row whoever can read it, as before. Every caller that is not system meets its own read door first: a header that read does not return answers as a header that exists nowhere — an unbound parent, and reference_not_found on the reference check (which a system caller's write skips altogether) |
packages/objectql/src/engine.ts#masterDetailHeadersCallerReads |
| 30 | Bulk data event organizationId OMITTED — the batch is published "not asserted" |
plugin-security | Get: nothing — the data.records.* event still publishes. Lose: the per-organization attribution: this exit is taken before the security middleware composes any tenant wall, so it records no Layer 0 verdict on the operation (OperationContext.tenantLayer0Verdict, #15813), and the engine's bulk producer — which reads that recorded verdict and nothing else — omits the key rather than filling it from the caller's tenantId; a tenant-scoped consumer then does not deliver the event inside an organization wall (#15225) |
packages/plugins/plugin-security/src/security-plugin.ts#start |
| 30b | The import doors admit a control opt-out — runAutomations: false and treatAsHistorical: true on POST /data/:object/import and /import/jobs |
rest | Get: an import whose rows are written with skipAutomations (no metadata-bound hook, no record-change trigger: an app's approvals and record freezes among them) or with skipStateMachine and preserveAudit. Every other caller, every signed-in user and administrator included, is refused 403 PERMISSION_DENIED before the body is parsed, so nothing is written and no job is created. Inbound HTTP never carries the flag, so this is an in-process caller's door. Note: the flag still suppresses none of those controls by itself (see below); it only admits the opt-outs that do |
packages/rest/src/import-prepare.ts#importControlOptOutRefusal |
| 30c | The import undo keeps the control skips — POST /data/import/jobs/:jobId/undo |
rest | Get: an undo whose deletes and restores are written with skipAutomations and skipStateMachine, so no metadata-bound hook, record-change trigger or state_machine rule refuses a reversal. Every other caller, every signed-in user and administrator included, undoes under those controls: a reversal one of them refuses is reported per row in the undo answer's failures and kept in the job's undo log, and the job is stamped reverted only once every row is. A historical job's preserveAudit belongs to the job, not the caller, and rides every undo. Inbound HTTP never carries the flag, so this is an in-process caller's door |
packages/rest/src/data-action-routes.ts#importUndoWriteContext |
One of the largest consumers of the flag;
node scripts/isystem-census.mjs --json lists every site with its package.
| # | Behaviour when isSystem |
What you get / what you lose | Anchor |
|---|---|---|---|
| 31 | Sharing-rule REVOCATION is skipped on the record-afterDelete hook — and on that hook only |
Lose: nothing permanently — the revoke is delivered, but deferred on the unbounded shape. The payload belongs to another subscriber: packages/plugins/plugin-sharing/src/record-share-cascade.ts binds on every sharing-capable object and stashes for system writes on its own account (#5103). When the deleted ids are enumerable it revokes inline; when they are not — a predicate delete whose row set the stash could not resolve — it hands the reclaim to a queued background orphan sweep instead, so the share rows outlive the deleted records until that sweep runs, with the boot orphan sweep behind it. No surviving record loses access either way, and a restart re-runs the same sweep. This is one subscriber declining work another owns, not elevation silencing a consequence. afterInsert / afterUpdate skips, and the before* stash skip that fed them, were removed by the 2026-08-31 ruling on #13533; a system write materialises exactly as a user write does |
packages/plugins/plugin-sharing/src/rule-hooks.ts#bindRuleHooks |
| 32 | Sharing write verdict short-circuits to allow |
Get: writes pass the sharing gate unconditionally | packages/plugins/plugin-sharing/src/sharing-service.ts#bypassVerdict |
| 33 | Record visibility / manage-shares checks return true | Get: no ownership or Modify-All requirement | packages/plugins/plugin-sharing/src/sharing-service.ts#canManageShares, #assertCanManageShares, #shouldBypass |
| 34 | grant() skips the enforcement + manage-shares assertions |
Get: the rule evaluator can materialise through the public API. Note it is not a bare skip: the system branch asserts the grant is not inert instead (a grant on an object no verdict can consult is refused) | packages/plugins/plugin-sharing/src/sharing-service.ts#grant |
| 35 | revoke() deletes directly, before the non-manual-source guard |
Get: the evaluator can revoke its own grants. Lose: the CONFLICT guard that warns a rule-materialised grant will be silently re-granted on the next reconcile |
packages/plugins/plugin-sharing/src/sharing-service.ts#revoke (the guard it deletes in front of is in the same function) |
| 36 | listShares() skips the management gate |
Get: full enumeration of who can see a record | packages/plugins/plugin-sharing/src/sharing-service.ts#listShares |
| 37 | sys_record_share reads are not self-scoped |
Get: tenant-wide share listing without manage_sharing |
packages/plugins/plugin-sharing/src/sharing-plugin.ts#buildSharingMiddleware |
| 38 | Share-link policy enabled check bypassed; system callers re-enter under a system context |
Get: link creation while the policy is off — resolution is not bypassed since #14033 (publicSharing.enabled is a standing policy held at every redemption): a link minted this way does not resolve until the block is enabled |
packages/plugins/plugin-sharing/src/share-link-service.ts#createLink, #revokeLink, #listLinks |
| 39 | Sharing-rule provenance stamp skipped | Lose: the row is not marked as an admin customization — seeder / defineRule / boot reconcilers are "the package door" |
packages/plugins/plugin-sharing/src/sharing-rule-provenance.ts#bindRuleProvenanceStamp |
| 40 | Sharing-rule service write + delete paths return early | Lose: the manage-rules gate on the service surface and the declared-name guard on sys_sharing_rule writes (a new row or a rename taking a declared rule's name, 409) — defineRule makes that check itself before its own system write, and the other system writers are the platform's machinery (the ownership ceremony, migrations) |
packages/plugins/plugin-sharing/src/sharing-rule-service.ts#assertCanManageRules, #bindDeclaredRuleNameGuard |
| # | Behaviour when isSystem |
Package | What you get / what you lose | Anchor |
|---|---|---|---|---|
| 41 | Approval record lock released — a locked record is writable | plugin-approvals | Get: engine self-writes (the status mirror) pass. Lose: the lock that stops edits while an approval is live. Note there is deliberately no admin exemption here — only isSystem |
packages/plugins/plugin-approvals/src/lifecycle-hooks.ts#bindApprovalLockHook |
| 42 | Delegation write guard bypassed | plugin-approvals | Get: service / seed / import may write delegation rows naming another delegator | packages/plugins/plugin-approvals/src/lifecycle-hooks.ts#bindDelegationWriteGuard |
| 43 | Approval actor / submitter / pending-approver checks bypassed (8 sites) | plugin-approvals | Get: approve, reject, recall, reassign without being a pending approver or the submitter | packages/plugins/plugin-approvals/src/approval-service.ts#isOverrideActor, #resolveActor, #sendBack, #resubmit, #reassign, #remind, #requestInfo, #comment |
| 44 | Attachment access hooks return early (insert + update + delete, and the read AST) | service-storage | Lose: attachment visibility scoping | packages/services/service-storage/src/attachment-access-hooks.ts#installAttachmentAccessHooks, #installAttachmentReadVisibility |
| 45 | Comment access hooks return early (insert + update + delete, and the read AST), and so do the activity and audit-log read gates (their read AST), the activity field redaction and the audit-log field redaction (the rows a read serves), and the query guard over both objects' value-bearing columns (the read AST) | plugin-audit | Get: the whole activity row and the whole sys_audit_log row, its before/after snapshots included, on find / findOne — the audit writer, each read gate's bounded probe and the activity backfill — and a filter, sort or grouping by those columns. Lose: comment visibility scoping, the narrowing of sys_activity and of sys_audit_log to rows whose parent record the caller can read, the redaction of a parent field's value from an activity row's text and recorded change and from a ledger row's before/after snapshots, the withholding of an activity row recording an update whose every changed field the reader is withheld, of a fired milestone row whose watched fields it is not all served, and of an unstamped row (on count and aggregate too, so a total agrees with the rows), and the refusal of a query over those columns for a reader withheld a field of the objects it can reach |
packages/plugins/plugin-audit/src/comment-access-hooks.ts#installCommentAccessHooks, #installCommentReadVisibility, packages/plugins/plugin-audit/src/activity-read-visibility.ts#installActivityReadVisibility, packages/plugins/plugin-audit/src/audit-log-read-visibility.ts#installAuditLogReadVisibility, packages/plugins/plugin-audit/src/activity-field-redaction.ts#installActivityFieldRedaction, packages/plugins/plugin-audit/src/audit-log-field-redaction.ts#installAuditLogFieldRedaction, packages/plugins/plugin-audit/src/parent-field-query-guard.ts#installParentFieldQueryGuard |
| 46 | Knowledge search returns hits unfiltered | service-knowledge | Lose: the permission filter over search results | packages/services/service-knowledge/src/knowledge-service.ts#applyPermissionFilter |
| 47 | The inbound-mail read gates return early (the read AST) | service-mailbox | Get: every sys_mail_message and sys_mail_association row on find / findOne — the ingest writer and each gate's own pre-scan, which read the association and mailbox rows a decision needs. Lose: the narrowing of a message to callers who can read a record it is associated with or who own its mailbox (so the unassociated pool is restricted), and of an association to callers who can read the record it names (on count and aggregate too, so a total agrees with the rows) |
packages/services/service-mailbox/src/mail-read-visibility.ts#gateMiddleware |
| 47b | The inbound-mail ingest seam: ingest admits the system context ALONE, and rematch admits it without the organization, owner or manage_mailboxes check |
service-mailbox | Get: a mail source hands a mailbox its messages, and server code re-matches any mailbox. Lose: nothing a user held — every other caller of ingest is refused (PERMISSION_DENIED), because an ingested message is shown on every record its correspondents' addresses match and a user who could hand the seam a message could write one in a correspondent's name; every other caller of rematch must be in the mailbox's organization and own the mailbox or hold manage_mailboxes |
packages/services/service-mailbox/src/ingest/mail-ingest-service.ts#ingest, #authorizeRematch |
| 47c | The inbound-mail IMAP sync: syncMailbox and syncAll admit the system context ALONE |
service-mailbox | Get: the sync job reads every mailbox row (its organization, address and IMAP cursor), writes the cursor back, and hands each fetched message to ingest under the system context. Lose: nothing a user held — every other caller is refused (PERMISSION_DENIED), because a sync stores a mailbox's mail and moves its cursor |
packages/services/service-mailbox/src/sync/mailbox-sync-service.ts#syncMailbox, #syncAll |
| 47d | The print service REFUSES the system context: printRecordToFile admits a human user acting for themselves alone |
service-print | Get: nothing — a system caller is refused (PERMISSION_DENIED) before any page is read, session minted or browser started. Lose: nothing a user held — the PDF is rendered as the requesting user and never as the system, so a record is archived only by someone who could read it |
packages/services/service-print/src/print-service.ts#refuseUnlessHumanCaller |
| 47e | The caller-scoped attach verbs REFUSE the system context: attachFileAs and assertMayAttach admit a signed-in user alone |
service-storage | Get: nothing — a system caller is refused (PERMISSION_DENIED); the system attaches through attachFile, whose rows name no user. Lose: nothing a user held — the caller-scoped verbs insert the attachment under the caller's context so every insert gate judges that user and both rows name them |
packages/services/service-storage/src/file-attachment-service.ts#requireUserContext |
| # | Behaviour when isSystem |
Package | What you get / what you lose | Anchor |
|---|---|---|---|---|
| 48 | Object API-exposure gate bypassed (apiEnabled / apiMethods) |
runtime | Get: internal self-writes ignore exposure declarations — these govern external exposure, not engine self-writes. The gate is refuseUnexposedDataAction, which callData asks first and the /data domain asks before its request gate; callData reads the same bypass once more for each nested-relation condition's target |
packages/runtime/src/action-execution.ts#callData, #refuseUnexposedDataAction |
| 49 | Action requiredPermissions bypassed |
runtime | Get: engine self-invocation runs any action | packages/runtime/src/action-execution.ts#actionPermissionError |
| 50 | manage_metadata bypassed on metadata writes |
runtime, rest | Get: schema writes without the capability (the permission-set clone door's save included) — and, because REST's save door and the stored-version read doors ask one save verdict, an app's full stored version on /layers, ?layers=true and /diff, whatever its entry gates withhold |
packages/runtime/src/domains/meta.ts#handleMetadataRequest, packages/rest/src/meta-collection-routes.ts#registerMetaCollectionRoutes, packages/rest/src/meta-item-write-routes.ts#registerMetaItemWriteRoutes, packages/rest/src/meta-item-history-routes.ts#registerMetaItemHistoryRoutes, packages/rest/src/rest-server.ts#metaSaveVerdict, packages/runtime/src/domains/permission-set-clone.ts#handlePermissionSetCloneWrite |
| 51 | The shared metadata-write verdict itself returns allowed |
metadata-core | Get: the one function all of row 50's doors consult answers yes before any capability is examined | packages/metadata-core/src/meta-write-capability.ts#metaWriteCapabilityVerdict |
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no userId |
packages/runtime/src/domains/actions.ts#handleActionsRequest, packages/runtime/src/domains/ai.ts#handleAIRequest, packages/runtime/src/domains/automation.ts#handleAutomationRequest, packages/runtime/src/domains/meta.ts#handleMetadataRequest, packages/runtime/src/domains/security.ts#handleSecurityRequest, packages/runtime/src/domains/packages.ts#handlePackagesRequest, packages/runtime/src/domains/analytics.ts#handleAnalyticsRequest, packages/runtime/src/domains/i18n.ts#handleI18nRequest, packages/rest/src/external-datasource-routes.ts#registerExternalDatasourceRoutes, packages/rest/src/package-routes.ts#refusePackageRequest |
| 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | packages/runtime/src/domains/mcp.ts#handleMcpRequest |
| 54 | Package REST route capability gate bypassed | rest | Get: a marketplace publish over REST (POST /packages/publish, the one route the REST registrar mounts since #14503) without manage_metadata; the package read cohort (studio.access / setup.access) is enforced by the dispatcher /packages domain's own read gate, where the reads are served |
packages/rest/src/package-routes.ts#refusePackageRequest |
| 55 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | packages/runtime/src/domains/packages.ts#requireManageMetadata, #requireReadCapability |
| 56 | Activation write / authoring refusals do not fire | runtime | Get: activation artifacts writable and authorable without the activation-authoring capability | packages/runtime/src/domains/activation-gate.ts#refuseUngrantedActivationWrite, #refuseUngrantedActivationAuthoring |
| 55b | Walled environment-metadata refusal does not fire | metadata-core, rest, runtime | Get: under isolated / group, a write of the environment's metadata, of any type, at the /meta write doors (save, reset, publish, rollback) and POST /meta/_migrate-stored on both transports, at every /packages write door (create, the manifest PATCH, enable, disable, publish, publish-drafts, discard-drafts, revert, rollback, commits/:commitId/revert, duplicate, adopt-orphans, delete), at the /automation definition writes (create, update, delete, clone), and at the permission-set clone door. Every other caller below the platform-operator rung is refused 403 there (ADR-0131 D3, D12 items 7–8, D11), because one organization's write would change what every organization resolves. The seeders, the package door and migrations write the environment's metadata this way. Under single the refusal is inert for every caller |
packages/metadata-core/src/meta-write-capability.ts#walledCatalogWriteVerdict, packages/rest/src/rest-server.ts#walledCatalogRefusal, packages/runtime/src/domains/packages.ts#refuseWalledCatalogPackageWrite, packages/runtime/src/domains/automation.ts#refuseWalledFlowDefinitionWrite |
| 57 | Automation run-state read, flow-authoring write, the paused-run caller gate (screen read and resume), the two operator run-lifecycle writes and the elevated self-triggered start at every door that starts a flow by name all pass | runtime | Get: run state, flow writes, a paused run's screen and its resume with no grant and without being the run's starter — and cancelling or restoring a suspension without the platform-operator rung. The screen read and the resume ask one predicate, so this is one bypass for both. The lifecycle bypass is the in-process owner's door: plugin-approvals' revise-window recall (ADR-0044) cancels on behalf of a decision it already authorized and recorded. And starting a flow declared runAs: 'system' whose type is self-triggered (autolaunched, record_change, schedule) through any door that starts a flow by name — the trigger route, an action of type: 'flow' (REST /actions and the MCP run_action bridge) and a declared endpoint of type: 'flow' — which every other caller, the guest principal included, is refused 403 PERMISSION_DENIED (the maintainer's rulings, letters B and A). The three doors ask one predicate, so this is one bypass for all of them, and it is the in-process caller's, such as a job: inbound HTTP never carries the flag |
packages/runtime/src/domains/automation.ts#mayReadRunState, #refuseUngrantedFlowWrite, #isRunStarterOrRunStateReader, #refuseUngrantedRunLifecycleWrite, packages/runtime/src/flow-start-admission.ts#refusesElevatedSelfTriggeredStart |
| 58 | Audience-binding suggestion authority gate passes | plugin-security | Get: a system caller lists, accepts and dismisses suggestions without manage_metadata and, under a wall, without the platform-operator rung |
packages/plugins/plugin-security/src/suggested-audience-bindings.ts#assertDeploymentAuthority |
| 59 | Email-template organization door passes; webhook provenance stamp skipped | plugin-email, plugin-webhooks | Get (sys_email_template): a create or update of a template row. The door refuses every other write that names a caller, 403 PERMISSION_DENIED (ADR-0131 D6); the plugin itself no longer writes the table (templates resolve from the registry), so the v18 migration ceremony's promotion is the one remaining writer. Lose (sys_webhook): the row is not marked as an admin customization, and the declared-name guard does not run (a new row or a rename taking a declared webhook's name, 409) — the system writers are the platform's machinery (the legacy-secret sweep, the ownership ceremony, migrations) |
packages/plugins/plugin-email/src/email-template-door.ts#isOrganizationWrite, packages/plugins/plugin-webhooks/src/webhook-provenance.ts#bindWebhookProvenanceStamp, packages/plugins/plugin-webhooks/src/declared-webhooks.ts#bindDeclaredWebhookNameGuard |
| 60 | Automation flow data nodes re-add the owner_id stamp (the one place row 2's gap is compensated inline) |
service-automation | Get: a flow-authored INSERT under system elevation still lands owned, when the run resolved a user. Fill-only — flow-authored values win | packages/services/service-automation/src/runtime-identity.ts#stampSystemInsertOwner, called from packages/services/service-automation/src/builtin/crud-nodes.ts#registerCrudNodes |
| 61 | Inbox caller refusal names isSystem as what was carried |
service-messaging | Get: nothing — the refusal still fires. The flag only shapes the diagnostic, because privilege is not an authorization subject | packages/services/service-messaging/src/inbox-caller.ts#resolveInboxRecipient |
| 61b | The principal-session mint refuses a system context | plugin-auth | Get: nothing — mintPrincipalSession answers PERMISSION_DENIED / 403 before anything is minted. A principal session acts as the requesting user and never as the system, so a render or any other in-process act that needs a session must carry the user's own context |
packages/plugins/plugin-auth/src/principal-session-mint.ts#refuseUpFront |
| 62 | organization_id is not auto-stamped on INSERT — the organization-axis twin of the owner_id gap above |
organizations | Get: an elevated write may name another organization deliberately, which is what the per-organization seed replay, the orphan-row claim, imports and migrations all rely on. Lose: the fill-only stamp, so an elevated insert that names no organization lands organization_id = NULL and the wall hides it. ⛔ Neither path rewrites a supplied organization_id: the non-elevated path fills only an absent one, and a supplied value meets the Layer 0 write wall, which refuses a forged one 403 PERMISSION_DENIED exactly as it refuses an update re-pointing a row. Elevation skips the stamp and the wall alike, which is why it is the seam the legitimate cross-organization writers use |
packages/plugins/organizations/src/organizations-plugin.ts#start |
Not behaviours: these three sites read the flag solely to copy it into another context or envelope. They are listed because the census counts them, and because a reader tracing where elevation travels needs them.
| # | Site | Package | What it does |
|---|---|---|---|
| 63 | packages/objectql/src/engine.ts#buildSession |
objectql | Propagates isSystem into the hook session so hooks can tell engine self-writes from user writes |
| 64 | packages/objectql/src/engine.ts#isSystem |
objectql | ScopedContext.isSystem getter — re-exposes the underlying execution context's flag |
| 65 | packages/runtime/src/sandbox/body-runner.ts#executionContextFromHook |
runtime | Rebuilds an ExecutionContext from a hook session, carrying the flag across |
Just as costly as the list above. Each of these is a separate switch, and
assuming isSystem covers it is a documented source of bugs.
| Assumption | Reality | Anchor |
|---|---|---|
| "It suppresses triggers / record-change automation" | No. Only skipTriggers does. A bare { isSystem: true } on a seed write re-fired automation on freshly seeded rows and wedged first boot |
packages/metadata-protocol/src/seed-loader.ts#SEED_OPTIONS (rationale at #writeDeferredReference, #3760), packages/spec/src/automation/flow.zod.ts#runAs |
| "It skips the state machine" | No. That is skipStateMachine, carried by seed replay and by treatAsHistorical imports |
packages/objectql/src/engine.ts FSM gate; see State Machine |
| "It skips validation rules" | No. Field shape, format, script and the rest still run. The readonly strip runs before validation precisely so a discarded value is not judged |
packages/objectql/src/engine.ts#insert |
"It preserves a supplied updated_at / updated_by" |
No. That is preserveAudit, a separate opt-in — and an UPDATE-path exemption only |
packages/spec/src/data/field.zod.ts#readonly (#3493 / #6640) |
"It stamps created_by" |
No. Audit stamping reads userId from the context. A user-less system write stamps nothing — that is today's behaviour, not an error |
packages/services/service-automation/src/runtime-identity.ts#stampSystemInsertOwner |
| "It bypasses every guard" | No. The last-admin guard applies to every context, isSystem included — the deprovision path that actually locks an org out is the system one |
packages/plugins/plugin-auth/src/last-admin-guard.ts |
| "A client can request it" | No. Never settable from inbound HTTP or from an action body | packages/rest/src/rest-server.ts#enforceAuth; packages/runtime/src/domains/actions.ts#handleActionsRequest |
Recorded rather than smoothed over, because a reader who hits one of these should recognise it instead of re-deriving it.
-
The
owner_idgap has two independent compensations and no shared mechanism. Row 2 is a real gap; the platform repairs it twice, in unrelated places — inline for automation flow writes (packages/services/service-automation/src/runtime-identity.ts#stampSystemInsertOwner, whose own comment states the reason: "the security middleware that stamps it short-circuits onisSystem— so the writer fills it here"), and as a boot-time sweep for seeded rows (packages/plugins/plugin-security/src/claim-seed-ownership.ts). Any third system write path gets neither. If you add one, stamp ownership yourself. -
Sharing materialisation is no longer skipped — the rough edge is closed, and it is recorded here rather than deleted. This entry used to describe "configured but inert": nine installed sharing rules, matching records, correct positions — and
sys_record_shareempty, repaired only by a re-evaluation or a restart. The reading that made it a rough edge rather than a defect was that the boot backfill eventually fixes it, so the behaviour was correct and merely undiscoverable; #6783 shipped an INFO notice on that reasoning.That reading did not survive contact with a runtime write. An approval node's write-back is a system write —
lockRecord: truemeans only a platform write can land while the record is locked — and it happens long after boot, so no backfill was coming: a manager approved a request and the teammate who depended on the criteria rulestatus == "approved"could not see it at all. The maintainer ruled on 2026-08-31 (#13533) that the skip was a bug, because a sharing rule's declared semantics is a published promise andisSystemnames the operator, never a consequence that need not happen. Both materialisation skips and the notice that announced them are gone; row 31 is now theafterDeleteskip alone, which survives on the separate ground that another subscriber delivers that payload.⚠️ The observability half of that reading is worth keeping in mind independently: the only signal a builder ever had was one INFO line, and nothing in the docs or the tests said "after approval, when does the team see it?". A compensating path that exists but that nobody can be expected to know about is not a compensating path. -
Strict write observability is inert under elevation. Row 22: a caller that asked to be told loudly about dropped fields is told nothing, because nothing was dropped. The two facts are indistinguishable from the outside.
-
revoke()skips its own conflict guard. Row 35 is correct for the rule evaluator and surprising for anything else: a system caller can delete a rule-materialised grant that the next reconcile silently restores. -
applySystemFieldsdoes not read this flag. It is named as if it did.packages/objectql/src/registry.ts#applySystemFieldsis schema-side column provisioning — which columns an object carries — and consumesExecutionContext.isSystemzero times. The write-time ownership behaviour people attribute to it is row 2, inplugin-security.
Maintainer ruling, #4707, 2026-08-06. Recorded here at the ruling's own request, so the proposal stops being re-opened.
Ownership injection, readonly bypass and sharing materialisation are
independent decisions, and a seed loader plausibly wants the first two but not
the third. The concept is nevertheless staying as one boolean:
- Shipped semantics.
isSystemis a published contract read at over a hundred sites across some twenty packages. Splitting it is a breaking contract change across all of them. (The ruling was taken when the census read 80 sites in 18 packages; the count has grown since, which strengthens rather than weakens the argument.) - No business pull. No app has asked for the combinations a split would enable; the observed need was to understand the flag, which is what this page serves.
- Combinatorics are worse for AI authors, not better. Three independent switches are eight states, most of them untested and several of them incoherent (grant materialisation without ownership). One flag plus this table is judged more mistake-proof at authoring time than a surface where a wrong combination is expressible and silently valid.
The trade-off accepted with that ruling is that elevation stays coarse: you
cannot ask for the ownership behaviour without also taking the sharing
behaviour. Where a narrower need exists, the platform answers it with a
separate, explicit option next to isSystem — skipTriggers,
preserveAudit, skipStateMachine, runAs — rather than by subdividing the
flag. That is the pattern to follow for any new narrow exemption.
The table's value is exhaustiveness, so it is built by census, not by recall — and the census is a committed instrument rather than someone's afternoon:
node scripts/isystem-census.mjs # the live totals, for the tree you run it on
node scripts/isystem-census.mjs --json # every site, its kind and its packageThis page states no totals, and that is deliberate. How many sites, in how many packages and files, is derived data: it moves whenever a read arrives or leaves, and a figure typed into hand-written prose is paid for at every merge. Two branches that each add a read bump the same sentence and conflict on it — or, bumping it to the same number, text-merge clean to a total neither tree holds, which is how this page once went silently wrong (#16919). So the totals are computed where they are read: the census prints them for the tree you run it on, and the check below prints them again on every pass. What CI holds is the register itself — every elevation read anchored, every anchor resolving — not a sum of it. A site that arrived or vanished is a human's editorial call: add or drop its row.
⛔ Not a grep. A text scan is where a census starts and it cannot be where
one ends: it returns prose inside comments and strings, the three unrelated
metadata fields, and the isSystemObject / isSystemObjectName /
isSystemLedgerObject name helpers. Worse, it loses real sites — a regex pass
over this same corpus silently dropped 6 reads in
the saved-report service (retired since, #20102) to a quoting desync and 11 more to
(ctx?.session as any)?.isSystem casts. So the census walks the TypeScript AST
and classifies each appearance of the identifier by where the parser puts it:
a read (a table row here), a declaration, an object-literal or type
key (a producer, not a behaviour), or something else.
Corpus: tracked .ts / .tsx / .mts / .cts under packages/ and
examples/, excluding dist/. A file counts as a test when its path carries
.test. / .spec. or a tests/ / __tests__/ / qa/ segment. The census
narrows that corpus in the order below. The raw text it starts from is kept here
as a dated snapshot:
| Raw text | Count |
|---|---|
Lines carrying isSystem in the corpus |
1811 |
| — in tests | 1013 |
| — in non-test sources | 798 |
Appearances of the bare identifier isSystem in non-test sources |
813 |
| — parsed as an object-literal / type key (producers and option objects) | 310 |
| — the remainder: text inside comments and string literals | 358 |
The rest of those appearances are what the census classifies, and the page restates none of its figures:
| Census step | What it holds |
|---|---|
| Appearances parsed as a declaration | every declaring slot — see below |
| Appearances parsed as a property read | every read, before the collision is subtracted |
| Appearances parsed in some other syntactic position | a local, a cast, a conditional |
| Of those reads: reads of one of the unrelated metadata fields | subtracted — the collision this page opens with |
Of those reads: reads of ExecutionContext.isSystem |
the census answer: every one sits in a symbol a row above anchors |
| — behaviour-bearing (rows 1–62 above) | sections 1 to 5 |
| — carry the flag onward only (rows 63–65 above) | section 6 |
node scripts/isystem-census.mjs prints how many appearances fell in each
syntactic slot, the subtraction, the answer and the packages and files that
hold it;
node scripts/check-system-context-census.mjs prints the answer again on every
pass, with the distinct symbols those reads live in and how many files hold more
than one read in one symbol.
The six raw-text rows are a dated decomposition, not a live claim: they were
measured on 2026-08-29 at ca1965f2b5 and CI does not re-derive them. They count
raw text over the whole corpus — every line, identifier appearance,
object-literal key and prose mention of the string isSystem under packages/
and examples/, tests included — which is a different population from the one
this page certifies, and it churns for reasons that have nothing to do with
elevation: a test that mentions the flag, a seed object carrying
isSystem: true, a comment. Enforcing them made this page's own pull request
un-landable, because CI scores a merge with main and the merge queue
re-derives that merge against a newer main on every attempt: three unrelated
merges moved these six numbers eight times in one night, while the census
population — 109 sites in 20 packages across 45 files at the time — did not
move once. They are kept because they are how far a raw grep sits from the
census answer, which is the whole argument of this section; they are unenforced
because a count of lines in test files certifies nothing. Re-measure them with
node scripts/isystem-census.mjs when you want them current, and move the date.
What the census counts as a declaration. Not only the three field
declarations above — those are three distinct fields that happen to share a
name, and only the first is elevation. It counts every position where the
parser puts the identifier in a declaring slot: those three, plus the
structural type literals that restate ExecutionContext.isSystem's shape inline
rather than importing it ({ isSystem: true; tenantId?: string },
context?: { isSystem?: boolean }, and the get isSystem() accessor on the
engine's context wrapper). A restatement is a producer's declaration of the
shape it will build, never a read, so a new one moves the census's declaration
count and nothing on this page — the read population and the anchored rows
above stay where they are. The most recent arrival is the scoped seed context threaded into the org-admin permission-set lookup in
packages/plugins/plugin-security/src/auto-org-admin-grant.ts, so that read is scoped
to the granting organization rather than read organization-less (#11670). It
reads the legacy sys_permission_set ids an old grant may carry; that object
is no longer registered (ADR-0131 D3), so the read answers no rows. ⛔ Cited as a FILE deliberately, with no
#symbol: this page's symbol anchors name the enclosing declaration of an
elevation read, and a declaration of the shape is not one — so there is no
symbol here for a row to name, and inventing one would put a name on the page
that no rename could ever red.
Counting by hand is what made the previous edition wrong in two independent
ways, so both are worth naming. Its headline said "80 distinct sites across 18
packages" while its own tables anchored 77 — the number never matched the
page it described. And a grep -c for anchor-shaped text over the previous edition answered
64, because it counted lines carrying an anchor, not anchors: that page's
real anchor population was 111 once continuation anchors and range ends are
counted. Decompose a text count before comparing it to anything.
A line-number anchor rots on every unrelated edit to the same file, silently:
re-resolving all 111 anchors of the edition before last found 101 pointing at
a line that no longer held what the row named, while only 10 were still
correct — and 41 of them named a basename that matches two files, so they
could not be placed without reading the row's Package column. Neither failure is
reachable from this edition: it carries no line numbers at all, and it spells
every path in full from the repository root. Each anchor is
packages/…/file.ts#symbol, resolved by the shared symbol-anchor resolver
(scripts/symbol-anchors.mjs) against that file's own declaration sites — the
same resolver, and the same registration shape, that holds docs/adr/**.
Renaming a symbol is now a loud red instead of a silent misdirection.
- Delete a whole symbol and this page reds — the anchor stops resolving and the symbol set stops matching, in the same run.
- Add or delete one of several reads inside a symbol that keeps at least one,
and the symbol set does not move, so it does not red. While this page stated
totals, the site count moved on that edit — but the repair it prescribed was a
generator rewriting the number, never a row. The previous edition's line
numbers did close this one: both reads lost in the last drift lived inside
callerContext()helpers that still exist under the same names. Closing it again means a span-aware resolver and per-read disambiguation in those files; that is its own card, and it is deliberately not folded in here.
Resolving anchors is nevertheless the second check, not the first. ⭐ A gate that only checks what the page already says can never find what the page failed to say. Re-resolving every anchor of the edition before last would have passed while it was missing 32 sites and its headline was 29 too low. So the load-bearing direction runs census → page: every elevation read in the code must be anchored here, and a symbol whose last read vanishes from the code leaves an anchor that names no read site, which is what makes a row describing a protection that no longer exists fail.
Four checks run, in scripts/check-system-context-census.mjs:
| Check | What fails |
|---|---|
| Population | a file holding an elevation read with no anchor here, or a symbol holding one that no anchor here names |
| Resolution | an anchor naming no tracked file, or naming a symbol that file does not declare — and any surviving line number, which is no longer an anchor form |
| Classification | an anchor that is not a read site and is not a declared non-read citation |
| Dated snapshot | a raw-text row above reworded off the page, or its measurement date dropped — the numbers themselves are never compared |
A new read of ExecutionContext.isSystem belongs in this table in the same PR
that introduces it — CI will say so when it lands in a symbol no row anchors
yet; inside a symbol that already has one, only the reviewer will.
- Authorization Architecture — the six-gate enforcement chain this flag short-circuits
- Security & Access Control — the
readonlywrite strip and its exemptions - State Machine —
skipStateMachine,preserveAudit,treatAsHistorical - Sharing Rules — what row 31 is skipping on the record-
afterDeletehook