@feltdb/core 0.4.12 → 0.4.14

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 (165) hide show
  1. package/dist/cli/commands.js +42 -25
  2. package/dist/cli/index.js +1 -1
  3. package/dist/create/cli.js +39 -4
  4. package/dist/create/create.js +30 -18
  5. package/dist/create/docker-compose-generator.js +68 -11
  6. package/dist/create/package-versions.js +1 -1
  7. package/dist/create/server-source/Cargo.lock +2238 -0
  8. package/dist/create/server-source/Cargo.toml +7 -0
  9. package/dist/create/server-source/crates/feltdb/Cargo.lock +175 -0
  10. package/dist/create/server-source/crates/feltdb/Cargo.toml +79 -0
  11. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
  12. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
  13. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
  14. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
  15. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
  19. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
  20. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
  21. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
  22. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
  23. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
  24. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
  25. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
  26. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
  27. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
  28. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
  29. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +286 -0
  30. package/dist/create/server-source/crates/feltdb/src/admission.rs +192 -0
  31. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
  32. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
  33. package/dist/create/server-source/crates/feltdb/src/analytics.rs +475 -0
  34. package/dist/create/server-source/crates/feltdb/src/application.rs +2244 -0
  35. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
  36. package/dist/create/server-source/crates/feltdb/src/authorization.rs +1030 -0
  37. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
  38. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
  39. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
  40. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
  41. package/dist/create/server-source/crates/feltdb/src/capability.rs +903 -0
  42. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
  43. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
  44. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
  45. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
  46. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
  47. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
  48. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +465 -0
  49. package/dist/create/server-source/crates/feltdb/src/convergence.rs +618 -0
  50. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
  51. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +380 -0
  52. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
  53. package/dist/create/server-source/crates/feltdb/src/cron.rs +294 -0
  54. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
  55. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
  56. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
  57. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
  58. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
  59. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
  60. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +316 -0
  61. package/dist/create/server-source/crates/feltdb/src/execution.rs +426 -0
  62. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
  63. package/dist/create/server-source/crates/feltdb/src/indexing.rs +779 -0
  64. package/dist/create/server-source/crates/feltdb/src/lib.rs +2838 -0
  65. package/dist/create/server-source/crates/feltdb/src/materialization.rs +184 -0
  66. package/dist/create/server-source/crates/feltdb/src/metrics.rs +267 -0
  67. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
  68. package/dist/create/server-source/crates/feltdb/src/observability.rs +366 -0
  69. package/dist/create/server-source/crates/feltdb/src/operation.rs +251 -0
  70. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
  71. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +344 -0
  72. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
  73. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +166 -0
  74. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
  75. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
  76. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
  77. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
  78. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
  79. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
  80. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
  81. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
  82. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
  83. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
  84. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
  85. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
  86. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
  87. package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
  88. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
  89. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
  90. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
  91. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
  92. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
  93. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
  94. package/dist/create/server-source/crates/feltdb/src/production_api.rs +400 -0
  95. package/dist/create/server-source/crates/feltdb/src/provenance.rs +207 -0
  96. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +217 -0
  97. package/dist/create/server-source/crates/feltdb/src/references.rs +404 -0
  98. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
  99. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +160 -0
  100. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
  101. package/dist/create/server-source/crates/feltdb/src/routing.rs +409 -0
  102. package/dist/create/server-source/crates/feltdb/src/sharding.rs +523 -0
  103. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +3118 -0
  104. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +291 -0
  105. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
  106. package/dist/create/server-source/crates/feltdb/src/storage.rs +267 -0
  107. package/dist/create/server-source/crates/feltdb/src/submission.rs +477 -0
  108. package/dist/create/server-source/crates/feltdb/src/sync.rs +483 -0
  109. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +822 -0
  110. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
  111. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +473 -0
  112. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
  113. package/dist/create/server-source/crates/feltdb/src/transactions.rs +646 -0
  114. package/dist/create/server-source/crates/feltdb/src/trigger.rs +390 -0
  115. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
  116. package/dist/create/server-source/crates/feltdb/src/workflow.rs +713 -0
  117. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
  118. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
  119. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
  120. package/dist/create/server-source/crates/feltdb/src/workload.rs +1310 -0
  121. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +23 -0
  122. package/dist/create/server-source/crates/feltdb-server/README.md +79 -0
  123. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +73 -0
  124. package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
  125. package/dist/create/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
  126. package/dist/create/server-source/crates/feltdb-server/src/audit.rs +59 -0
  127. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +308 -0
  128. package/dist/create/server-source/crates/feltdb-server/src/authorization.rs +228 -0
  129. package/dist/create/server-source/crates/feltdb-server/src/backup.rs +416 -0
  130. package/dist/create/server-source/crates/feltdb-server/src/causal.rs +218 -0
  131. package/dist/create/server-source/crates/feltdb-server/src/certification.rs +223 -0
  132. package/dist/create/server-source/crates/feltdb-server/src/clock.rs +132 -0
  133. package/dist/create/server-source/crates/feltdb-server/src/cluster.rs +165 -0
  134. package/dist/create/server-source/crates/feltdb-server/src/connections.rs +1044 -0
  135. package/dist/create/server-source/crates/feltdb-server/src/content.rs +111 -0
  136. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +250 -0
  137. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +78 -0
  138. package/dist/create/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
  139. package/dist/create/server-source/crates/feltdb-server/src/leases.rs +123 -0
  140. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +25 -0
  141. package/dist/create/server-source/crates/feltdb-server/src/main.rs +8715 -0
  142. package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +97 -0
  143. package/dist/create/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
  144. package/dist/create/server-source/crates/feltdb-server/src/principals.rs +510 -0
  145. package/dist/create/server-source/crates/feltdb-server/src/providers.rs +269 -0
  146. package/dist/create/server-source/crates/feltdb-server/src/releases.rs +2563 -0
  147. package/dist/create/server-source/crates/feltdb-server/src/sessions.rs +156 -0
  148. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
  149. package/dist/create/server-source/crates/feltdb-server/src/versions.rs +264 -0
  150. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
  151. package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
  152. package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
  153. package/dist/flowspec.d.ts +2 -0
  154. package/dist/flowspec.d.ts.map +1 -1
  155. package/dist/flowspec.js +28 -4
  156. package/dist/state-contract.d.ts +7 -1
  157. package/dist/state-contract.d.ts.map +1 -1
  158. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  159. package/dist/studio/components/ConflictExplorer.d.ts.map +1 -1
  160. package/dist/studio/components/index.js +1 -1
  161. package/dist/studio/{components-Q0Nx6gLE.js → components-9kDSWiGL.js} +163 -47
  162. package/dist/studio/index.js +31 -28
  163. package/dist/studio-app/assets/{index-BHOmLZF-.js → index-DVdvmf69.js} +9 -9
  164. package/dist/studio-app/index.html +1 -1
  165. package/package.json +1 -1
@@ -0,0 +1,510 @@
1
+ use crate::{
2
+ authorization::{authorize, AuthorizationRequest, Subject},
3
+ tenancy::TenancyStore,
4
+ };
5
+ use rand::{distributions::Alphanumeric, Rng};
6
+ use serde::{Deserialize, Serialize};
7
+ use std::{
8
+ fs,
9
+ path::PathBuf,
10
+ sync::{Arc, RwLock},
11
+ time::{SystemTime, UNIX_EPOCH},
12
+ };
13
+
14
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15
+ #[serde(rename_all = "snake_case")]
16
+ pub enum PrincipalKind {
17
+ Agent,
18
+ Workload,
19
+ Service,
20
+ Worker,
21
+ Connection,
22
+ }
23
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24
+ #[serde(rename_all = "snake_case")]
25
+ pub enum PrincipalStatus {
26
+ Active,
27
+ Suspended,
28
+ Running,
29
+ Completed,
30
+ Failed,
31
+ }
32
+ #[derive(Debug, Clone, Serialize, Deserialize)]
33
+ pub struct PrincipalRecord {
34
+ pub id: String,
35
+ pub application_id: String,
36
+ pub kind: PrincipalKind,
37
+ pub name: String,
38
+ pub version: u64,
39
+ pub status: PrincipalStatus,
40
+ pub created_by: String,
41
+ pub created_at: u64,
42
+ pub capabilities: Vec<String>,
43
+ pub parent_id: Option<String>,
44
+ pub worker_id: Option<String>,
45
+ pub started_at: Option<u64>,
46
+ pub completed_at: Option<u64>,
47
+ }
48
+ #[derive(Debug, Clone, Serialize, Deserialize)]
49
+ pub struct PrincipalAuditEvent {
50
+ pub event: String,
51
+ pub actor: String,
52
+ pub principal_id: String,
53
+ pub application_id: String,
54
+ pub capability: Option<String>,
55
+ pub decision: Option<bool>,
56
+ pub at: u64,
57
+ }
58
+ #[derive(Debug, Default, Serialize, Deserialize)]
59
+ struct PrincipalRecords {
60
+ principals: Vec<PrincipalRecord>,
61
+ audit: Vec<PrincipalAuditEvent>,
62
+ }
63
+ #[derive(Clone)]
64
+ pub struct PrincipalStore {
65
+ path: PathBuf,
66
+ records: Arc<RwLock<PrincipalRecords>>,
67
+ }
68
+
69
+ fn now() -> u64 {
70
+ SystemTime::now()
71
+ .duration_since(UNIX_EPOCH)
72
+ .unwrap_or_default()
73
+ .as_secs()
74
+ }
75
+ fn id(prefix: &str) -> String {
76
+ let value: String = rand::thread_rng()
77
+ .sample_iter(&Alphanumeric)
78
+ .take(18)
79
+ .map(char::from)
80
+ .collect();
81
+ format!("{prefix}_{value}")
82
+ }
83
+ fn kind_subject(record: &PrincipalRecord) -> Subject {
84
+ match record.kind {
85
+ PrincipalKind::Agent => Subject::Agent(record.id.clone()),
86
+ PrincipalKind::Workload => Subject::Workload(record.id.clone()),
87
+ PrincipalKind::Service => Subject::Service(record.id.clone()),
88
+ PrincipalKind::Worker => Subject::Worker(record.id.clone()),
89
+ PrincipalKind::Connection => Subject::Connection(record.id.clone()),
90
+ }
91
+ }
92
+
93
+ impl PrincipalStore {
94
+ pub fn load(path: impl Into<PathBuf>) -> Result<Self, String> {
95
+ let path = path.into();
96
+ let records = if path.exists() {
97
+ serde_json::from_slice(&fs::read(&path).map_err(|error| error.to_string())?)
98
+ .map_err(|error| error.to_string())?
99
+ } else {
100
+ PrincipalRecords::default()
101
+ };
102
+ Ok(Self {
103
+ path,
104
+ records: Arc::new(RwLock::new(records)),
105
+ })
106
+ }
107
+ fn persist(&self, records: &PrincipalRecords) -> Result<(), String> {
108
+ if let Some(parent) = self.path.parent() {
109
+ fs::create_dir_all(parent).map_err(|error| error.to_string())?;
110
+ }
111
+ let temporary = self.path.with_extension("tmp");
112
+ fs::write(
113
+ &temporary,
114
+ serde_json::to_vec_pretty(records).map_err(|error| error.to_string())?,
115
+ )
116
+ .map_err(|error| error.to_string())?;
117
+ fs::rename(temporary, &self.path).map_err(|error| error.to_string())
118
+ }
119
+ fn event(
120
+ records: &mut PrincipalRecords,
121
+ event: &str,
122
+ actor: &str,
123
+ principal: &PrincipalRecord,
124
+ capability: Option<&str>,
125
+ decision: Option<bool>,
126
+ ) {
127
+ records.audit.push(PrincipalAuditEvent {
128
+ event: event.into(),
129
+ actor: actor.into(),
130
+ principal_id: principal.id.clone(),
131
+ application_id: principal.application_id.clone(),
132
+ capability: capability.map(str::to_string),
133
+ decision,
134
+ at: now(),
135
+ });
136
+ }
137
+ fn application_context(tenancy: &TenancyStore, application_id: &str) -> Result<String, String> {
138
+ tenancy
139
+ .application_tenant(application_id)
140
+ .ok_or("application not found".into())
141
+ }
142
+
143
+ pub fn list(
144
+ &self,
145
+ actor: &str,
146
+ tenancy: &TenancyStore,
147
+ application_id: &str,
148
+ ) -> Result<Vec<PrincipalRecord>, String> {
149
+ let tenant_id = Self::application_context(tenancy, application_id)?;
150
+ if !tenancy.authorize_application(actor, &tenant_id, application_id, "applications:read") {
151
+ return Err("forbidden".into());
152
+ }
153
+ Ok(self
154
+ .records
155
+ .read()
156
+ .map_err(|_| "principal store lock poisoned")?
157
+ .principals
158
+ .iter()
159
+ .filter(|principal| principal.application_id == application_id)
160
+ .cloned()
161
+ .collect())
162
+ }
163
+ pub fn create(
164
+ &self,
165
+ actor: &str,
166
+ tenancy: &TenancyStore,
167
+ application_id: &str,
168
+ kind: PrincipalKind,
169
+ name: &str,
170
+ ) -> Result<PrincipalRecord, String> {
171
+ let tenant_id = Self::application_context(tenancy, application_id)?;
172
+ let required = match kind {
173
+ PrincipalKind::Agent => "agents:write",
174
+ PrincipalKind::Connection => "connections:write",
175
+ _ => "applications:write",
176
+ };
177
+ if !tenancy.authorize_application(actor, &tenant_id, application_id, required) {
178
+ return Err("forbidden".into());
179
+ }
180
+ let prefix = match kind {
181
+ PrincipalKind::Agent => "agent",
182
+ PrincipalKind::Workload => "workload",
183
+ PrincipalKind::Service => "service",
184
+ PrincipalKind::Worker => "worker",
185
+ PrincipalKind::Connection => "connection",
186
+ };
187
+ let principal = PrincipalRecord {
188
+ id: id(prefix),
189
+ application_id: application_id.into(),
190
+ kind,
191
+ name: name.into(),
192
+ version: 1,
193
+ status: PrincipalStatus::Active,
194
+ created_by: actor.into(),
195
+ created_at: now(),
196
+ capabilities: vec![],
197
+ parent_id: None,
198
+ worker_id: None,
199
+ started_at: None,
200
+ completed_at: None,
201
+ };
202
+ let mut records = self
203
+ .records
204
+ .write()
205
+ .map_err(|_| "principal store lock poisoned")?;
206
+ records.principals.push(principal.clone());
207
+ Self::event(
208
+ &mut records,
209
+ "principal.created",
210
+ actor,
211
+ &principal,
212
+ None,
213
+ None,
214
+ );
215
+ self.persist(&records)?;
216
+ Ok(principal)
217
+ }
218
+ pub fn grant(
219
+ &self,
220
+ actor: &str,
221
+ tenancy: &TenancyStore,
222
+ principal_id: &str,
223
+ capability: &str,
224
+ ) -> Result<PrincipalRecord, String> {
225
+ if capability == "secrets:plaintext" {
226
+ return Err("plaintext secret access cannot be granted".into());
227
+ }
228
+ let mut records = self
229
+ .records
230
+ .write()
231
+ .map_err(|_| "principal store lock poisoned")?;
232
+ let index = records
233
+ .principals
234
+ .iter()
235
+ .position(|principal| principal.id == principal_id)
236
+ .ok_or("principal not found")?;
237
+ let application_id = records.principals[index].application_id.clone();
238
+ let tenant_id = Self::application_context(tenancy, &application_id)?;
239
+ if actor == principal_id
240
+ || !tenancy.authorize_application(
241
+ actor,
242
+ &tenant_id,
243
+ &application_id,
244
+ "applications:write",
245
+ )
246
+ {
247
+ return Err("forbidden".into());
248
+ }
249
+ if !records.principals[index]
250
+ .capabilities
251
+ .iter()
252
+ .any(|existing| existing == capability)
253
+ {
254
+ records.principals[index]
255
+ .capabilities
256
+ .push(capability.into());
257
+ records.principals[index].version += 1;
258
+ }
259
+ let principal = records.principals[index].clone();
260
+ Self::event(
261
+ &mut records,
262
+ "principal.capability_granted",
263
+ actor,
264
+ &principal,
265
+ Some(capability),
266
+ None,
267
+ );
268
+ self.persist(&records)?;
269
+ Ok(principal)
270
+ }
271
+ pub fn authorize(&self, principal_id: &str, application_id: &str, capability: &str) -> bool {
272
+ let mut records = match self.records.write() {
273
+ Ok(records) => records,
274
+ Err(_) => return false,
275
+ };
276
+ let Some(principal) = records
277
+ .principals
278
+ .iter()
279
+ .find(|principal| principal.id == principal_id)
280
+ .cloned()
281
+ else {
282
+ return false;
283
+ };
284
+ let active = matches!(
285
+ principal.status,
286
+ PrincipalStatus::Active | PrincipalStatus::Running
287
+ ) && principal.application_id == application_id;
288
+ let allowed = active
289
+ && capability != "secrets:plaintext"
290
+ && authorize(&AuthorizationRequest {
291
+ subject: &kind_subject(&principal),
292
+ tenant_id: "delegated",
293
+ application_id: Some(application_id),
294
+ capability,
295
+ direct_grants: &principal.capabilities,
296
+ memberships: &[],
297
+ roles: &[],
298
+ })
299
+ .allowed;
300
+ Self::event(
301
+ &mut records,
302
+ "principal.authorized",
303
+ principal_id,
304
+ &principal,
305
+ Some(capability),
306
+ Some(allowed),
307
+ );
308
+ let _ = self.persist(&records);
309
+ allowed
310
+ }
311
+ pub fn create_workload(
312
+ &self,
313
+ actor: &str,
314
+ tenancy: &TenancyStore,
315
+ agent_id: &str,
316
+ ) -> Result<PrincipalRecord, String> {
317
+ let mut records = self
318
+ .records
319
+ .write()
320
+ .map_err(|_| "principal store lock poisoned")?;
321
+ let agent = records
322
+ .principals
323
+ .iter()
324
+ .find(|principal| principal.id == agent_id && principal.kind == PrincipalKind::Agent)
325
+ .cloned()
326
+ .ok_or("agent not found")?;
327
+ let tenant_id = Self::application_context(tenancy, &agent.application_id)?;
328
+ if !tenancy.authorize_application(
329
+ actor,
330
+ &tenant_id,
331
+ &agent.application_id,
332
+ "workloads:execute",
333
+ ) && !tenancy.authorize_application(
334
+ actor,
335
+ &tenant_id,
336
+ &agent.application_id,
337
+ "applications:write",
338
+ ) {
339
+ return Err("forbidden".into());
340
+ }
341
+ let workload = PrincipalRecord {
342
+ id: id("workload"),
343
+ application_id: agent.application_id.clone(),
344
+ kind: PrincipalKind::Workload,
345
+ name: format!("{} execution", agent.name),
346
+ version: agent.version,
347
+ status: PrincipalStatus::Running,
348
+ created_by: actor.into(),
349
+ created_at: now(),
350
+ capabilities: agent.capabilities.clone(),
351
+ parent_id: Some(agent.id),
352
+ worker_id: None,
353
+ started_at: Some(now()),
354
+ completed_at: None,
355
+ };
356
+ records.principals.push(workload.clone());
357
+ Self::event(
358
+ &mut records,
359
+ "workload.created",
360
+ actor,
361
+ &workload,
362
+ None,
363
+ None,
364
+ );
365
+ self.persist(&records)?;
366
+ Ok(workload)
367
+ }
368
+ pub fn assign_worker(
369
+ &self,
370
+ actor: &str,
371
+ tenancy: &TenancyStore,
372
+ workload_id: &str,
373
+ worker_id: &str,
374
+ ) -> Result<(), String> {
375
+ let mut records = self
376
+ .records
377
+ .write()
378
+ .map_err(|_| "principal store lock poisoned")?;
379
+ let workload_index = records
380
+ .principals
381
+ .iter()
382
+ .position(|principal| {
383
+ principal.id == workload_id && principal.kind == PrincipalKind::Workload
384
+ })
385
+ .ok_or("workload not found")?;
386
+ let worker = records
387
+ .principals
388
+ .iter()
389
+ .find(|principal| principal.id == worker_id && principal.kind == PrincipalKind::Worker)
390
+ .cloned()
391
+ .ok_or("worker not found")?;
392
+ let app_id = records.principals[workload_index].application_id.clone();
393
+ if worker.application_id != app_id {
394
+ return Err("cross-application worker assignment denied".into());
395
+ }
396
+ let tenant_id = Self::application_context(tenancy, &app_id)?;
397
+ if !tenancy.authorize_application(actor, &tenant_id, &app_id, "applications:write") {
398
+ return Err("forbidden".into());
399
+ }
400
+ records.principals[workload_index].worker_id = Some(worker_id.into());
401
+ self.persist(&records)
402
+ }
403
+ pub fn worker_can_execute(&self, worker_id: &str, workload_id: &str) -> bool {
404
+ self.records
405
+ .read()
406
+ .map(|records| {
407
+ records.principals.iter().any(|principal| {
408
+ principal.id == workload_id
409
+ && principal.kind == PrincipalKind::Workload
410
+ && principal.worker_id.as_deref() == Some(worker_id)
411
+ && principal.status == PrincipalStatus::Running
412
+ })
413
+ })
414
+ .unwrap_or(false)
415
+ }
416
+ pub fn execution_snapshot(
417
+ &self,
418
+ worker_id: &str,
419
+ workload_id: &str,
420
+ ) -> Option<(String, String, Vec<String>)> {
421
+ self.records
422
+ .read()
423
+ .ok()?
424
+ .principals
425
+ .iter()
426
+ .find(|principal| {
427
+ principal.id == workload_id
428
+ && principal.kind == PrincipalKind::Workload
429
+ && principal.worker_id.as_deref() == Some(worker_id)
430
+ && principal.status == PrincipalStatus::Running
431
+ })
432
+ .map(|workload| {
433
+ (
434
+ workload.application_id.clone(),
435
+ workload.id.clone(),
436
+ workload.capabilities.clone(),
437
+ )
438
+ })
439
+ }
440
+ }
441
+
442
+ #[cfg(test)]
443
+ mod tests {
444
+ use super::*;
445
+ fn stores() -> (TenancyStore, PrincipalStore, String, String) {
446
+ let suffix = id("test");
447
+ let tenancy =
448
+ TenancyStore::load(std::env::temp_dir().join(format!("{suffix}-tenancy.json")))
449
+ .unwrap();
450
+ let tenant = tenancy.create_tenant("owner", "Acme Principals").unwrap();
451
+ let app = tenancy
452
+ .create_application("owner", &tenant.id, "Operations")
453
+ .unwrap();
454
+ let principals =
455
+ PrincipalStore::load(std::env::temp_dir().join(format!("{suffix}-principals.json")))
456
+ .unwrap();
457
+ (tenancy, principals, tenant.id, app.id)
458
+ }
459
+ #[test]
460
+ fn agent_and_workload_are_scoped_without_escalation() {
461
+ let (tenancy, principals, _, app) = stores();
462
+ let agent = principals
463
+ .create("owner", &tenancy, &app, PrincipalKind::Agent, "Coordinator")
464
+ .unwrap();
465
+ principals
466
+ .grant("owner", &tenancy, &agent.id, "state:read")
467
+ .unwrap();
468
+ assert!(principals.authorize(&agent.id, &app, "state:read"));
469
+ assert!(!principals.authorize(&agent.id, &app, "state:write"));
470
+ assert!(!principals.authorize(&agent.id, "app_other", "state:read"));
471
+ assert!(principals
472
+ .grant(&agent.id, &tenancy, &agent.id, "state:write")
473
+ .is_err());
474
+ let workload = principals
475
+ .create_workload("owner", &tenancy, &agent.id)
476
+ .unwrap();
477
+ assert!(principals.authorize(&workload.id, &app, "state:read"));
478
+ assert!(!principals.authorize(&workload.id, &app, "state:write"));
479
+ }
480
+ #[test]
481
+ fn worker_only_executes_assigned_workload_and_secrets_remain_opaque() {
482
+ let (tenancy, principals, _, app) = stores();
483
+ let agent = principals
484
+ .create("owner", &tenancy, &app, PrincipalKind::Agent, "Agent")
485
+ .unwrap();
486
+ principals
487
+ .grant("owner", &tenancy, &agent.id, "connection:use")
488
+ .unwrap();
489
+ assert!(principals
490
+ .grant("owner", &tenancy, &agent.id, "secrets:plaintext")
491
+ .is_err());
492
+ let workload = principals
493
+ .create_workload("owner", &tenancy, &agent.id)
494
+ .unwrap();
495
+ let worker = principals
496
+ .create(
497
+ "owner",
498
+ &tenancy,
499
+ &app,
500
+ PrincipalKind::Worker,
501
+ "worker-east",
502
+ )
503
+ .unwrap();
504
+ assert!(!principals.worker_can_execute(&worker.id, &workload.id));
505
+ principals
506
+ .assign_worker("owner", &tenancy, &workload.id, &worker.id)
507
+ .unwrap();
508
+ assert!(principals.worker_can_execute(&worker.id, &workload.id));
509
+ }
510
+ }