@substrat-run/adapter-sqlite 0.140.0 → 0.142.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import Database from 'better-sqlite3';
4
- import { accessLogEntry, adminLogEntry, delegatedReadParams, operationSeriesCount, opsFailureEntry, opsFailureFingerprint, issueEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunEntry, modelUsageEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, createTenantInput, domainEvent, domainEventInput, exportReadInput, importBatch, importCursorMove, exportsOfManifestJson, importsOfManifestJson, entitlementGrant, entitlementGrantInput, eventId, meterReading, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, identityLink, identityPool, instant, connection, capabilityGrant, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, systemSwitch, systemSwitchRecord, peerSwitch, verticalCaller, verticalSlug as verticalSlugOf, subjectRef, requestFingerprint, createConnectionInput, connectLinkFilter, connectLinkKey, consumeConnectLinkInput, mintConnectLinkInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, createOrgInput, promotionAcknowledgement, bindAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, routeTarget, ROUTE_SCOPE_HOSTNAMES_MAX, registerVerticalInput, vertical as verticalSchema, verticalChannel, verticalVersion, objectRef, entityObjectRef, assertModuleEmittableType, assertKernelAuthoredType, grantRefFromProof, org as orgSchema, orgMembership, principalId, resolvedIdentity, identityMembership, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, subjectShredReceipt, SCOPE_TABLE_PAGE_DEFAULT, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, verticalServingState, callsOfManifestJson, outboundOfManifestJson, listLimitOf, pageOf, countedPageOf, SCOPE_GATE_REASONS, substratError, assertReplayableDump, delegatedReadRecord, ownerTransferAudit, memberChangeAudit, peerBindingAudit, copyMarkAudit, redrainEventsInput, REDRAIN_BATCH, } from '@substrat-run/contracts';
5
- import { actorOf, asPrincipal, assertAllowed, assertNoSecret, assertPermissionKey, CAPABILITY_DDL, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, CAPABILITY_EXCHANGE_OPERATION, capabilityAttachmentWriteRefused, capabilityTokenHash, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, entityStateMigrations, assertNoCallerPurge, isUnreachableParent, heldPurgePass, purgeDueOf, purgeHeldBy, lifecycleRefusal, purgeOnlyKeysOf, purgeReportOf, purgeStillDue, refuseTrashedTarget, registerTrashTargets, runPurgePass, withheldKeysFor, statefulTablesOf, afterMigration, afterRuntimeDdl, derivesAnything, repairDerivedObjects, StateColumnLost, exchangeCapability as exchangeCapabilitySecret, guardSecrets, mintBecomeCapability, plausibleSessionToken, readCapabilityPage, redactSecrets, redactSecretText, moduleLog, consoleLogSink, asyncInvocationId, asyncLinePass, consoleInvocationLineSink, resolveCapabilitySession, revokeCapabilityAsPlatform, assertReadOnlyQuery, attachmentBlobKey, attachmentSha256, entitlementDenial, requiredEntitlementFor, foldMeterReading, guardSpine, guardSqlLimits, DO_SQL_LIMITS, tooManyResultColumns, tooManyTableColumns, parseValidationRecords, resolveScopeRecord, ulid, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, FINDINGS_DDL, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink, listConnectLinks as listConnectLinkRows, readConnectLink, restoreConnectLinkRow, revokeConnectLinkRow, createFindingRule, findingOfOpsFailure, findingOfSweepRun, pruneFindings, listFindingRules, listFindings, observeFinding, revokeFindingRule, setFindingStatus, assertRowLimit, assertRowOffset, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, MODEL_USAGE_RETENTION_DAYS, foldModelUsage, backoffAt, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, eraseSubjectFromModules, moduleRowsErased, moduleErasurePlan, redactSubjectScopeText, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_STEP_RECORD_SQL, JOB_LEASE_EXPIRED_NOTE, assertLeaseMs, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, admitPeer, collectPeers, peerSeats, resolveVerticalInstanceFrom, resolvePeerInstanceFrom, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, PEER_SWITCHES_DDL, PEER_BINDINGS_DDL, SWITCH_OWED_DDL, SWITCH_FENCES_DDL, TABLE_OWNERS_DDL, assertMigrationLeavesLedgerAlone, recordOwnershipSteps, runMigrationStatements, executableSqlStatements, blankSqlComments, recordWriteSuperseded, switchFencesOf, switchSupersededMessage, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, moveSwitch, reassertActionOf, reassertEntry, reassertOffRow, recordSwitchedOff, recordSwitchedOn, restoreSwitchRecord, scopesSwitchedOffFor, switchActionOf, switchAuditSubject, recordAuditOutcome, auditWarningOf, switchNotFoundMessage, switchRecordsOf, switchedOffOf, switchesBackfillSqlOf, switchesTableExists, tenantHeldOf, tenantHoldsGrant, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, withRecorded, VERSION_MIGRATIONS_DDL, splitManifestMigrations, splitVersionMigrationsBatch, versionMigrationsOf, writeVersionMigrations, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, ImpersonationRefused, assertImpersonationWrites, assertModuleEnqueueableKind, assertSessionUsable, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, impersonationStampOf, mapImpersonationRow, newImpersonationSession, resolveRetryPolicy, executorOutcomeOf, isDeliveryRefusal, refusalJournalText, membershipFencesBackfillSql, MEMBERSHIP_FENCES_DDL, membershipFencesTableExists, MEMBERSHIP_FENCE_SINCE_SQL, RAISE_MEMBERSHIP_FENCE_SQL, memberAddedAudit, orgChangeBound, tenantCoverage, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexMigrations, listIndexPlans, listQuery, cursorOf, moduleMigrations, MIGRATION_DIGEST_FENCE_DDL, MIGRATION_DIGEST_MARK_LEGACY, assertJournalDumpCoherent, assertMigrationSql, migrationDivergence, migrationFailedError, migrationSteps, planMigrations, isSearchIndexTable, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, assertAttachmentExtractors, resolveAttachmentTextBounds, assertJobRegistrable, attachmentRecordOfRow, attachmentTextJob, enqueueAttachmentText, ATTACHMENT_TEXT_BACKFILL_JOB, ATTACHMENT_TEXT_JOB, kernelJobFor, queueAttachmentTextBackfill, startAttachmentTextBackfill, attachmentTextBackfillJob, reconcileAttachmentText, recordAttachmentText, searchAttachments, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, emptyImportResult, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, emittedSinceQuery, emittedReportOf, EMITTED_REPORT_CAP, exportedSinceQuery, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, importCursorSourceOf, exportBreaksOf, exportBreakRefusal, bindExportBreaksOf, bindExportBreakRefusal, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, JOB_RUN_DUE_AT, jobRunListLimit, jobRunOf, runDueJobRuns, startJobRun, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, globalFetch, createUlid, facetEvents, readHistory, walkEventCause, walkEventEffects, assertRedrainWindow, readInvocation, readDeadLetters, readLifecycleFlow, readOperationSeries, domainEventOf, readUndrainedOutbox, undrainedEventsOf, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, unknownRoleError, } from '@substrat-run/kernel';
4
+ import { accessLogEntry, adminLogEntry, delegatedReadParams, operationSeriesCount, opsFailureEntry, opsFailureFingerprint, issueEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunEntry, modelUsageEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, createTenantInput, domainEvent, domainEventInput, exportReadInput, importBatch, importCursorMove, exportsOfManifestJson, importsOfManifestJson, entitlementGrant, entitlementGrantInput, eventId, meterReading, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, identityLink, identityPool, instant, isSubstratError, connection, capabilityGrant, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, systemSwitch, systemSwitchRecord, peerSwitch, verticalCaller, verticalSlug as verticalSlugOf, subjectRef, requestFingerprint, createConnectionInput, connectLinkFilter, connectLinkKey, consumeConnectLinkInput, mintConnectLinkInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, createOrgInput, promotionAcknowledgement, bindAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, routeTarget, ROUTE_SCOPE_HOSTNAMES_MAX, registerVerticalInput, vertical as verticalSchema, verticalChannel, verticalVersion, objectRef, entityObjectRef, assertModuleEmittableType, assertKernelAuthoredType, grantRefFromProof, org as orgSchema, orgMembership, principalId, resolvedIdentity, identityMembership, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, principalBecomeCapabilityInput, subjectShredReceipt, SCOPE_TABLE_PAGE_DEFAULT, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, verticalServingState, callsOfManifestJson, outboundOfManifestJson, listLimitOf, pageOf, countedPageOf, SCOPE_GATE_REASONS, substratError, assertReplayableDump, delegatedReadRecord, ownerTransferAudit, memberChangeAudit, peerBindingAudit, copyMarkAudit, redrainEventsInput, REDRAIN_BATCH, } from '@substrat-run/contracts';
5
+ import { actorOf, asPrincipal, assertAllowed, assertNoSecret, assertPermissionKey, CAPABILITY_DDL, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, CAPABILITY_EXCHANGE_OPERATION, CAPABILITY_BECOME_MINT_OPERATION, becomeMintCheck, mintBecomeCapabilityAsPrincipal, revokeBecomeCapabilityAsPrincipal, readBecomeLinkStates, capabilityAttachmentWriteRefused, capabilityTokenHash, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, entityStateMigrations, assertNoCallerPurge, isUnreachableParent, heldPurgePass, purgeDueOf, purgeHeldBy, lifecycleRefusal, purgeOnlyKeysOf, purgeReportOf, purgeStillDue, refuseTrashedTarget, registerTrashTargets, runPurgePass, withheldKeysFor, statefulTablesOf, afterMigration, afterRuntimeDdl, derivesAnything, repairDerivedObjects, StateColumnLost, exchangeCapability as exchangeCapabilitySecret, guardSecrets, mintBecomeCapability, plausibleSessionToken, readCapabilityPage, redactSecrets, redactSecretText, moduleLog, consoleLogSink, asyncInvocationId, asyncLinePass, consoleInvocationLineSink, resolveCapabilitySession, revokeCapabilityAsPlatform, assertReadOnlyQuery, attachmentBlobKey, attachmentSha256, entitlementDenial, requiredEntitlementFor, foldMeterReading, guardSpine, guardSqlLimits, DO_SQL_LIMITS, tooManyResultColumns, tooManyTableColumns, parseValidationRecords, resolveScopeRecord, ulid, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, FINDINGS_DDL, SCOPE_STORAGE_DDL, forgetScopeStorage, listScopeStorageAttemptRows, listScopeStorageRows, pruneScopeStorageRows, recordScopeStorageRows, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink, listConnectLinks as listConnectLinkRows, readConnectLink, restoreConnectLinkRow, revokeConnectLinkRow, createFindingRule, findingOfOpsFailure, findingOfSweepRun, pruneFindings, listFindingRules, listFindings, observeFinding, revokeFindingRule, setFindingStatus, assertRowLimit, assertRowOffset, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, MODEL_USAGE_RETENTION_DAYS, foldModelUsage, backoffAt, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, eraseSubjectFromModules, moduleRowsErased, moduleErasurePlan, redactSubjectScopeText, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_STEP_RECORD_SQL, JOB_LEASE_EXPIRED_NOTE, assertLeaseMs, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, admitPeer, collectPeers, peerSeats, resolveVerticalInstanceFrom, resolvePeerInstanceFrom, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, PEER_SWITCHES_DDL, PEER_BINDINGS_DDL, SWITCH_OWED_DDL, SWITCH_FENCES_DDL, TABLE_OWNERS_DDL, assertMigrationLeavesLedgerAlone, recordOwnershipSteps, runMigrationStatements, executableSqlStatements, blankSqlComments, recordWriteSuperseded, switchFencesOf, switchSupersededMessage, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, moveSwitch, reassertActionOf, reassertEntry, reassertOffRow, recordSwitchedOff, recordSwitchedOn, restoreSwitchRecord, scopesSwitchedOffFor, switchActionOf, switchAuditSubject, recordAuditOutcome, auditWarningOf, switchNotFoundMessage, switchRecordsOf, switchedOffOf, switchesBackfillSqlOf, switchesTableExists, tenantHeldOf, tenantHoldsGrant, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, withRecorded, VERSION_MIGRATIONS_DDL, splitManifestMigrations, splitVersionMigrationsBatch, versionMigrationsOf, writeVersionMigrations, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, ImpersonationRefused, assertImpersonationWrites, assertModuleEnqueueableKind, assertSessionUsable, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, impersonationStampOf, mapImpersonationRow, newImpersonationSession, resolveRetryPolicy, executorOutcomeOf, isDeliveryRefusal, refusalJournalText, membershipFencesBackfillSql, MEMBERSHIP_FENCES_DDL, membershipFencesTableExists, MEMBERSHIP_FENCE_SINCE_SQL, RAISE_MEMBERSHIP_FENCE_SQL, memberAddedAudit, orgChangeBound, tenantCoverage, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexMigrations, listIndexPlans, listQuery, cursorOf, moduleMigrations, MIGRATION_DIGEST_FENCE_DDL, MIGRATION_DIGEST_MARK_LEGACY, assertJournalDumpCoherent, assertMigrationSql, migrationDivergence, migrationFailedError, migrationSteps, planMigrations, isSearchIndexTable, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, assertAttachmentExtractors, resolveAttachmentTextBounds, assertJobRegistrable, attachmentRecordOfRow, attachmentTextJob, enqueueAttachmentText, ATTACHMENT_TEXT_BACKFILL_JOB, ATTACHMENT_TEXT_JOB, kernelJobFor, queueAttachmentTextBackfill, startAttachmentTextBackfill, attachmentTextBackfillJob, reconcileAttachmentText, recordAttachmentText, searchAttachments, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, emptyImportResult, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, emittedSinceQuery, emittedReportOf, EMITTED_REPORT_CAP, exportedSinceQuery, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, importCursorSourceOf, exportBreaksOf, exportBreakRefusal, bindExportBreaksOf, bindExportBreakRefusal, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, JOB_RUN_DUE_AT, jobRunListLimit, jobRunOf, runDueJobRuns, startJobRun, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, globalFetch, createUlid, facetEvents, readHistory, walkEventCause, walkEventEffects, assertRedrainWindow, readInvocation, readDeadLetters, readLifecycleFlow, readOperationSeries, domainEventOf, readUndrainedOutbox, undrainedEventsOf, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, unknownRoleError, } from '@substrat-run/kernel';
6
6
  import { attributedView, delegatedRevokeSql, writeExplicitTupleIn, grantEntityShapeIn, shapeTopUpBatch, topUpEntityGrantShapes, settlePlatformRequestIn, } from '@substrat-run/kernel';
7
7
  import { ADMIN_LOG_INDEXES_SQL, SETTLE_INTENT_SQL, SETTLE_OUTCOME_SQL, settleOutcomeParamsOf, readAuditedOperations, unknownOutcomeOf, } from '@substrat-run/kernel';
8
8
  import { INERT_SCOPE_REASON, isPrimaryScopeRow } from '@substrat-run/kernel';
9
+ import { COPY_BACKFILL_SCOPE_SQL, COPY_BACKFILL_SQL, COPY_BACKFILL_SUPERSEDE_SQL, COPY_CLAIM_SQL, COPY_EXPIRED_SQL, copyBackfillParams, copyBackfillRefusal, COPY_MOVE_CONFIRM_SQL, COPY_MOVE_LIVE_PREDICATE, SCOPE_COPY_LEASE_MS, SCOPE_SCRIPT_COPY_COLUMNS, copyMoveConfirmParams, copyMoveLiveParams, scopeScriptCopyOf, } from '@substrat-run/kernel';
9
10
  import { GRANT_CHILDREN_INDEX_DDL, grantedEntitiesForContext } from '@substrat-run/kernel';
10
11
  import { LEGACY_SCOPE_ROWS_BACKFILL, assertSpineTablesBuilt, capabilitiesForLoad, dumpRowsInsert, isSpineTable, loadDirectoryDump, repointScopeGrants, settleCopiedWork, emittedHere, spineColumnAdditions } from '@substrat-run/kernel';
11
12
  import { ScopeActor } from './actor.js';
@@ -810,6 +811,10 @@ export class SqliteScopeHost {
810
811
  status TEXT NOT NULL DEFAULT 'active',
811
812
  schema_version TEXT NOT NULL DEFAULT '0',
812
813
  vertical_version_id TEXT,
814
+ erasure_epoch INTEGER,
815
+ -- #1722: set once a reap has started draining this scope's script copies, and never
816
+ -- cleared: only deleting the row ends it. While set, no copy move is recorded or bound.
817
+ reap_claimed_at TEXT,
813
818
  provisioned_version_id TEXT,
814
819
  -- Last FAILED migration attempt (§5.3). All null / 0 = healthy. Written on
815
820
  -- the failure path so a scope that fails closed stops rendering as active;
@@ -831,8 +836,29 @@ export class SqliteScopeHost {
831
836
  -- When the scope last entered the archived state (§4.4). Drives the reap sweep's
832
837
  -- age filter; null for scopes that never archived, cleared on unarchive.
833
838
  archived_at TEXT,
839
+ -- #1524: the status it was archived FROM. 'provisioning' = it never held data, so the
840
+ -- storage gauge neither reads nor counts it. Written with archived_at, cleared with it.
841
+ archived_from_status TEXT,
834
842
  created_at TEXT NOT NULL
835
843
  );
844
+ -- #1722: additive copy inventory; historical audit backfill is a separate act.
845
+ CREATE TABLE IF NOT EXISTS scope_script_copies (
846
+ tenant_id TEXT NOT NULL,
847
+ scope_id TEXT NOT NULL,
848
+ script_ref TEXT NOT NULL,
849
+ move_id TEXT NOT NULL,
850
+ state TEXT NOT NULL,
851
+ load_stamp TEXT,
852
+ revision TEXT,
853
+ last_attempt_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
854
+ -- 'source' | 'destination'. A pending entry belongs to its move until lease_until; a
855
+ -- sweep that claims it after that sets lease_owner (kernel scope-copy-ledger.ts).
856
+ role TEXT,
857
+ lease_until TEXT,
858
+ lease_owner TEXT,
859
+ PRIMARY KEY (tenant_id, scope_id, script_ref, move_id)
860
+ );
861
+ CREATE INDEX IF NOT EXISTS scope_script_copies_state ON scope_script_copies (state, scope_id);
836
862
  -- Private host state. Module SQL opens only the scope file, never this directory;
837
863
  -- neither table is included in directory exports or scope dumps (#2074).
838
864
  CREATE TABLE IF NOT EXISTS private_continuation_keys (
@@ -1358,6 +1384,8 @@ export class SqliteScopeHost {
1358
1384
  );
1359
1385
  CREATE INDEX IF NOT EXISTS _substrat_model_usage_tenant ON _substrat_model_usage (tenant_id, at);
1360
1386
  CREATE INDEX IF NOT EXISTS _substrat_model_usage_at ON _substrat_model_usage (at);
1387
+ -- #1524: the stored storage gauge — one row per (scope, UTC day), kernel-owned DDL.
1388
+ ${SCOPE_STORAGE_DDL}
1361
1389
  CREATE INDEX IF NOT EXISTS scopes_tenant ON scopes (tenant_id, scope_id);
1362
1390
  `);
1363
1391
  this.ensureDirectoryColumns();
@@ -2113,7 +2141,7 @@ export class SqliteScopeHost {
2113
2141
  const actingAs = (operation) => authority.kind === 'capability-session'
2114
2142
  ? {
2115
2143
  kind: 'capability',
2116
- id: resolveCapabilitySession(spineSql(rt.db), authority.hash, this.clock(), operation),
2144
+ id: resolveCapabilitySession(spineSql(rt.db), authority.hash, this.clock(), operation, true),
2117
2145
  }
2118
2146
  : authority;
2119
2147
  const node = { tenantId: rt.tenantId, scopeId: rt.scopeId };
@@ -2547,6 +2575,7 @@ export class SqliteScopeHost {
2547
2575
  forgetSwitchesOf(switchSqlOf(this.directory), scopeId);
2548
2576
  this.directory.prepare('DELETE FROM private_continuation_positions WHERE scope_id = ?').run(scopeId);
2549
2577
  this.directory.prepare('DELETE FROM private_continuation_keys WHERE scope_id = ?').run(scopeId);
2578
+ forgetScopeStorage(redactionSqlOf(this.directory), scopeId);
2550
2579
  this.directory.prepare('DELETE FROM scopes WHERE scope_id = ?').run(scopeId);
2551
2580
  })();
2552
2581
  this.recordAdmin(actor, 'deleteSnapshot', { tenantId, scopeId }, null, {
@@ -2688,14 +2717,14 @@ export class SqliteScopeHost {
2688
2717
  // Same lifecycle gates as the principal door, and reached the same way: a
2689
2718
  // suspended tenant refuses a support session exactly as it refuses a user.
2690
2719
  const scope = this.directory
2691
- .prepare('SELECT tenant_id, status FROM scopes WHERE scope_id = ?')
2720
+ .prepare('SELECT tenant_id FROM scopes WHERE scope_id = ?')
2692
2721
  .get(scopeId);
2693
2722
  if (!scope || scope.tenant_id !== tenantId) {
2694
2723
  throw substratError('not_found', `unknown scope for tenant: (${tenantId}, ${scopeId})`);
2695
2724
  }
2696
- if (scope.status !== 'active') {
2697
- throw substratError('conflict', `scope not active (status: ${scope.status}): ${scopeId}`, { reason: SCOPE_GATE_REASONS.notActive });
2698
- }
2725
+ const refusal = this.scopeWorkRefusal(tenantId, scopeId);
2726
+ if (refusal)
2727
+ throw substratError('conflict', refusal, { reason: SCOPE_GATE_REASONS.notActive });
2699
2728
  const rt = this.runtime(tenantId, scopeId);
2700
2729
  await this.applyPendingMigrations(rt);
2701
2730
  return this.buildStub(tenantId, scopeId, rt, asPrincipal(record.principal), options, record.id);
@@ -2717,14 +2746,14 @@ export class SqliteScopeHost {
2717
2746
  throw substratError('not_found', `module not registered on this host: ${moduleId}`);
2718
2747
  }
2719
2748
  const scope = this.directory
2720
- .prepare('SELECT tenant_id, status FROM scopes WHERE scope_id = ?')
2749
+ .prepare('SELECT tenant_id FROM scopes WHERE scope_id = ?')
2721
2750
  .get(scopeId);
2722
2751
  if (!scope || scope.tenant_id !== tenantId) {
2723
2752
  throw substratError('not_found', `unknown scope: ${scopeId}`);
2724
2753
  }
2725
- if (scope.status !== 'active') {
2726
- throw substratError('conflict', `scope not active (status: ${scope.status}): ${scopeId}`, { reason: SCOPE_GATE_REASONS.notActive });
2727
- }
2754
+ const refusal = this.scopeWorkRefusal(tenantId, scopeId);
2755
+ if (refusal)
2756
+ throw substratError('conflict', refusal, { reason: SCOPE_GATE_REASONS.notActive });
2728
2757
  const rt = this.runtime(tenantId, scopeId);
2729
2758
  await this.applyPendingMigrations(rt);
2730
2759
  return this.buildStub(tenantId, scopeId, rt, { kind: 'system', id: moduleId }, undefined, undefined, purge);
@@ -2989,6 +3018,20 @@ export class SqliteScopeHost {
2989
3018
  await this.applyPendingMigrations(rt);
2990
3019
  return rt;
2991
3020
  }
3021
+ /** The directory's current hold, read again when background work is about to run. */
3022
+ scopeWorkRefusal(tenantId, scopeId, allowProvisioning = false) {
3023
+ const row = this.directory
3024
+ .prepare(`SELECT s.status AS scope, t.status AS tenant
3025
+ FROM scopes s JOIN tenants t ON t.tenant_id = s.tenant_id
3026
+ WHERE s.scope_id = ? AND s.tenant_id = ?`)
3027
+ .get(scopeId, tenantId);
3028
+ if (!row)
3029
+ return `unknown scope for tenant: (${tenantId}, ${scopeId})`;
3030
+ // A connector can serve provisioning work; suspension still holds it.
3031
+ if (allowProvisioning && row.scope === 'provisioning' && row.tenant === 'active')
3032
+ return null;
3033
+ return lifecycleRefusal(row, { tenantId, scopeId });
3034
+ }
2992
3035
  /**
2993
3036
  * The exchange (#1672) — a secret traded for a session, or for the principal a `become`
2994
3037
  * capability yields. One serialized scope task and one transaction, so the use it takes
@@ -3011,6 +3054,7 @@ export class SqliteScopeHost {
3011
3054
  // pseudo-operation name — the actor is the capability, as on every event it
3012
3055
  // goes on to cause.
3013
3056
  emit: (capability, event) => kernelEmit(this.operationContext(rt, { kind: 'capability', id: capability }, undefined, undefined, undefined, CAPABILITY_EXCHANGE_OPERATION, [], now), event),
3057
+ holdings: this.holdingsAt(tenantId, scopeId),
3014
3058
  }, secret, options?.mode);
3015
3059
  rt.db.exec('COMMIT');
3016
3060
  }
@@ -3123,6 +3167,65 @@ export class SqliteScopeHost {
3123
3167
  const rt = await this.openActiveScope(tenantId, scopeId);
3124
3168
  return rt.actor.turn(() => revokeScopeRoles(switchSqlOf(rt.db), scopeId, principalId.parse(principal), this.clock(), this.roleBound(caller, tenantId, scopeId), (run) => rt.db.transaction(run)()));
3125
3169
  }
3170
+ /**
3171
+ * A principal's `become` mint (#1686) — a member invite's link. The kernel's `becomeMintCheck`
3172
+ * and the write in ONE scope turn and one transaction, so nothing the target or the caller
3173
+ * holds can move between the check and the mint; a refusal writes nothing. The mint is on the
3174
+ * spine as `capability.become-minted`, the caller its actor.
3175
+ */
3176
+ async mintBecomeCapabilityBounded(tenantId, scopeId, caller, input) {
3177
+ const rt = await this.openActiveScope(tenantId, scopeId);
3178
+ const minter = principalId.parse(caller);
3179
+ const parsed = principalBecomeCapabilityInput.parse(input);
3180
+ const outcome = await rt.actor.turn(async () => {
3181
+ const checked = await becomeMintCheck({ sql: spineSql(rt.db), checker: this.checker }, minter, parsed.principal, { tenantId, scopeId });
3182
+ if (!checked.ok)
3183
+ return checked;
3184
+ const now = this.clock();
3185
+ rt.db.exec('BEGIN IMMEDIATE');
3186
+ try {
3187
+ const minted = await mintBecomeCapabilityAsPrincipal({
3188
+ sql: spineSql(rt.db),
3189
+ now,
3190
+ emit: (event) => kernelEmit(this.operationContext(rt, asPrincipal(minter), undefined, undefined, undefined, CAPABILITY_BECOME_MINT_OPERATION, [], now), event),
3191
+ }, minter, parsed, checked.targetDigest);
3192
+ rt.db.exec('COMMIT');
3193
+ return { ok: true, minted };
3194
+ }
3195
+ catch (err) {
3196
+ rt.db.exec('ROLLBACK');
3197
+ throw err;
3198
+ }
3199
+ });
3200
+ if (outcome.ok) {
3201
+ await this.dispatch(rt, null);
3202
+ await this.dispatchExecutors(rt, null);
3203
+ }
3204
+ return outcome;
3205
+ }
3206
+ /**
3207
+ * Revoke a `become` capability a principal minted (#1686) — withdrawing a member invite's
3208
+ * link, recorded with `by` as the revoker. `revoked` is false for anything else: a platform-minted
3209
+ * or an `act` capability is not this verb's. The kernel bounds `by` in the same turn: the link's
3210
+ * minter, or someone holding everything its principal holds now.
3211
+ */
3212
+ async revokeBecomeCapability(tenantId, scopeId, capabilityId, by) {
3213
+ const rt = await this.openActiveScope(tenantId, scopeId);
3214
+ return rt.actor.turn(() => revokeBecomeCapabilityAsPrincipal({ sql: spineSql(rt.db), checker: this.checker, node: { tenantId, scopeId }, now: this.clock() }, capabilityId, principalId.parse(by)));
3215
+ }
3216
+ /**
3217
+ * Where each named `become` link stands (#1686) — what a pending-invite list shows, so a link the
3218
+ * kernel revoked or that expired is never shown as open. A read in the scope's task.
3219
+ */
3220
+ async becomeLinkStates(tenantId, scopeId, ids) {
3221
+ const rt = await this.openActiveScope(tenantId, scopeId);
3222
+ return rt.actor.enqueue(() => readBecomeLinkStates(spineSql(rt.db), ids, this.clock(), this.holdingsAt(tenantId, scopeId)));
3223
+ }
3224
+ /** What a principal holds at this scope, for a `become` exchange (#1686); absent with a checker that cannot say. */
3225
+ holdingsAt(tenantId, scopeId) {
3226
+ const checker = this.checker;
3227
+ return checker.holdings ? (principal) => checker.holdings(asPrincipal(principal), { tenantId, scopeId }) : undefined;
3228
+ }
3126
3229
  /** The caller's bound per role at the scope, `null` for a role this tenant does not define (#1150). */
3127
3230
  roleBound(caller, tenantId, scopeId) {
3128
3231
  const subject = asPrincipal(principalId.parse(caller));
@@ -3185,10 +3288,7 @@ export class SqliteScopeHost {
3185
3288
  const report = { checks: [] };
3186
3289
  if (!this.modules.has(moduleId))
3187
3290
  return report;
3188
- const scope = this.directory
3189
- .prepare('SELECT status FROM scopes WHERE scope_id = ? AND tenant_id = ?')
3190
- .get(scopeId, tenantId);
3191
- if (!scope || scope.status !== 'active')
3291
+ if (this.scopeWorkRefusal(tenantId, scopeId))
3192
3292
  return report;
3193
3293
  const rt = this.runtime(tenantId, scopeId);
3194
3294
  await this.applyPendingMigrations(rt);
@@ -3265,10 +3365,7 @@ export class SqliteScopeHost {
3265
3365
  // not this driver's problem to raise — it simply has nothing due (the sweep
3266
3366
  // already enumerated `active` scopes; this guards a race where one archived
3267
3367
  // between enumeration and here).
3268
- const scope = this.directory
3269
- .prepare('SELECT status FROM scopes WHERE scope_id = ? AND tenant_id = ?')
3270
- .get(scopeId, tenantId);
3271
- if (!scope || scope.status !== 'active')
3368
+ if (this.scopeWorkRefusal(tenantId, scopeId))
3272
3369
  return report;
3273
3370
  const rt = this.runtime(tenantId, scopeId);
3274
3371
  await this.applyPendingMigrations(rt);
@@ -3361,6 +3458,14 @@ export class SqliteScopeHost {
3361
3458
  report.fired += 1;
3362
3459
  }
3363
3460
  catch (err) {
3461
+ // The system door can meet a suspension after the cadence gate. A refused
3462
+ // fire did not run, so leave its row due for the first live pass.
3463
+ if (isSubstratError(err) && err.code === 'conflict' && err.extensions.reason === SCOPE_GATE_REASONS.notActive) {
3464
+ report.lifecycleHeld = true;
3465
+ report.skipped += 1;
3466
+ report.runs.push({ operation: schedule.operation, outcome: 'skipped' });
3467
+ continue;
3468
+ }
3364
3469
  status = 'failed';
3365
3470
  failure = { error: err };
3366
3471
  report.failed += 1;
@@ -3924,6 +4029,12 @@ export class SqliteScopeHost {
3924
4029
  return report;
3925
4030
  }
3926
4031
  async dispatchExecutorsPass(rt, invocationId, report, lines, outcomes) {
4032
+ // A request that started while live may commit after a suspension. Its outbox
4033
+ // stays due without an attempt, just as a scheduled retry does.
4034
+ if (this.scopeWorkRefusal(rt.tenantId, rt.scopeId)) {
4035
+ report.lifecycleHeld = true;
4036
+ return;
4037
+ }
3927
4038
  const now = new Date().toISOString();
3928
4039
  const scope = this.executorScope(rt);
3929
4040
  // #2005: a non-primary scope (a fork, a snapshot, a preview of either kind) causes no
@@ -3947,6 +4058,10 @@ export class SqliteScopeHost {
3947
4058
  ORDER BY o.id`)
3948
4059
  .all(deliveryId, executor.eventType, now);
3949
4060
  for (const row of rows) {
4061
+ if (this.scopeWorkRefusal(rt.tenantId, rt.scopeId)) {
4062
+ report.lifecycleHeld = true;
4063
+ return;
4064
+ }
3950
4065
  report.attempted += 1;
3951
4066
  // #1901: this attempt's line. Its number is the one the journal records, which
3952
4067
  // `recordExecutorDelivery` answers — the Durable Object's RPC does the same.
@@ -4070,6 +4185,9 @@ export class SqliteScopeHost {
4070
4185
  return isPrimaryScopeRow(this.directory.prepare('SELECT kind, forked_from FROM scopes WHERE scope_id = ?').get(scopeId));
4071
4186
  }
4072
4187
  async drainDue(tenantId, scopeId) {
4188
+ if (this.scopeWorkRefusal(tenantId, scopeId)) {
4189
+ return { attempted: 0, delivered: 0, retrying: 0, deadLettered: 0, lifecycleHeld: true };
4190
+ }
4073
4191
  const rt = this.runtime(tenantId, scopeId);
4074
4192
  await this.applyPendingMigrations(rt);
4075
4193
  // #1525: null, and honestly so — a sweep is not a call. An attempt this pass makes
@@ -4254,13 +4372,11 @@ export class SqliteScopeHost {
4254
4372
  this.attachmentExtractors, this.attachmentTextBounds);
4255
4373
  }
4256
4374
  async startJobRun(tenantId, scopeId, input) {
4257
- const rt = this.runtime(tenantId, scopeId);
4258
- await this.applyPendingMigrations(rt);
4375
+ const rt = await this.openActiveScope(tenantId, scopeId);
4259
4376
  return jobRunOf(await startJobRun(this.jobStore(rt), input, ulid, this.clock));
4260
4377
  }
4261
4378
  async runDueJobs(tenantId, scopeId, options) {
4262
- const rt = this.runtime(tenantId, scopeId);
4263
- await this.applyPendingMigrations(rt);
4379
+ const rt = await this.openActiveScope(tenantId, scopeId);
4264
4380
  // #1575: the kernel's extraction jobs are this host's own, bound to this scope — no
4265
4381
  // deployment registers them, and none can shadow them. The backfill starts here, on
4266
4382
  // the drive, so a scope's first request never pays for attachments that predate it.
@@ -4292,6 +4408,9 @@ export class SqliteScopeHost {
4292
4408
  // directory, so a node control plane can drain routed intents exactly as the
4293
4409
  // Cloudflare one does; the context build is the same one `dispatchExecutors` hands
4294
4410
  // an in-process connector.
4411
+ const refusal = this.scopeWorkRefusal(tenantId, scopeId, true);
4412
+ if (refusal)
4413
+ throw substratError('conflict', refusal, { reason: SCOPE_GATE_REASONS.notActive });
4295
4414
  const rt = this.runtime(tenantId, scopeId);
4296
4415
  // `false`: this path deliberately does NOT enqueue, so nothing is held and
4297
4416
  // the connection's reads take an ordinary serialized turn. Built on a view bound to
@@ -5075,6 +5194,7 @@ export class SqliteScopeHost {
5075
5194
  expiresAt: r.expires_at,
5076
5195
  ...(r.serving_ref ? { servingRef: r.serving_ref } : {}),
5077
5196
  archivedAt: r.archived_at ?? null,
5197
+ archivedFromStatus: (r.archived_from_status ?? null),
5078
5198
  createdAt: r.created_at,
5079
5199
  });
5080
5200
  // The (version, scope) pair a bind and its impact read both start from, and the refusals
@@ -5125,14 +5245,15 @@ export class SqliteScopeHost {
5125
5245
  // Stamp/clear archived_at so the reap sweep can age scopes (§4.4). Entering
5126
5246
  // `archived` records when; `unarchive` (→ active, a restore) clears it so a later
5127
5247
  // re-archive dates from the new event; `reaped` keeps it as terminal history.
5248
+ // #1524: the status it left is recorded beside the stamp and cleared with it.
5128
5249
  if (to === 'archived') {
5129
5250
  this.directory
5130
- .prepare('UPDATE scopes SET status = ?, archived_at = ? WHERE scope_id = ?')
5131
- .run(to, new Date().toISOString(), scopeId);
5251
+ .prepare('UPDATE scopes SET status = ?, archived_at = ?, archived_from_status = ? WHERE scope_id = ?')
5252
+ .run(to, new Date().toISOString(), row.status, scopeId);
5132
5253
  }
5133
5254
  else if (to === 'active') {
5134
5255
  this.directory
5135
- .prepare('UPDATE scopes SET status = ?, archived_at = NULL WHERE scope_id = ?')
5256
+ .prepare('UPDATE scopes SET status = ?, archived_at = NULL, archived_from_status = NULL WHERE scope_id = ?')
5136
5257
  .run(to, scopeId);
5137
5258
  }
5138
5259
  else {
@@ -5141,8 +5262,11 @@ export class SqliteScopeHost {
5141
5262
  if (to === 'archived' || to === 'reaped') {
5142
5263
  this.directory.prepare('UPDATE peer_bindings SET invalidated = 1 WHERE tenant_id = ? AND target_scope_id = ?').run(tenantId, scopeId);
5143
5264
  }
5144
- if (to === 'reaped')
5265
+ if (to === 'reaped') {
5145
5266
  forgetSwitchesOf(switchSqlOf(this.directory), scopeId);
5267
+ // #1524: its storage is gone, so its samples describe nothing.
5268
+ forgetScopeStorage(redactionSqlOf(this.directory), scopeId);
5269
+ }
5146
5270
  // The audit target carries the scope's vertical (control-plane.md §4.4:
5147
5271
  // "vertical stays null until §4.2 lifecycle actions that name one"). It is
5148
5272
  // read from the scope rather than passed in, so the trail cannot disagree
@@ -6449,6 +6573,11 @@ export class SqliteScopeHost {
6449
6573
  if (opts?.expectedVersionId !== undefined && scope.vertical_version_id !== opts.expectedVersionId) {
6450
6574
  throw substratError('precondition_failed', 'scope binding changed; reload the scope and retry');
6451
6575
  }
6576
+ const epoch = this.directory.prepare('SELECT COALESCE(erasure_epoch, 0) AS erasure_epoch FROM scopes WHERE tenant_id = ? AND scope_id = ?')
6577
+ .get(tenantId, scopeId);
6578
+ if (opts?.expectedErasureEpoch !== undefined && epoch?.erasure_epoch !== opts.expectedErasureEpoch) {
6579
+ throw substratError('precondition_failed', 'scope erasure changed; reload the scope and retry');
6580
+ }
6452
6581
  const ack = bindAcknowledgement.parse(opts?.acknowledge ?? {});
6453
6582
  // #1756: an export an app in this tenant imports, dropped or re-versioned by what this
6454
6583
  // scope would run. Before the snapshot, so a refused bind leaves nothing behind.
@@ -6466,12 +6595,7 @@ export class SqliteScopeHost {
6466
6595
  await this.snapshotScope(actor, tenantId, scopeId);
6467
6596
  }
6468
6597
  }
6469
- const update = opts?.expectedVersionId === undefined
6470
- ? this.directory.prepare('UPDATE scopes SET vertical_version_id = ?, vertical = ? WHERE scope_id = ?')
6471
- .run(versionId, v.verticalSlug, scopeId)
6472
- : this.directory.prepare('UPDATE scopes SET vertical_version_id = ?, vertical = ? WHERE scope_id = ? AND vertical_version_id IS ?')
6473
- .run(versionId, v.verticalSlug, scopeId, opts.expectedVersionId);
6474
- if (update.changes === 0) {
6598
+ if (!this.moveScopeRow('vertical_version_id = ?, vertical = ?', [versionId, v.verticalSlug], tenantId, scopeId, opts)) {
6475
6599
  throw substratError('precondition_failed', 'scope binding changed; reload the scope and retry');
6476
6600
  }
6477
6601
  this.recordAdmin(actor, 'bindScopeVersion', { tenantId, scopeId }, null, {
@@ -6482,6 +6606,106 @@ export class SqliteScopeHost {
6482
6606
  ...(ack.exportBreak ? { acknowledged: ack } : {}),
6483
6607
  });
6484
6608
  },
6609
+ recordScopeScriptCopy: async (_actor, tenantId, scopeId, scriptRef, moveId, opts) => {
6610
+ if (!scriptRef || !moveId)
6611
+ throw substratError('conflict', 'copy script and move must name a real script');
6612
+ const result = this.directory.prepare(`INSERT INTO scope_script_copies (tenant_id, scope_id, script_ref, move_id, state, role, load_stamp, lease_until)
6613
+ SELECT tenant_id, scope_id, ?, ?, 'pending', ?, ?, ? FROM scopes
6614
+ WHERE tenant_id = ? AND scope_id = ? AND reap_claimed_at IS NULL
6615
+ ON CONFLICT (tenant_id, scope_id, script_ref, move_id) DO NOTHING`).run(scriptRef, moveId, opts?.role ?? null, opts?.loadStamp ?? null, new Date(Date.now() + (opts?.leaseMs ?? SCOPE_COPY_LEASE_MS)).toISOString(), tenantId, scopeId);
6616
+ if (result.changes > 0)
6617
+ return;
6618
+ const scope = this.directory.prepare('SELECT reap_claimed_at FROM scopes WHERE tenant_id = ? AND scope_id = ?')
6619
+ .get(tenantId, scopeId);
6620
+ if (!scope)
6621
+ throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
6622
+ if (scope.reap_claimed_at !== null)
6623
+ throw substratError('precondition_failed', `scope ${scopeId} is being reaped`);
6624
+ },
6625
+ backfillScopeScriptCopy: async (actor, tenantId, scopeId, scriptRef, opts) => {
6626
+ if (!scriptRef)
6627
+ throw substratError('conflict', 'a backfilled copy must name a real script');
6628
+ // The audit row commits with the entry, stamped with the erasure epoch read in the same unit.
6629
+ return this.directory.transaction(() => {
6630
+ const written = this.directory.prepare(COPY_BACKFILL_SQL).run(...copyBackfillParams(tenantId, scopeId, scriptRef)).changes > 0;
6631
+ const scope = this.directory.prepare(COPY_BACKFILL_SCOPE_SQL).get(tenantId, scopeId);
6632
+ if (!written)
6633
+ return copyBackfillRefusal(scope);
6634
+ this.recordAdmin(actor, 'backfillScopeCopy', { tenantId, scopeId }, { erasureEpoch: scope.erasure_epoch }, { scriptRef, fromLogId: opts?.fromLogId ?? null });
6635
+ return 'recorded';
6636
+ })();
6637
+ },
6638
+ claimExpiredScopeScriptCopies: async (_actor, input) => {
6639
+ const limit = assertRowLimit('limit', input.limit);
6640
+ return this.directory.transaction(() => {
6641
+ const due = this.directory.prepare(COPY_EXPIRED_SQL).all(input.now, limit);
6642
+ const claimed = [];
6643
+ for (const r of due) {
6644
+ if (this.directory.prepare(COPY_CLAIM_SQL).run(input.owner, input.leaseUntil, r.tenant_id, r.scope_id, r.script_ref, r.move_id, input.now).changes === 0)
6645
+ continue;
6646
+ claimed.push(this.directory.prepare(`SELECT ${SCOPE_SCRIPT_COPY_COLUMNS} FROM scope_script_copies
6647
+ WHERE tenant_id = ? AND scope_id = ? AND script_ref = ? AND move_id = ?`).get(r.tenant_id, r.scope_id, r.script_ref, r.move_id));
6648
+ }
6649
+ return claimed.map(scopeScriptCopyOf);
6650
+ })();
6651
+ },
6652
+ beginScopeScriptReap: async (_actor, tenantId, scopeId) => {
6653
+ const result = this.directory.prepare(`UPDATE scopes SET reap_claimed_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
6654
+ WHERE tenant_id = ? AND scope_id = ? AND reap_claimed_at IS NULL
6655
+ AND NOT EXISTS (SELECT 1 FROM scope_script_copies AS move
6656
+ WHERE move.tenant_id = scopes.tenant_id AND move.scope_id = scopes.scope_id
6657
+ AND move.state = 'pending')`).run(tenantId, scopeId);
6658
+ if (result.changes > 0)
6659
+ return;
6660
+ const scope = this.directory.prepare('SELECT reap_claimed_at FROM scopes WHERE tenant_id = ? AND scope_id = ?')
6661
+ .get(tenantId, scopeId);
6662
+ if (!scope)
6663
+ throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
6664
+ if (scope.reap_claimed_at !== null)
6665
+ return; // a reaper resumes its earlier claim
6666
+ throw substratError('precondition_failed', `scope ${scopeId} has a copy move in flight; retry reap after it settles`);
6667
+ },
6668
+ settleScopeScriptCopy: async (_actor, tenantId, scopeId, scriptRef, moveId, state, marker, opts) => this.directory.transaction(() => {
6669
+ const settled = this.directory.prepare(`UPDATE scope_script_copies SET state = ?, load_stamp = ?, revision = ?,
6670
+ last_attempt_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
6671
+ WHERE tenant_id = ? AND scope_id = ? AND script_ref = ? AND move_id = ?
6672
+ AND (state <> 'done' OR ? = 'done')
6673
+ ${opts?.claimedBy === undefined ? '' : "AND state = 'pending' AND lease_owner = ?"}`).run(state, marker?.loadStamp ?? null, marker?.revision ?? null, tenantId, scopeId, scriptRef, moveId, state, ...(opts?.claimedBy === undefined ? [] : [opts.claimedBy])).changes > 0;
6674
+ if (settled && state === 'done')
6675
+ this.directory.prepare(COPY_BACKFILL_SUPERSEDE_SQL).run(tenantId, scopeId);
6676
+ return settled;
6677
+ })(),
6678
+ touchScopeScriptCopy: async (_actor, tenantId, scopeId, scriptRef, moveId) => {
6679
+ this.directory.prepare(`UPDATE scope_script_copies SET last_attempt_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
6680
+ WHERE tenant_id = ? AND scope_id = ? AND script_ref = ? AND move_id = ? AND state = 'eligible'`).run(tenantId, scopeId, scriptRef, moveId);
6681
+ },
6682
+ listScopeScriptCopies: async (_actor, filter) => {
6683
+ const where = [];
6684
+ const args = [];
6685
+ if (filter.tenantId) {
6686
+ where.push('tenant_id = ?');
6687
+ args.push(filter.tenantId);
6688
+ }
6689
+ if (filter.scopeId) {
6690
+ where.push('scope_id = ?');
6691
+ args.push(filter.scopeId);
6692
+ }
6693
+ if (filter.state) {
6694
+ where.push('state = ?');
6695
+ args.push(filter.state);
6696
+ }
6697
+ if (filter.after) {
6698
+ if (!filter.tenantId || !filter.scopeId)
6699
+ throw substratError('validation_failed', 'copy cursor requires tenant and scope');
6700
+ where.push('(script_ref > ? OR (script_ref = ? AND move_id > ?))');
6701
+ args.push(filter.after.scriptRef, filter.after.scriptRef, filter.after.moveId);
6702
+ }
6703
+ const limit = assertRowLimit('limit', filter.limit ?? 100);
6704
+ const order = filter.scopeId ? 'script_ref, move_id' : 'last_attempt_at, tenant_id, scope_id, script_ref';
6705
+ const rows = this.directory.prepare(`SELECT ${SCOPE_SCRIPT_COPY_COLUMNS} FROM scope_script_copies ${where.length ? `WHERE ${where.join(' AND ')}` : ''}
6706
+ ORDER BY ${order} LIMIT ?`).all(...args, limit);
6707
+ return rows.map(scopeScriptCopyOf);
6708
+ },
6485
6709
  /**
6486
6710
  * Record that this scope's provision has now run against `versionId` (#1172).
6487
6711
  * Written only after a provision or reconcile succeeded — a receipt, never a hope.
@@ -6559,9 +6783,9 @@ export class SqliteScopeHost {
6559
6783
  if (breaks.length > 0)
6560
6784
  throw substratError('precondition_failed', bindExportBreakRefusal(breaks));
6561
6785
  }
6562
- this.directory
6563
- .prepare('UPDATE scopes SET serving_ref = ? WHERE scope_id = ?')
6564
- .run(servingRef, scopeId);
6786
+ if (!this.moveScopeRow('serving_ref = ?', [servingRef], tenantId, scopeId, opts)) {
6787
+ throw substratError('precondition_failed', 'scope erasure or reap changed; reload and retry');
6788
+ }
6565
6789
  this.recordAdmin(actor, 'setScopeServingRef', { tenantId, scopeId }, { servingRef: scope.serving_ref }, { servingRef, ...(ack.exportBreak ? { acknowledged: ack } : {}) });
6566
6790
  },
6567
6791
  recordKeptCopyResolution: async (actor, tenantId, scopeId, r) => {
@@ -7428,8 +7652,15 @@ export class SqliteScopeHost {
7428
7652
  this.recordAccess(actor, 'openSubjectPayloads', { tenantId, scopeId }, { subjects: new Set(items.map((i) => i.subjectId)).size }, opened.filter((o) => o !== null).length);
7429
7653
  return opened;
7430
7654
  },
7431
- shredSubject: async (actor, tenantId, scopeId, subjectId) => {
7655
+ // `stamp` is the erasure epoch an orchestrated erasure's claim compared (#1722). A direct shred
7656
+ // claims nothing and records the epoch it ran under: the backfill orders erasures by them.
7657
+ shredSubject: async (actor, tenantId, scopeId, subjectId, stamp) => {
7432
7658
  this.assertScope(tenantId, scopeId);
7659
+ const before = stamp ?? {
7660
+ erasureEpoch: this.directory.prepare(COPY_BACKFILL_SCOPE_SQL).get(tenantId, scopeId)
7661
+ ?.erasure_epoch ?? 0,
7662
+ path: 'direct',
7663
+ };
7433
7664
  // Redact the live spine FIRST. Both halves are idempotent and a crash between them
7434
7665
  // converges on retry, so the order is decided by which half-done state harms the
7435
7666
  // person: dying after this leaves ciphertext in a backup that no key opens; dying
@@ -7511,12 +7742,38 @@ export class SqliteScopeHost {
7511
7742
  // BOTH logs, which is unusual and deliberate: the admin log because this is a
7512
7743
  // mutation, the access log because it destroys evidence. An erasure is the one
7513
7744
  // action where "who asked for this to disappear" is itself the record.
7514
- this.recordAdmin(actor, 'shredSubject', { tenantId, scopeId }, null, receipt);
7745
+ this.recordAdmin(actor, 'shredSubject', { tenantId, scopeId }, before, receipt);
7515
7746
  // BOTH counts: the access log's number is "how much evidence this destroyed", and
7516
7747
  // an intent payload is a whole event's worth of it.
7517
7748
  this.recordAccess(actor, 'shredSubject', { tenantId, scopeId }, { subjectId }, redacted.changes + intentsRedacted + jobRunsRedacted + idempotencyResults + moduleRowsErased(vertical));
7518
7749
  return receipt;
7519
7750
  },
7751
+ scopeErasureEpoch: async (_actor, tenantId, scopeId) => {
7752
+ // NULL on every row from before the column (#1722): the same 0 every CAS compares against.
7753
+ const row = this.directory.prepare('SELECT COALESCE(erasure_epoch, 0) AS erasure_epoch FROM scopes WHERE tenant_id = ? AND scope_id = ?')
7754
+ .get(tenantId, scopeId);
7755
+ if (!row)
7756
+ throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
7757
+ return row.erasure_epoch;
7758
+ },
7759
+ finalizeSubjectShred: async (actor, tenantId, scopeId, subjectId, redactions, expected) => {
7760
+ if (redactions.length === 0)
7761
+ throw substratError('conflict', 'subject erasure has no confirmed scope redaction');
7762
+ const claimed = this.directory.prepare(`UPDATE scopes SET erasure_epoch = COALESCE(erasure_epoch, 0) + 1
7763
+ WHERE tenant_id = ? AND scope_id = ? AND vertical_version_id IS ?
7764
+ AND serving_ref IS ? AND COALESCE(erasure_epoch, 0) = ?
7765
+ AND NOT EXISTS (SELECT 1 FROM scope_script_copies AS copy
7766
+ WHERE copy.tenant_id = scopes.tenant_id AND copy.scope_id = scopes.scope_id
7767
+ AND copy.state = 'pending')
7768
+ AND (SELECT COUNT(*) FROM scope_script_copies AS copy
7769
+ WHERE copy.tenant_id = scopes.tenant_id AND copy.scope_id = scopes.scope_id) = ?`).run(tenantId, scopeId, expected.versionId, expected.servingRef, expected.epoch, expected.copyCount);
7770
+ if (claimed.changes === 0)
7771
+ throw substratError('precondition_failed', 'scope route or copy inventory changed during subject erasure; retry after it settles');
7772
+ // The pure adapter has one co-located scope store. Its existing atomic shred is
7773
+ // the final local redaction and key destruction, after remote copies confirmed.
7774
+ const shred = this.admin.shredSubject;
7775
+ return shred(actor, tenantId, scopeId, subjectId, { erasureEpoch: expected.epoch, path: 'orchestrated' });
7776
+ },
7520
7777
  // -- impersonation (K-42, #868) ----------------------------------------
7521
7778
  beginImpersonation: async (actor, input) => {
7522
7779
  // Fail closed on the scope before minting anything, exactly as the
@@ -7589,6 +7846,8 @@ export class SqliteScopeHost {
7589
7846
  '_substrat_switch_owed', // #2045: subjects owed a re-assert, per scope
7590
7847
  '_substrat_findings', // #1748: the tenant's findings
7591
7848
  '_substrat_finding_rules', // #1748: the tenant's suppress rules
7849
+ '_substrat_scope_storage', // #1524: storage samples of scopes whose storage is gone
7850
+ '_substrat_scope_storage_attempts', // #1524: and the phase's last try at each
7592
7851
  ];
7593
7852
  const clear = this.directory.transaction(() => {
7594
7853
  for (const table of tables) {
@@ -7681,7 +7940,11 @@ export class SqliteScopeHost {
7681
7940
  const reading = foldMeterReading({
7682
7941
  readAt: instant.parse(new Date().toISOString()),
7683
7942
  tenants: this.directory.prepare(`SELECT tenant_id, slug, status FROM tenants${where}`).all(...args).map((r) => ({ tenantId: r.tenant_id, slug: r.slug, status: r.status })),
7684
- scopes: this.directory.prepare(`SELECT tenant_id, status FROM scopes${where}`).all(...args).map((r) => ({ tenantId: r.tenant_id, status: r.status })),
7943
+ scopes: this.directory.prepare(`SELECT tenant_id, status, archived_from_status FROM scopes${where}`).all(...args).map((r) => ({
7944
+ tenantId: r.tenant_id,
7945
+ status: r.status,
7946
+ archivedFromStatus: r.archived_from_status,
7947
+ })),
7685
7948
  entitlements: this.directory
7686
7949
  .prepare(`SELECT tenant_id, entitlement_key, plan, expires_at FROM _substrat_entitlements${where}`)
7687
7950
  .all(...args).map((r) => ({
@@ -7690,6 +7953,9 @@ export class SqliteScopeHost {
7690
7953
  plan: r.plan,
7691
7954
  expiresAt: r.expires_at,
7692
7955
  })),
7956
+ // #1524: the stored gauge's latest sample per non-reaped scope. A directory read only.
7957
+ storage: listScopeStorageRows(redactionSqlOf(this.directory), { tenantId: only, latest: true }),
7958
+ storageAttempts: listScopeStorageAttemptRows(redactionSqlOf(this.directory), only),
7693
7959
  });
7694
7960
  // The count that matters for K-24 is how many TENANTS this reading covered —
7695
7961
  // "read the meter for one tenant" and "metered the whole fleet" are different
@@ -8554,6 +8820,20 @@ export class SqliteScopeHost {
8554
8820
  this.directory.prepare('DELETE FROM _substrat_model_usage WHERE at < ?').run(horizon);
8555
8821
  return { recorded: res.changes > 0 };
8556
8822
  },
8823
+ recordScopeStorage: async (_actor, readings) => ({
8824
+ recorded: this.directory.transaction(() => recordScopeStorageRows(redactionSqlOf(this.directory), readings))(),
8825
+ }),
8826
+ listScopeStorage: async (actor, filter) => {
8827
+ const rows = listScopeStorageRows(redactionSqlOf(this.directory), filter);
8828
+ this.recordAccess(actor, 'listScopeStorage', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter ?? null, rows.length);
8829
+ return rows;
8830
+ },
8831
+ listScopeStorageAttempts: async (actor) => {
8832
+ const rows = listScopeStorageAttemptRows(redactionSqlOf(this.directory));
8833
+ this.recordAccess(actor, 'listScopeStorageAttempts', { tenantId: null }, null, rows.length);
8834
+ return rows;
8835
+ },
8836
+ pruneScopeStorage: async (_actor, limit) => pruneScopeStorageRows(redactionSqlOf(this.directory), Date.now(), limit),
8557
8837
  listModelUsage: async (actor, filter) => {
8558
8838
  const rows = this.selectModelUsage(filter);
8559
8839
  this.recordAccess(actor, 'listModelUsage', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter, rows.length);
@@ -8581,6 +8861,28 @@ export class SqliteScopeHost {
8581
8861
  * directory. `PRAGMA table_info` is available in the pure adapter (the DO adapter
8582
8862
  * has to attempt-and-tolerate instead — see its `ensureDirectoryColumns`).
8583
8863
  */
8864
+ /**
8865
+ * Move a scope's binding or route (#1722), conditionally: on the binding the caller read, on
8866
+ * the erasure epoch its carry read (a NULL epoch, from before the column, is 0) with no reap
8867
+ * claimed, and on its copy move still owning a live lease. The move's confirmation commits in
8868
+ * the same transaction as the row. False when any condition failed and nothing was written.
8869
+ */
8870
+ moveScopeRow(set, setArgs, tenantId, scopeId, cond) {
8871
+ const now = new Date().toISOString();
8872
+ return this.directory.transaction(() => {
8873
+ const moved = this.directory.prepare(`UPDATE scopes SET ${set} WHERE scope_id = ?
8874
+ ${cond?.expectedVersionId === undefined ? '' : 'AND vertical_version_id IS ?'}
8875
+ ${cond?.expectedErasureEpoch === undefined ? '' : 'AND COALESCE(erasure_epoch, 0) = ? AND reap_claimed_at IS NULL'}
8876
+ ${cond?.confirmMove === undefined ? '' : COPY_MOVE_LIVE_PREDICATE}`).run(...setArgs, scopeId, ...(cond?.expectedVersionId === undefined ? [] : [cond.expectedVersionId]), ...(cond?.expectedErasureEpoch === undefined ? [] : [cond.expectedErasureEpoch]), ...(cond?.confirmMove === undefined ? [] : copyMoveLiveParams(cond.confirmMove, now)));
8877
+ if (moved.changes === 0)
8878
+ return false;
8879
+ if (cond?.confirmMove) {
8880
+ this.directory.prepare(COPY_MOVE_CONFIRM_SQL).run(...copyMoveConfirmParams(cond.confirmMove, tenantId, scopeId, now));
8881
+ this.directory.prepare(COPY_BACKFILL_SUPERSEDE_SQL).run(tenantId, scopeId);
8882
+ }
8883
+ return true;
8884
+ })();
8885
+ }
8584
8886
  ensureColumn(db, table, column, ddl) {
8585
8887
  // Without case, as SQLite resolves a column name: a restore may have added this column
8586
8888
  // already, untyped and spelled as a newer kernel's dump spelled it (#1883).
@@ -8767,6 +9069,8 @@ export class SqliteScopeHost {
8767
9069
  // #1172: which version this scope's provision hook last ran against. Null on every
8768
9070
  // existing row, which reads as "unknown, reconcile once" rather than "up to date".
8769
9071
  this.ensureColumn(this.directory, 'scopes', 'provisioned_version_id', 'provisioned_version_id TEXT');
9072
+ this.ensureColumn(this.directory, 'scopes', 'erasure_epoch', 'erasure_epoch INTEGER');
9073
+ this.ensureColumn(this.directory, 'scopes', 'reap_claimed_at', 'reap_claimed_at TEXT');
8770
9074
  // §4.8's grace-window timestamp on tenants (mirrors scopes' archived_at).
8771
9075
  this.ensureColumn(this.directory, 'tenants', 'deleting_at', 'deleting_at TEXT');
8772
9076
  this.ensureColumn(this.directory, 'tenants', 'provisioned_by_tenant', 'provisioned_by_tenant TEXT REFERENCES tenants(tenant_id)');
@@ -8853,6 +9157,7 @@ export class SqliteScopeHost {
8853
9157
  ['expires_at', 'expires_at TEXT'],
8854
9158
  ['serving_ref', 'serving_ref TEXT'],
8855
9159
  ['archived_at', 'archived_at TEXT'],
9160
+ ['archived_from_status', 'archived_from_status TEXT'],
8856
9161
  ]) {
8857
9162
  if (!existing.has(column))
8858
9163
  this.directory.exec(`ALTER TABLE scopes ADD COLUMN ${ddl}`);
@@ -9617,6 +9922,11 @@ export class SqliteScopeHost {
9617
9922
  // KERNEL_DDL is all IF NOT EXISTS, so a scope DB created before K-21 keeps the
9618
9923
  // old shape — ALTER the tombstone in.
9619
9924
  this.ensureColumn(db, '_substrat_tuples', 'revoked_at', 'revoked_at TEXT');
9925
+ // #2126: NULL keeps every existing capability's attachment behavior unchanged.
9926
+ this.ensureColumn(db, '_substrat_capabilities', 'attachments', 'attachments TEXT');
9927
+ // #1686: NULL on every row already there — none is a principal-minted become.
9928
+ this.ensureColumn(db, '_substrat_capabilities', 'target_digest', 'target_digest TEXT');
9929
+ this.ensureColumn(db, '_substrat_capabilities', 'revoked_reason', 'revoked_reason TEXT');
9620
9930
  db.exec(GRANT_CHILDREN_INDEX_DDL);
9621
9931
  // Executor retry state (#100), same reasoning: scopes provisioned before it
9622
9932
  // already have the table. Defaults read as "terminal", which is exactly right