@feltdb/core 0.8.4 → 0.8.6

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.
Files changed (160) hide show
  1. package/dist/create/package-versions.js +1 -1
  2. package/dist/create/server-source/Cargo.lock +165 -0
  3. package/dist/create/server-source/Cargo.toml +9 -0
  4. package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
  5. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
  6. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
  7. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
  8. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
  9. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
  10. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
  11. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
  12. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
  13. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
  14. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
  15. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
  19. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
  20. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
  21. package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
  22. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
  23. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
  24. package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
  25. package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
  26. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
  27. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
  28. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
  29. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
  30. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
  31. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
  32. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
  33. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
  34. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
  35. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
  36. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
  37. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
  38. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
  39. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
  40. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
  41. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
  42. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
  43. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
  44. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
  45. package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
  46. package/dist/create/server-source/crates/feltdb/src/lib.rs +1648 -46
  47. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
  48. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
  49. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
  50. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
  51. package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
  52. package/dist/create/server-source/crates/feltdb/src/operation.rs +39 -0
  53. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
  54. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
  55. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
  56. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
  57. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
  58. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
  59. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
  60. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
  61. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
  62. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
  63. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +79 -70
  64. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
  65. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
  66. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
  67. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
  68. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
  69. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
  70. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
  71. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
  72. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
  73. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
  74. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
  75. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
  76. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
  77. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
  78. package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
  79. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
  80. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
  81. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
  82. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
  83. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
  84. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
  85. package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
  86. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
  87. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +13 -4
  88. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
  89. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
  90. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
  91. package/dist/create/server-source/crates/feltdb/src/state_model.rs +1565 -536
  92. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
  93. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
  94. package/dist/create/server-source/crates/feltdb/src/storage.rs +9 -3
  95. package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
  96. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
  97. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
  98. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
  99. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
  100. package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
  101. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
  102. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
  103. package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
  104. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +3 -4
  105. package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +132 -0
  106. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
  107. package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +272 -0
  108. package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +467 -0
  109. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +309 -0
  110. package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +445 -0
  111. package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +518 -0
  112. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
  113. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
  114. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
  115. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
  116. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
  117. package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +278 -0
  118. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +2 -1
  119. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +80 -25
  120. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
  121. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
  122. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
  123. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
  124. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
  125. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
  126. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
  127. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
  128. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
  129. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1365 -0
  130. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
  131. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
  132. package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +417 -0
  133. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
  134. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
  135. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
  136. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
  137. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
  138. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
  139. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
  140. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
  141. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
  142. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
  143. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
  144. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
  145. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
  146. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
  147. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +442 -0
  148. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +16 -1
  149. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
  150. package/dist/create/server-source/crates/feltdb-server/src/main.rs +695 -47
  151. package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +21 -0
  152. package/dist/studio-app/assets/{feltdb_wasm-CVQWgXO-.js → feltdb_wasm-C1VhI-U5.js} +1 -1
  153. package/dist/studio-app/assets/feltdb_wasm_bg-C8HXbAXb.wasm +0 -0
  154. package/dist/studio-app/assets/{index-DwgNAIIX.js → index-Bbos1m2U.js} +1 -1
  155. package/dist/studio-app/index.html +1 -1
  156. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  157. package/dist/workload.d.ts +2 -1
  158. package/dist/workload.d.ts.map +1 -1
  159. package/package.json +1 -1
  160. package/dist/studio-app/assets/feltdb_wasm_bg-CNVpvaZV.wasm +0 -0
@@ -62,7 +62,7 @@ use feltdb::{
62
62
  },
63
63
  workload::{CreateWorkload, WorkloadStore},
64
64
  AtomicMutation, DatabaseSnapshot, FeltDb, FlowError, JsonCasResult, Operation,
65
- PeerAdvertisement, PeerId, RecordPrecondition, StoredRow,
65
+ PeerAdvertisement, PeerId, RecordPrecondition, StateTriggerStore, StoredRow,
66
66
  };
67
67
  use feltdb_server::{
68
68
  app_state::{AppState, BoundedQueryCursor},
@@ -1421,9 +1421,10 @@ struct WorkerReconcileRequest {
1421
1421
  fn workload_error(error: feltdb::workload::WorkloadError) -> ApiError {
1422
1422
  let status = match error.code.as_str() {
1423
1423
  "WORKLOAD_NOT_FOUND" => StatusCode::NOT_FOUND,
1424
- "WORKLOAD_STALE_FENCE" | "WORKLOAD_TRANSITION_DENIED" | "WORKLOAD_NOT_READY" => {
1425
- StatusCode::CONFLICT
1426
- }
1424
+ "WORKLOAD_STALE_FENCE"
1425
+ | "WORKLOAD_TRANSITION_DENIED"
1426
+ | "WORKLOAD_NOT_READY"
1427
+ | "WORKLOAD_NOT_YET_ELIGIBLE" => StatusCode::CONFLICT,
1427
1428
  "WORKLOAD_UNAUTHORIZED" | "WORKLOAD_CAPABILITY_DENIED" => StatusCode::FORBIDDEN,
1428
1429
  "WORKLOAD_STORAGE_FAILURE" => StatusCode::SERVICE_UNAVAILABLE,
1429
1430
  _ => StatusCode::UNPROCESSABLE_ENTITY,
@@ -6882,7 +6883,8 @@ async fn application_openapi() -> Json<Value> {
6882
6883
  },
6883
6884
  "security": [{ "bearer": [] }],
6884
6885
  "paths": {
6885
- "/health": { "get": { "security": [], "responses": { "200": { "description": "Runtime health" } } } },
6886
+ "/health": { "get": { "security": [], "responses": { "200": { "description": "Process liveness" } } } },
6887
+ "/health/ready": { "get": { "security": [], "responses": { "200": { "description": "Storage and scoped-operation readiness" }, "503": { "description": "Not ready" } } } },
6886
6888
  "/application": { "get": { "responses": { "200": { "description": "Active application contract identity" } } } },
6887
6889
  "/schema": { "get": { "responses": { "200": { "description": "Active application schema" } } } },
6888
6890
  "/query": { "post": { "responses": { "200": { "description": "Bounded authorized query" } } } },
@@ -8090,6 +8092,8 @@ struct Config {
8090
8092
  peers: Vec<String>,
8091
8093
  peer_token_env: String,
8092
8094
  sync_interval: Duration,
8095
+ max_in_flight: usize,
8096
+ request_deadline: Duration,
8093
8097
  audit: PathBuf,
8094
8098
  allowed_origins: Vec<axum::http::HeaderValue>,
8095
8099
  worker_enabled: bool,
@@ -8118,6 +8122,11 @@ impl Config {
8118
8122
  let mut peers = Vec::new();
8119
8123
  let mut peer_token_env = "FELTDB_PEER_TOKEN".to_string();
8120
8124
  let mut sync_interval = Duration::from_secs(2);
8125
+ // Concurrency past this point does not buy throughput: the database
8126
+ // serializes every read and write on one lock. It only decides whether
8127
+ // an overload is shed quickly or queued invisibly.
8128
+ let mut max_in_flight = 64usize;
8129
+ let mut request_deadline = Duration::from_secs(10);
8121
8130
  let mut audit = None;
8122
8131
  let mut allowed_origins = std::env::var("FELTDB_ALLOWED_ORIGINS")
8123
8132
  .ok()
@@ -8164,6 +8173,23 @@ impl Config {
8164
8173
  }
8165
8174
  sync_interval = Duration::from_millis(milliseconds);
8166
8175
  }
8176
+ "--max-in-flight" => {
8177
+ max_in_flight = value()?
8178
+ .parse()
8179
+ .map_err(|_| "invalid --max-in-flight".to_string())?;
8180
+ if max_in_flight == 0 {
8181
+ return Err("--max-in-flight must be at least 1".to_string());
8182
+ }
8183
+ }
8184
+ "--request-deadline-ms" => {
8185
+ let milliseconds: u64 = value()?
8186
+ .parse()
8187
+ .map_err(|_| "invalid --request-deadline-ms".to_string())?;
8188
+ if milliseconds < 100 {
8189
+ return Err("--request-deadline-ms must be at least 100".to_string());
8190
+ }
8191
+ request_deadline = Duration::from_millis(milliseconds);
8192
+ }
8167
8193
  "--audit" => audit = Some(PathBuf::from(value()?)),
8168
8194
  "--allow-origin" => allowed_origins.push(
8169
8195
  value()?
@@ -8203,6 +8229,8 @@ impl Config {
8203
8229
  peers,
8204
8230
  peer_token_env,
8205
8231
  sync_interval,
8232
+ max_in_flight,
8233
+ request_deadline,
8206
8234
  audit,
8207
8235
  allowed_origins,
8208
8236
  worker_enabled,
@@ -8289,6 +8317,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
8289
8317
  if manage_workloads()? {
8290
8318
  return Ok(());
8291
8319
  }
8320
+ if manage_state_triggers()? {
8321
+ return Ok(());
8322
+ }
8292
8323
  if manage_backup()? {
8293
8324
  return Ok(());
8294
8325
  }
@@ -8336,6 +8367,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
8336
8367
  let grant_store = GrantStore::load(config.data.with_extension("grants.json"))?;
8337
8368
  let sync_store = SyncStore::load(config.data.with_extension("sync.json"))?;
8338
8369
  let workload_store = WorkloadStore::load(config.data.with_extension("workloads.json"))?;
8370
+ let state_trigger_store = StateTriggerStore::load(config.data.with_extension("triggers.json"))?;
8339
8371
  let mesh_store = WorkerMeshStore::load(config.data.with_extension("workers.json"))?;
8340
8372
  let content_store = ContentStore::new(config.data.with_extension("content"))?;
8341
8373
  let artifact_store = ArtifactStore::load(
@@ -8352,10 +8384,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
8352
8384
  let audit_handle = AuditLog::new(config.audit.clone());
8353
8385
  let state = AppState {
8354
8386
  started_at: Instant::now(),
8387
+ admission: Arc::new(tokio::sync::Semaphore::new(config.max_in_flight)),
8388
+ request_deadline: config.request_deadline,
8355
8389
  ids: Arc::new(AtomicU64::new(db.sequence()?)),
8356
8390
  db,
8357
8391
  namespace: Arc::from(config.namespace.clone()),
8358
8392
  auth_enabled: config.auth_enabled,
8393
+ // Argon2 is deliberately memory-hard. Bound concurrent legacy/session
8394
+ // verification so a burst waits asynchronously instead of exhausting
8395
+ // a small production VM with one allocation per request.
8396
+ authentication_workers: Arc::new(tokio::sync::Semaphore::new(2)),
8359
8397
  keys: KeyStore::load(&config.keys)?,
8360
8398
  metrics: Metrics::default(),
8361
8399
  cluster: ClusterStore::load(
@@ -8403,6 +8441,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
8403
8441
  grants: Arc::new(std::sync::Mutex::new(grant_store)),
8404
8442
  sync: Arc::new(std::sync::Mutex::new(sync_store)),
8405
8443
  workloads: Arc::new(std::sync::Mutex::new(workload_store)),
8444
+ state_triggers: Arc::new(std::sync::Mutex::new(state_trigger_store)),
8406
8445
  mesh: Arc::new(std::sync::Mutex::new(mesh_store)),
8407
8446
  readiness_probe: Arc::new(readiness_probe),
8408
8447
  bounded_query_cursors: Arc::new(std::sync::Mutex::new(HashMap::new())),
@@ -9057,6 +9096,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
9057
9096
 
9058
9097
  let mut app = Router::new()
9059
9098
  .route("/health", get(health))
9099
+ .route("/health/ready", get(health_ready))
9060
9100
  .route("/v1/health", get(health))
9061
9101
  .route("/v1/openapi.json", get(application_openapi))
9062
9102
  .route("/v1/auth/signup", axum::routing::post(auth_sign_up))
@@ -9080,6 +9120,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
9080
9120
  )
9081
9121
  .merge(protected)
9082
9122
  .merge(query_execution_diagnostics_router())
9123
+ .layer(middleware::from_fn_with_state(state.clone(), admit_request))
9083
9124
  .layer(middleware::from_fn(protocol_version))
9084
9125
  .layer(middleware::from_fn(attribute_request))
9085
9126
  .layer(middleware::from_fn_with_state(state.clone(), count_request))
@@ -9165,16 +9206,52 @@ async fn authenticate(
9165
9206
  .headers()
9166
9207
  .get(AUTHORIZATION)
9167
9208
  .and_then(|header| header.to_str().ok())
9168
- .and_then(|header| header.strip_prefix("Bearer "));
9169
- let machine = token.and_then(|token| state.keys.authenticate(token, &state.namespace));
9209
+ .and_then(|header| header.strip_prefix("Bearer "))
9210
+ .map(str::to_owned);
9211
+ let keys = state.keys.clone();
9212
+ let identities = state.identities.clone();
9213
+ let namespace = state.namespace.clone();
9214
+ let authentication_worker = match state.authentication_workers.clone().acquire_owned().await
9215
+ {
9216
+ Ok(permit) => permit,
9217
+ Err(error) => {
9218
+ tracing::error!(%error, "authentication worker limit closed");
9219
+ return ApiError(
9220
+ StatusCode::SERVICE_UNAVAILABLE,
9221
+ "authentication temporarily unavailable".into(),
9222
+ )
9223
+ .into_response();
9224
+ }
9225
+ };
9226
+ let (machine, actor) = match tokio::task::spawn_blocking(move || {
9227
+ let _authentication_worker = authentication_worker;
9228
+ let machine = token
9229
+ .as_deref()
9230
+ .and_then(|token| keys.authenticate(token, &namespace));
9231
+ let actor = if machine.is_none() {
9232
+ token
9233
+ .as_deref()
9234
+ .and_then(|token| identities.authenticate_session(token))
9235
+ } else {
9236
+ None
9237
+ };
9238
+ (machine, actor)
9239
+ })
9240
+ .await
9241
+ {
9242
+ Ok(authentication) => authentication,
9243
+ Err(error) => {
9244
+ tracing::error!(%error, "bearer authentication worker failed");
9245
+ return ApiError(
9246
+ StatusCode::SERVICE_UNAVAILABLE,
9247
+ "authentication temporarily unavailable".into(),
9248
+ )
9249
+ .into_response();
9250
+ }
9251
+ };
9170
9252
  // A valid service key is already authoritative. Do not feed that
9171
9253
  // high-entropy bearer token through the Argon2-backed human-session
9172
9254
  // verifier as well; doing so blocks the request worker needlessly.
9173
- let actor = if machine.is_none() {
9174
- token.and_then(|token| state.identities.authenticate_session(token))
9175
- } else {
9176
- None
9177
- };
9178
9255
  let human = request
9179
9256
  .headers()
9180
9257
  .get(COOKIE)
@@ -9425,6 +9502,234 @@ async fn attribute_request(request: Request<axum::body::Body>, next: Next) -> Re
9425
9502
  next.run(request).await
9426
9503
  }
9427
9504
 
9505
+ /// The overall status, from the conditions that actually bear on it.
9506
+ ///
9507
+ /// Storage participates. Before it did not, so a database that had discarded an
9508
+ /// incomplete final append reported exactly what a cleanly replayed one did —
9509
+ /// which is the difference between an operator learning their last write was
9510
+ /// lost and never finding out.
9511
+ fn overall_status(
9512
+ membership_healthy: bool,
9513
+ clock_healthy: bool,
9514
+ storage: &feltdb::StorageHealth,
9515
+ ) -> &'static str {
9516
+ if membership_healthy && clock_healthy && storage.is_clean() {
9517
+ "healthy"
9518
+ } else {
9519
+ "degraded"
9520
+ }
9521
+ }
9522
+
9523
+ #[cfg(test)]
9524
+ mod admission_tests {
9525
+ use super::overloaded;
9526
+ use axum::http::StatusCode;
9527
+ use axum::response::IntoResponse;
9528
+
9529
+ /// An overload is refused with a status and a hint, not a hang.
9530
+ ///
9531
+ /// The managed incident produced client-side timeouts and upstream 502s
9532
+ /// because the server had no inbound deadline and no admission bound: a
9533
+ /// request that could not proceed simply waited. A caller cannot act on
9534
+ /// that. It can act on this.
9535
+ #[test]
9536
+ fn an_overload_is_an_explicit_refusal_with_a_retry_hint() {
9537
+ let response = overloaded("server is at its in-flight request limit").into_response();
9538
+ assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
9539
+ assert_eq!(
9540
+ response
9541
+ .headers()
9542
+ .get(axum::http::header::RETRY_AFTER)
9543
+ .and_then(|value| value.to_str().ok()),
9544
+ Some("1"),
9545
+ "a caller is told when to come back"
9546
+ );
9547
+ }
9548
+
9549
+ /// 503 rather than 500: the request was refused, not mishandled.
9550
+ #[test]
9551
+ fn an_overload_is_not_reported_as_a_server_fault() {
9552
+ let response = overloaded("request exceeded the server deadline").into_response();
9553
+ assert_ne!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
9554
+ assert!(response.status().is_server_error());
9555
+ }
9556
+ }
9557
+
9558
+ #[cfg(test)]
9559
+ mod health_status_tests {
9560
+ use super::overall_status;
9561
+ use feltdb::StorageHealth;
9562
+
9563
+ const RECOVERED: StorageHealth = StorageHealth::RecoveredIncompleteWrite {
9564
+ byte_offset: 128,
9565
+ discarded_bytes: 40,
9566
+ };
9567
+
9568
+ /// Storage participates in the overall status.
9569
+ ///
9570
+ /// It did not before: a database that had discarded an incomplete final
9571
+ /// append reported exactly what a cleanly replayed one did, so the
9572
+ /// distinction the durable-log work established never reached an operator.
9573
+ #[test]
9574
+ fn a_recovered_database_is_reported_degraded() {
9575
+ assert_eq!(overall_status(true, true, &StorageHealth::Clean), "healthy");
9576
+ assert_eq!(overall_status(true, true, &RECOVERED), "degraded");
9577
+ }
9578
+
9579
+ /// And the conditions that already counted still do, so this narrows
9580
+ /// nothing.
9581
+ #[test]
9582
+ fn membership_and_clock_still_count() {
9583
+ assert_eq!(
9584
+ overall_status(false, true, &StorageHealth::Clean),
9585
+ "degraded"
9586
+ );
9587
+ assert_eq!(
9588
+ overall_status(true, false, &StorageHealth::Clean),
9589
+ "degraded"
9590
+ );
9591
+ assert_eq!(overall_status(false, false, &RECOVERED), "degraded");
9592
+ }
9593
+
9594
+ /// The reported storage value names an observation rather than asserting
9595
+ /// the property that is still unproven.
9596
+ #[test]
9597
+ fn the_storage_label_is_an_observation_not_a_promise() {
9598
+ assert_eq!(StorageHealth::Clean.label(), "clean");
9599
+ assert_eq!(RECOVERED.label(), "recovered-incomplete-write");
9600
+ for label in [StorageHealth::Clean.label(), RECOVERED.label()] {
9601
+ assert_ne!(label, "durable", "health must not claim durability");
9602
+ }
9603
+ }
9604
+ }
9605
+
9606
+ /// Shed load at the door, and abandon work the caller is no longer waiting for.
9607
+ ///
9608
+ /// Two failures this replaces, both observed in production:
9609
+ ///
9610
+ /// - **No inbound deadline.** A slow operation ran until the *client* gave up,
9611
+ /// and then kept running, so the load that caused the stall outlived every
9612
+ /// request that reported it.
9613
+ /// - **No admission bound.** Requests queued on the database's single lock
9614
+ /// until an upstream proxy returned 502. Concurrency past the bound buys no
9615
+ /// throughput — every read and write serializes on that lock — so all it
9616
+ /// decides is whether an overload is refused quickly or hidden.
9617
+ ///
9618
+ /// Both now produce an explicit `503` with `Retry-After`, which is a signal a
9619
+ /// caller can act on rather than a timeout it has to interpret.
9620
+ async fn admit_request(
9621
+ State(state): State<AppState>,
9622
+ request: axum::extract::Request,
9623
+ next: Next,
9624
+ ) -> axum::response::Response {
9625
+ let Ok(_permit) = state.admission.clone().try_acquire_owned() else {
9626
+ state.metrics.request_shed();
9627
+ return overloaded("server is at its in-flight request limit");
9628
+ };
9629
+
9630
+ match tokio::time::timeout(state.request_deadline, next.run(request)).await {
9631
+ Ok(response) => response,
9632
+ Err(_) => {
9633
+ state.metrics.request_deadline_exceeded();
9634
+ overloaded("request exceeded the server deadline")
9635
+ }
9636
+ }
9637
+ }
9638
+
9639
+ /// A refusal a caller can act on: an explicit status, a stable code, and a hint.
9640
+ fn overloaded(detail: &str) -> axum::response::Response {
9641
+ let mut response = (
9642
+ StatusCode::SERVICE_UNAVAILABLE,
9643
+ Json(json!({
9644
+ "error": detail,
9645
+ "code": "SERVICE_OVERLOADED",
9646
+ })),
9647
+ )
9648
+ .into_response();
9649
+ response.headers_mut().insert(
9650
+ axum::http::header::RETRY_AFTER,
9651
+ axum::http::HeaderValue::from_static("1"),
9652
+ );
9653
+ response
9654
+ }
9655
+
9656
+ /// Readiness: whether this server can actually serve a scoped operation.
9657
+ ///
9658
+ /// `/health` answers "is this process alive". It reads cluster state and the
9659
+ /// lease clock and never touches storage, so it cannot fail for a database
9660
+ /// reason — which is exactly how a deployment reported `200 healthy` while
9661
+ /// every scoped read timed out.
9662
+ ///
9663
+ /// This endpoint takes the database lock and performs a real bounded read, off
9664
+ /// the runtime and under the same deadline a request gets. **A server whose
9665
+ /// scoped operations are stalled reports `503` here.**
9666
+ async fn health_ready(State(state): State<AppState>) -> axum::response::Response {
9667
+ let probe = state.clone();
9668
+ let started = Instant::now();
9669
+ let outcome = tokio::time::timeout(
9670
+ state.request_deadline,
9671
+ tokio::task::spawn_blocking(move || {
9672
+ // A real scoped operation: acquire the state lock and read through
9673
+ // it. Bounded, so a large database does not make readiness slow.
9674
+ let health = probe.db.health();
9675
+ let _ = probe
9676
+ .db
9677
+ .list_collection_page("_flow_capabilities", None, 1)?;
9678
+ Ok::<_, feltdb::FlowError>(health)
9679
+ }),
9680
+ )
9681
+ .await;
9682
+
9683
+ let waited_ms = started.elapsed().as_millis() as u64;
9684
+ match outcome {
9685
+ Ok(Ok(Ok(health))) => {
9686
+ let ready = health.is_nominal();
9687
+ let body = Json(json!({
9688
+ "ready": ready,
9689
+ "storage": health.storage.label(),
9690
+ "durable_format": health.durable_format.to_string(),
9691
+ "probe_ms": waited_ms,
9692
+ }));
9693
+ if ready {
9694
+ (StatusCode::OK, body).into_response()
9695
+ } else {
9696
+ // Usable, but not in the condition a clean open produces.
9697
+ (StatusCode::SERVICE_UNAVAILABLE, body).into_response()
9698
+ }
9699
+ }
9700
+ Ok(Ok(Err(error))) => (
9701
+ StatusCode::SERVICE_UNAVAILABLE,
9702
+ Json(json!({
9703
+ "ready": false,
9704
+ "error": error.to_string(),
9705
+ "code": "STORAGE_UNAVAILABLE",
9706
+ "probe_ms": waited_ms,
9707
+ })),
9708
+ )
9709
+ .into_response(),
9710
+ Ok(Err(error)) => (
9711
+ StatusCode::SERVICE_UNAVAILABLE,
9712
+ Json(json!({
9713
+ "ready": false,
9714
+ "error": error.to_string(),
9715
+ "code": "STORAGE_PROBE_FAILED",
9716
+ "probe_ms": waited_ms,
9717
+ })),
9718
+ )
9719
+ .into_response(),
9720
+ Err(_) => (
9721
+ StatusCode::SERVICE_UNAVAILABLE,
9722
+ Json(json!({
9723
+ "ready": false,
9724
+ "error": "storage probe exceeded the server deadline",
9725
+ "code": "STORAGE_STALLED",
9726
+ "probe_ms": waited_ms,
9727
+ })),
9728
+ )
9729
+ .into_response(),
9730
+ }
9731
+ }
9732
+
9428
9733
  async fn health(State(state): State<AppState>) -> Json<HealthResponse<'static>> {
9429
9734
  let membership = match state.cluster.proposal().map(|value| value.phase) {
9430
9735
  Some(ProposalPhase::Preparing | ProposalPhase::Prepared) => "recovering",
@@ -9432,16 +9737,16 @@ async fn health(State(state): State<AppState>) -> Json<HealthResponse<'static>>
9432
9737
  _ => "healthy",
9433
9738
  };
9434
9739
  let clock = state.lease_clock.is_healthy();
9740
+ // Observed, not asserted. An incompatible or corrupt database never opens,
9741
+ // so a running server has already established its format; what remains to
9742
+ // report is whether replay discarded anything.
9743
+ let database = state.db.health();
9435
9744
  Json(HealthResponse {
9436
- status: if membership == "healthy" && clock {
9437
- "healthy"
9438
- } else {
9439
- "degraded"
9440
- },
9745
+ status: overall_status(membership == "healthy", clock, &database.storage),
9441
9746
  version: env!("CARGO_PKG_VERSION"),
9442
9747
  git_commit: option_env!("FELTDB_GIT_COMMIT").unwrap_or("unknown"),
9443
9748
  runtime: "self-hosted",
9444
- storage: "durable",
9749
+ storage: database.storage.label(),
9445
9750
  fabric: "healthy",
9446
9751
  execution: if state.worker_enabled {
9447
9752
  "autonomous"
@@ -9482,22 +9787,33 @@ async fn managed_diagnostics(State(state): State<AppState>) -> Json<ManagedDiagn
9482
9787
  }
9483
9788
 
9484
9789
  async fn readiness(State(state): State<AppState>) -> Result<Json<Value>, ApiError> {
9485
- if let Some(parent) = state.readiness_probe.parent() {
9486
- std::fs::create_dir_all(parent)
9790
+ let readiness_probe = state.readiness_probe.clone();
9791
+ let db = state.db.clone();
9792
+ let (state_version, storage) = tokio::task::spawn_blocking(move || {
9793
+ if let Some(parent) = readiness_probe.parent() {
9794
+ std::fs::create_dir_all(parent)
9795
+ .map_err(|error| ApiError(StatusCode::SERVICE_UNAVAILABLE, error.to_string()))?;
9796
+ }
9797
+ let mut probe = OpenOptions::new()
9798
+ .create(true)
9799
+ .write(true)
9800
+ .truncate(true)
9801
+ .open(readiness_probe.as_ref())
9487
9802
  .map_err(|error| ApiError(StatusCode::SERVICE_UNAVAILABLE, error.to_string()))?;
9488
- }
9489
- let mut probe = OpenOptions::new()
9490
- .create(true)
9491
- .write(true)
9492
- .truncate(true)
9493
- .open(state.readiness_probe.as_ref())
9494
- .map_err(|error| ApiError(StatusCode::SERVICE_UNAVAILABLE, error.to_string()))?;
9495
- probe
9496
- .write_all(b"feltdb-ready-v1\n")
9497
- .and_then(|_| probe.sync_all())
9498
- .map_err(|error| ApiError(StatusCode::SERVICE_UNAVAILABLE, error.to_string()))?;
9499
- let state_version = state.db.sequence()?;
9500
- let storage = state.db.instance_id()?;
9803
+ probe
9804
+ .write_all(b"feltdb-ready-v1\n")
9805
+ .and_then(|_| probe.sync_all())
9806
+ .map_err(|error| ApiError(StatusCode::SERVICE_UNAVAILABLE, error.to_string()))?;
9807
+ Ok::<_, ApiError>((db.sequence()?, db.instance_id()?))
9808
+ })
9809
+ .await
9810
+ .map_err(|error| {
9811
+ tracing::error!(%error, "readiness worker failed");
9812
+ ApiError(
9813
+ StatusCode::SERVICE_UNAVAILABLE,
9814
+ "readiness check temporarily unavailable".into(),
9815
+ )
9816
+ })??;
9501
9817
  Ok(Json(json!({
9502
9818
  "status": "ready",
9503
9819
  "authority": "rust",
@@ -9566,10 +9882,15 @@ fn manage_keys() -> Result<bool, Box<dyn std::error::Error>> {
9566
9882
  .map(str::to_string)
9567
9883
  .collect()
9568
9884
  };
9569
- let key = store.create(
9885
+ // `--worker <id>` issues a credential that authenticates as that
9886
+ // one worker rather than as a service. It is a narrowing: the
9887
+ // worker lifecycle routes accept it only for its own worker id, and
9888
+ // it cannot satisfy a check that wants a human or a service.
9889
+ let key = store.create_for_worker(
9570
9890
  name,
9571
9891
  values("--scope", "state:read,state:write,events:read"),
9572
9892
  values("--namespace", "default"),
9893
+ option("--worker"),
9573
9894
  )?;
9574
9895
  println!(
9575
9896
  "API key created (id: {}). This secret will not be shown again:\n{}",
@@ -9584,7 +9905,7 @@ fn manage_keys() -> Result<bool, Box<dyn std::error::Error>> {
9584
9905
  }
9585
9906
  println!("API key revoked: {id}");
9586
9907
  }
9587
- _ => return Err("usage: feltdb-server keys <create|list|revoke> [--keys path] [--name name] [--scope scopes] [--namespace names] [--id key-id]".into()),
9908
+ _ => return Err("usage: feltdb-server keys <create|list|revoke> [--keys path] [--name name] [--scope scopes] [--namespace names] [--worker worker-id] [--id key-id]".into()),
9588
9909
  }
9589
9910
  Ok(true)
9590
9911
  }
@@ -9682,6 +10003,61 @@ fn manage_workloads() -> Result<bool, Box<dyn std::error::Error>> {
9682
10003
  Ok(true)
9683
10004
  }
9684
10005
 
10006
+ fn manage_state_triggers() -> Result<bool, Box<dyn std::error::Error>> {
10007
+ let arguments: Vec<String> = std::env::args().collect();
10008
+ if arguments.get(1).map(String::as_str) != Some("trigger") {
10009
+ return Ok(false);
10010
+ }
10011
+ let command = arguments.get(2).map(String::as_str).unwrap_or("list");
10012
+ let option = |name: &str| {
10013
+ arguments
10014
+ .iter()
10015
+ .position(|value| value == name)
10016
+ .and_then(|index| arguments.get(index + 1))
10017
+ .cloned()
10018
+ };
10019
+ let data = PathBuf::from(option("--data").unwrap_or_else(|| "./data/feltdb.log".into()));
10020
+ let trigger_path = PathBuf::from(option("--store").unwrap_or_else(|| {
10021
+ data.with_extension("triggers.json")
10022
+ .to_string_lossy()
10023
+ .into_owned()
10024
+ }));
10025
+ let mut triggers = StateTriggerStore::load(trigger_path)?;
10026
+ match command {
10027
+ "define" => {
10028
+ let input = option("--input").ok_or("trigger define requires --input trigger.json")?;
10029
+ let trigger: feltdb::StateTrigger = serde_json::from_slice(&std::fs::read(input)?)?;
10030
+ triggers.define(trigger)?;
10031
+ println!("{}", serde_json::to_string_pretty(&triggers.list())?);
10032
+ }
10033
+ "list" => println!("{}", serde_json::to_string_pretty(&triggers.list())?),
10034
+ "status" => println!(
10035
+ "{}",
10036
+ serde_json::to_string_pretty(&json!({
10037
+ "triggers": triggers.triggers.len(),
10038
+ "cursors": triggers.cursor_versions(),
10039
+ }))?
10040
+ ),
10041
+ "evaluate" => {
10042
+ let db = FeltDb::open(&data)?;
10043
+ let workload_path = PathBuf::from(
10044
+ option("--workload-store")
10045
+ .unwrap_or_else(|| data.with_extension("workloads.json").to_string_lossy().into_owned()),
10046
+ );
10047
+ let mut workloads = WorkloadStore::load(workload_path)?;
10048
+ let produced = triggers.evaluate(&db, &mut workloads, unix_seconds_i64())?;
10049
+ println!("{}", serde_json::to_string_pretty(&produced)?);
10050
+ }
10051
+ _ => {
10052
+ return Err(
10053
+ "usage: feltdb-server trigger <define|list|status|evaluate> [--data path] [--store path] [--workload-store path] [--input trigger.json]"
10054
+ .into(),
10055
+ )
10056
+ }
10057
+ }
10058
+ Ok(true)
10059
+ }
10060
+
9685
10061
  fn manage_backup() -> Result<bool, Box<dyn std::error::Error>> {
9686
10062
  let arguments: Vec<String> = std::env::args().collect();
9687
10063
  if arguments.get(1).map(String::as_str) != Some("backup") {
@@ -10006,9 +10382,25 @@ fn start_peer_sessions(
10006
10382
  }
10007
10383
  }
10008
10384
  }
10009
- match state.db.compact_operation_log(&peers) {
10010
- Ok(removed) => state.metrics.operations_compacted(removed as u64),
10011
- Err(error) => tracing::warn!(%error, "operation compaction failed"),
10385
+ // Policy-driven, and off the runtime. Compaction rewrites the log
10386
+ // while holding the lock that serializes every read and write, so
10387
+ // running it on every tick stopped the world on every tick — and
10388
+ // running it inline blocked a runtime worker while it did.
10389
+ let compactor = state.clone();
10390
+ let compaction_peers = peers.clone();
10391
+ match tokio::task::spawn_blocking(move || {
10392
+ compactor.db.maybe_compact_operation_log(&compaction_peers)
10393
+ })
10394
+ .await
10395
+ {
10396
+ Ok(Ok(outcome)) => {
10397
+ state.metrics.operations_compacted(outcome.pruned() as u64);
10398
+ if outcome.rewrote_log() {
10399
+ tracing::debug!(pruned = outcome.pruned(), "durable log rewritten");
10400
+ }
10401
+ }
10402
+ Ok(Err(error)) => tracing::warn!(%error, "operation compaction failed"),
10403
+ Err(error) => tracing::warn!(%error, "operation compaction task failed"),
10012
10404
  }
10013
10405
  tokio::time::sleep(interval).await;
10014
10406
  }
@@ -10103,6 +10495,197 @@ async fn run_worker_pass(state: &AppState) -> Result<(), ApiError> {
10103
10495
  }
10104
10496
  execute_agent_run(state, &row.value, &worker).await?;
10105
10497
  }
10498
+
10499
+ evaluate_state_triggers(state)?;
10500
+ execute_eligible_workloads(state, &worker)?;
10501
+ Ok(())
10502
+ }
10503
+
10504
+ /// How long an autonomous claim holds its lease.
10505
+ ///
10506
+ /// Long enough that an ordinary capability program finishes inside it, short
10507
+ /// enough that a worker dying mid-execution returns the work quickly. The lease
10508
+ /// is the only thing that makes worker death recoverable, so this is a real
10509
+ /// bound rather than a formality.
10510
+ const WORKLOAD_LEASE_MS: u64 = 30_000;
10511
+
10512
+ fn state_trigger_store(
10513
+ state: &AppState,
10514
+ ) -> Result<std::sync::MutexGuard<'_, feltdb::StateTriggerStore>, ApiError> {
10515
+ state.state_triggers.lock().map_err(|_| {
10516
+ ApiError(
10517
+ StatusCode::INTERNAL_SERVER_ERROR,
10518
+ "state trigger store unavailable".into(),
10519
+ )
10520
+ })
10521
+ }
10522
+
10523
+ fn state_trigger_error(error: feltdb::StateTriggerError) -> ApiError {
10524
+ let status = match error.code.as_str() {
10525
+ "TRIGGER_INVALID" | "TRIGGER_UNAUTHORIZED" => StatusCode::BAD_REQUEST,
10526
+ "WORKLOAD_UNAUTHORIZED" => StatusCode::FORBIDDEN,
10527
+ "TRIGGER_STATE_REPLAY_FAILED" | "TRIGGER_STORAGE_FAILURE" | "WORKLOAD_STORAGE_FAILURE" => {
10528
+ StatusCode::SERVICE_UNAVAILABLE
10529
+ }
10530
+ _ => StatusCode::CONFLICT,
10531
+ };
10532
+ ApiError(
10533
+ status,
10534
+ serde_json::to_string(&error).unwrap_or(error.message),
10535
+ )
10536
+ }
10537
+
10538
+ fn evaluate_state_triggers(state: &AppState) -> Result<(), ApiError> {
10539
+ let mut triggers = state_trigger_store(state)?;
10540
+ if triggers.triggers.is_empty() {
10541
+ return Ok(());
10542
+ }
10543
+ let mut workloads = workload_store(state)?;
10544
+ triggers
10545
+ .evaluate(&state.db, &mut workloads, unix_seconds_i64())
10546
+ .map_err(state_trigger_error)?;
10547
+ Ok(())
10548
+ }
10549
+
10550
+ /// The durable workload store, or a service error if its lock is poisoned.
10551
+ fn workload_store(
10552
+ state: &AppState,
10553
+ ) -> Result<std::sync::MutexGuard<'_, feltdb::workload::WorkloadStore>, ApiError> {
10554
+ state.workloads.lock().map_err(|_| {
10555
+ ApiError(
10556
+ StatusCode::INTERNAL_SERVER_ERROR,
10557
+ "workload store unavailable".into(),
10558
+ )
10559
+ })
10560
+ }
10561
+
10562
+ /// The capabilities this node can actually run.
10563
+ ///
10564
+ /// This is what the worker presents to [`WorkloadStore::claim`], which requires
10565
+ /// the workload's `capability_snapshot` to be a subset of it. That check is
10566
+ /// therefore load-bearing rather than ceremonial: a workload whose declared
10567
+ /// capabilities this node cannot execute is never claimed by it, and stays
10568
+ /// `Ready` for a node that can.
10569
+ fn worker_capabilities(state: &AppState) -> Result<BTreeSet<String>, ApiError> {
10570
+ let mut capabilities: BTreeSet<String> = ["search", "identity"]
10571
+ .into_iter()
10572
+ .map(str::to_string)
10573
+ .collect();
10574
+ for row in state.db.list_collection("_flow_capabilities")? {
10575
+ if let Some((_, name)) = row.key.split_once(':') {
10576
+ capabilities.insert(name.to_string());
10577
+ }
10578
+ }
10579
+ Ok(capabilities)
10580
+ }
10581
+
10582
+ /// Claim and execute the durable workloads this node is eligible to run.
10583
+ ///
10584
+ /// This is the composition that makes `WorkloadStore` an execution primitive
10585
+ /// rather than a work data model. It adds no queue and no worker system: the
10586
+ /// scan is the pass that already runs, the claim is the existing lease and
10587
+ /// fencing, the execution is the existing capability interpreter, and the
10588
+ /// transitions are the existing state machine.
10589
+ ///
10590
+ /// Temporal eligibility needs no special handling here — `Workload::is_eligible_at`
10591
+ /// filters candidates and `WorkloadStore::claim` refuses anything early, so
10592
+ /// `not_before` and retry backoff gate this path by construction.
10593
+ ///
10594
+ /// The store lock is released before execution. A capability program reads
10595
+ /// application state through the same database, so holding the workload lock
10596
+ /// across it would make every other workload operation wait on unrelated work.
10597
+ fn execute_eligible_workloads(state: &AppState, worker: &str) -> Result<(), ApiError> {
10598
+ let capabilities = worker_capabilities(state)?;
10599
+ let now = unix_seconds_i64();
10600
+ let candidates: Vec<(String, String, Value)> = {
10601
+ let store = workload_store(state)?;
10602
+ store
10603
+ .workloads
10604
+ .values()
10605
+ // Selection, not enforcement. `WorkloadStore::claim` independently
10606
+ // refuses work that is early or beyond this node's capabilities, and
10607
+ // that is where the guarantee lives; filtering here only avoids
10608
+ // attempting a claim, every pass, that is known to be refused.
10609
+ .filter(|workload| {
10610
+ workload.state == feltdb::workload::WorkloadState::Ready
10611
+ && workload.is_eligible_at(now)
10612
+ && workload.capability_snapshot.is_subset(&capabilities)
10613
+ })
10614
+ .map(|workload| {
10615
+ (
10616
+ workload.workload_id.clone(),
10617
+ workload.definition_id.clone(),
10618
+ workload.input.clone(),
10619
+ )
10620
+ })
10621
+ .collect()
10622
+ };
10623
+
10624
+ for (id, definition, input) in candidates {
10625
+ // Unlike the filter above, this one is enforcement: nothing downstream
10626
+ // knows whether `definition_id` names something runnable. Claiming a
10627
+ // workload only to dead-letter it would consume work another node could
10628
+ // have executed, so an unrunnable definition is left untouched.
10629
+ if !is_executable_capability(state, &definition)? {
10630
+ continue;
10631
+ }
10632
+ let claim = match workload_store(state)?.claim(
10633
+ &id,
10634
+ worker,
10635
+ &capabilities,
10636
+ unix_seconds_i64(),
10637
+ WORKLOAD_LEASE_MS,
10638
+ ) {
10639
+ Ok(claim) => claim,
10640
+ // Another worker took it, or eligibility moved between the scan and
10641
+ // the claim. Both are ordinary outcomes of concurrent workers, not
10642
+ // failures of this pass.
10643
+ Err(_) => continue,
10644
+ };
10645
+ workload_store(state)?
10646
+ .start(
10647
+ &id,
10648
+ &claim.claim_id,
10649
+ worker,
10650
+ claim.fencing_token,
10651
+ unix_seconds_i64(),
10652
+ )
10653
+ .map_err(workload_error)?;
10654
+
10655
+ match execute_named_capability(state, &definition, &input) {
10656
+ Ok(output) => {
10657
+ workload_store(state)?
10658
+ .complete(
10659
+ &id,
10660
+ &claim.claim_id,
10661
+ worker,
10662
+ claim.fencing_token,
10663
+ &format!("result_{id}_{}", claim.fencing_token),
10664
+ output,
10665
+ vec![],
10666
+ 1,
10667
+ unix_seconds_i64(),
10668
+ )
10669
+ .map_err(workload_error)?;
10670
+ }
10671
+ Err(error) => {
10672
+ // A capability program that fails does so deterministically, so
10673
+ // this failure kind is deliberately outside the default
10674
+ // `retry_on` set: retrying it would burn attempts to reach the
10675
+ // same dead letter. Only the message is carried, because the
10676
+ // store refuses failure text that looks like a secret.
10677
+ let _ = workload_store(state)?.fail(
10678
+ &id,
10679
+ &claim.claim_id,
10680
+ worker,
10681
+ claim.fencing_token,
10682
+ "execution_error",
10683
+ &format!("capability {definition} failed with status {}", error.0),
10684
+ unix_seconds_i64(),
10685
+ );
10686
+ }
10687
+ }
10688
+ }
10106
10689
  Ok(())
10107
10690
  }
10108
10691
 
@@ -10138,10 +10721,10 @@ fn execute_builtin_capability(
10138
10721
  Ok(Value::Array(
10139
10722
  state
10140
10723
  .db
10141
- .list_collection(collection)?
10724
+ .query_collection(collection, Some(limit), |row| {
10725
+ row.value.to_string().to_lowercase().contains(&needle)
10726
+ })?
10142
10727
  .into_iter()
10143
- .filter(|row| row.value.to_string().to_lowercase().contains(&needle))
10144
- .take(limit)
10145
10728
  .map(|row| row.value)
10146
10729
  .collect(),
10147
10730
  ))
@@ -11182,11 +11765,11 @@ async fn get_provenance(
11182
11765
  Path((collection, id)): Path<(String, String)>,
11183
11766
  ) -> Result<Json<Value>, ApiError> {
11184
11767
  let key = record_key(&collection, &id)?;
11768
+ // A keyed lookup. This used to materialize the whole collection — cloning
11769
+ // every row under the global state lock — to find one record.
11185
11770
  let row = state
11186
11771
  .db
11187
- .list_collection(&collection)?
11188
- .into_iter()
11189
- .find(|row| row.key == key)
11772
+ .get_collection_record(&collection, &key)?
11190
11773
  .ok_or_else(|| ApiError(StatusCode::NOT_FOUND, "record not found".to_string()))?;
11191
11774
  let operation = row.operation.as_ref();
11192
11775
  Ok(Json(json!({
@@ -11216,12 +11799,14 @@ async fn search_collection(
11216
11799
  ));
11217
11800
  }
11218
11801
  let needle = request.query.to_lowercase();
11802
+ // Borrows each row and stops at the limit, rather than cloning the whole
11803
+ // collection and filtering afterwards. Only matches are cloned.
11219
11804
  let records = state
11220
11805
  .db
11221
- .list_collection(&collection)?
11806
+ .query_collection(&collection, Some(request.limit), |row| {
11807
+ row.value.to_string().to_lowercase().contains(&needle)
11808
+ })?
11222
11809
  .into_iter()
11223
- .filter(|row| row.value.to_string().to_lowercase().contains(&needle))
11224
- .take(request.limit)
11225
11810
  .map(record_response)
11226
11811
  .collect();
11227
11812
  Ok(Json(records))
@@ -13008,6 +13593,69 @@ async fn shutdown_signal() {
13008
13593
  tokio::select! { _ = ctrl_c => {}, _ = terminate => {} }
13009
13594
  }
13010
13595
 
13596
+ #[cfg(test)]
13597
+ mod worker_identity_tests {
13598
+ use super::grant_subject;
13599
+ use feltdb::authorization::Subject as GrantSubject;
13600
+ use feltdb_server::auth::Principal;
13601
+
13602
+ fn principal(subject_type: &str, identity: Option<&str>) -> Principal {
13603
+ Principal {
13604
+ key_id: "key-1".into(),
13605
+ scopes: vec!["*".into()],
13606
+ subject_type: subject_type.into(),
13607
+ identity_id: identity.map(str::to_string),
13608
+ session_id: None,
13609
+ }
13610
+ }
13611
+
13612
+ /// The workload worker lifecycle compares the authenticated subject against
13613
+ /// the `worker_id` in the request, so which subject each principal maps to
13614
+ /// *is* the security boundary. Nothing but the authenticated principal
13615
+ /// participates in this mapping.
13616
+ #[test]
13617
+ fn only_a_worker_principal_maps_to_a_worker_subject() {
13618
+ assert_eq!(
13619
+ grant_subject(&principal("worker", Some("worker-x"))),
13620
+ GrantSubject::Worker("worker-x".into()),
13621
+ );
13622
+ // Every other principal maps somewhere a worker check can never accept,
13623
+ // no matter what identity it carries or what scopes it holds.
13624
+ for (subject_type, expected) in [
13625
+ ("human", GrantSubject::Human("worker-x".into())),
13626
+ ("user", GrantSubject::Human("worker-x".into())),
13627
+ ("service", GrantSubject::Service("worker-x".into())),
13628
+ ("api_key", GrantSubject::Service("worker-x".into())),
13629
+ ("agent", GrantSubject::Agent("worker-x".into())),
13630
+ ] {
13631
+ let subject = grant_subject(&principal(subject_type, Some("worker-x")));
13632
+ assert_eq!(subject, expected, "{subject_type} mapped unexpectedly");
13633
+ assert_ne!(
13634
+ subject,
13635
+ GrantSubject::Worker("worker-x".into()),
13636
+ "{subject_type} must never satisfy a worker check"
13637
+ );
13638
+ }
13639
+ }
13640
+
13641
+ /// A worker principal is one worker, not any worker.
13642
+ #[test]
13643
+ fn a_worker_subject_is_bound_to_its_own_identity() {
13644
+ let subject = grant_subject(&principal("worker", Some("worker-x")));
13645
+ assert_ne!(subject, GrantSubject::Worker("worker-y".into()));
13646
+ }
13647
+
13648
+ /// Without an identity the principal falls back to its key id, so a worker
13649
+ /// credential missing its worker id cannot silently become a wildcard.
13650
+ #[test]
13651
+ fn a_worker_principal_without_an_identity_falls_back_to_its_key_id() {
13652
+ assert_eq!(
13653
+ grant_subject(&principal("worker", None)),
13654
+ GrantSubject::Worker("key-1".into()),
13655
+ );
13656
+ }
13657
+ }
13658
+
13011
13659
  #[cfg(test)]
13012
13660
  mod authority_gate_tests {
13013
13661
  use super::{api_error_body, authorize_transaction_collections, state_authorization};