@feltdb/core 0.12.1 → 0.13.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.
Files changed (139) hide show
  1. package/dist/build-identity.json +13 -4
  2. package/dist/cli/commands.js +96 -45
  3. package/dist/cli/workspace-integration.js +8 -0
  4. package/dist/collection.d.ts +41 -4
  5. package/dist/collection.d.ts.map +1 -1
  6. package/dist/collection.js +116 -47
  7. package/dist/composition.d.ts.map +1 -1
  8. package/dist/composition.js +11 -1
  9. package/dist/create/build-identity.json +13 -4
  10. package/dist/create/cli.js +49 -40
  11. package/dist/create/create.js +32 -19
  12. package/dist/create/package-versions.js +1 -1
  13. package/dist/create/server-source/Cargo.lock +18 -3
  14. package/dist/create/server-source/Cargo.toml +4 -2
  15. package/dist/create/server-source/crates/feltdb/Cargo.toml +20 -0
  16. package/dist/create/server-source/crates/feltdb/LICENSE +21 -0
  17. package/dist/create/server-source/crates/feltdb/README.md +75 -0
  18. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +320 -0
  19. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +1545 -0
  20. package/dist/create/server-source/crates/feltdb/src/authority.rs +145 -15
  21. package/dist/create/server-source/crates/feltdb/src/lib.rs +227 -23
  22. package/dist/create/server-source/crates/feltdb/src/state_model.rs +1 -5
  23. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +204 -1
  24. package/dist/create/server-source/crates/feltdb/src/workload.rs +685 -53
  25. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +1188 -0
  26. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +2884 -0
  27. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +534 -0
  28. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +23340 -0
  29. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +18 -12
  30. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +17 -5
  31. package/dist/create/server-source/crates/feltdb-client/Cargo.toml +29 -0
  32. package/dist/create/server-source/crates/feltdb-client/LICENSE +21 -0
  33. package/dist/create/server-source/crates/feltdb-client/README.md +70 -0
  34. package/dist/create/server-source/crates/feltdb-client/src/application.rs +527 -0
  35. package/dist/create/server-source/crates/feltdb-client/src/authority.rs +679 -0
  36. package/dist/create/server-source/crates/feltdb-client/src/control.rs +292 -0
  37. package/dist/create/server-source/crates/feltdb-client/src/error.rs +227 -0
  38. package/dist/create/server-source/crates/feltdb-client/src/events.rs +146 -0
  39. package/dist/create/server-source/crates/feltdb-client/src/lib.rs +466 -0
  40. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/README.md +4 -0
  41. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/private-ca.pem +20 -0
  42. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server-key.pem +28 -0
  43. package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server.pem +20 -0
  44. package/dist/create/server-source/crates/feltdb-client/tests/private_ca.rs +95 -0
  45. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +20 -1
  46. package/dist/create/server-source/crates/feltdb-server/LICENSE +21 -0
  47. package/dist/create/server-source/crates/feltdb-server/README.md +12 -1
  48. package/dist/create/server-source/crates/feltdb-server/src/agent_execution.rs +511 -0
  49. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +28 -0
  50. package/dist/create/server-source/crates/feltdb-server/src/backup.rs +6 -2
  51. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +21 -0
  52. package/dist/create/server-source/crates/feltdb-server/src/main.rs +845 -138
  53. package/dist/create/server-source/crates/feltdb-server/tests/rust_client.rs +683 -0
  54. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +3 -1
  55. package/dist/create/setup-summary.js +40 -0
  56. package/dist/create/template/default-project/README.md +12 -7
  57. package/dist/create/template/default-project/src/pages/Dashboard.tsx +1 -1
  58. package/dist/create/template/dot-feltdb-README.md +3 -3
  59. package/dist/db.d.ts +27 -1
  60. package/dist/db.d.ts.map +1 -1
  61. package/dist/db.js +60 -21
  62. package/dist/development-runtime-bridge.d.ts +20 -0
  63. package/dist/development-runtime-bridge.d.ts.map +1 -1
  64. package/dist/development-runtime-bridge.js +74 -4
  65. package/dist/embedded-transaction.d.ts +75 -8
  66. package/dist/embedded-transaction.d.ts.map +1 -1
  67. package/dist/embedded-transaction.js +124 -18
  68. package/dist/error-codes.d.ts +2 -0
  69. package/dist/error-codes.d.ts.map +1 -1
  70. package/dist/error-codes.js +2 -0
  71. package/dist/feltdb.d.ts +1 -11
  72. package/dist/feltdb.d.ts.map +1 -1
  73. package/dist/file-db.d.ts +6 -0
  74. package/dist/file-db.d.ts.map +1 -1
  75. package/dist/file-db.js +43 -11
  76. package/dist/http-db.d.ts +13 -0
  77. package/dist/http-db.d.ts.map +1 -1
  78. package/dist/http-db.js +33 -4
  79. package/dist/indexeddb-db.d.ts.map +1 -1
  80. package/dist/indexeddb-db.js +65 -41
  81. package/dist/memory-db.d.ts.map +1 -1
  82. package/dist/memory-db.js +29 -8
  83. package/dist/studio/{KeyManagementPanel-DZuSeWBK.js → KeyManagementPanel-skUNnKRw.js} +36 -28
  84. package/dist/studio/app.d.ts +8 -1
  85. package/dist/studio/app.d.ts.map +1 -1
  86. package/dist/studio/components/KeyManagementPanel.d.ts.map +1 -1
  87. package/dist/studio/components/KeyManagementPanel.js +1 -1
  88. package/dist/studio/components/RouteErrorBoundary.d.ts +5 -0
  89. package/dist/studio/components/RouteErrorBoundary.d.ts.map +1 -0
  90. package/dist/studio/components/index.js +2 -2
  91. package/dist/studio/{components-Bivm2lX6.js → components-BSGmHfif.js} +6 -6
  92. package/dist/studio/control-plane/Agents.d.ts.map +1 -1
  93. package/dist/studio/control-plane/Data.d.ts.map +1 -1
  94. package/dist/studio/control-plane/Instance.d.ts +4 -1
  95. package/dist/studio/control-plane/Instance.d.ts.map +1 -1
  96. package/dist/studio/control-plane/Operations.d.ts +4 -1
  97. package/dist/studio/control-plane/Operations.d.ts.map +1 -1
  98. package/dist/studio/control-plane/Overview.d.ts.map +1 -1
  99. package/dist/studio/control-plane/Schema.d.ts.map +1 -1
  100. package/dist/studio/control-plane/context.d.ts +4 -1
  101. package/dist/studio/control-plane/context.d.ts.map +1 -1
  102. package/dist/studio/control-plane/index.js +2 -2
  103. package/dist/studio/control-plane/primitives.d.ts +24 -0
  104. package/dist/studio/control-plane/primitives.d.ts.map +1 -1
  105. package/dist/studio/{control-plane-CLm69XwX.js → control-plane-CaBA7Rgv.js} +1374 -1298
  106. package/dist/studio/index.d.ts +1 -0
  107. package/dist/studio/index.d.ts.map +1 -1
  108. package/dist/studio/index.js +415 -237
  109. package/dist/studio/navigation.d.ts +57 -0
  110. package/dist/studio/navigation.d.ts.map +1 -0
  111. package/dist/studio/studio.css +1 -1
  112. package/dist/studio/utils/control-plane-api.d.ts +2 -0
  113. package/dist/studio/utils/control-plane-api.d.ts.map +1 -1
  114. package/dist/studio/utils/control-plane-api.js +9 -4
  115. package/dist/studio/utils/service-api.d.ts.map +1 -1
  116. package/dist/studio/utils/service-api.js +5 -4
  117. package/dist/studio-app/assets/{feltdb_wasm-DACcD-zs.js → feltdb_wasm-3hDngl1h.js} +1 -1
  118. package/dist/studio-app/assets/feltdb_wasm_bg-CFQT4gCy.wasm +0 -0
  119. package/dist/studio-app/assets/index-Dql1hS5m.js +30 -0
  120. package/dist/studio-app/assets/{index-CiC8VY-a.css → index-vi21oEQa.css} +1 -1
  121. package/dist/studio-app/index.html +2 -2
  122. package/dist/telemetry.d.ts +1 -0
  123. package/dist/telemetry.d.ts.map +1 -1
  124. package/dist/telemetry.js +4 -3
  125. package/dist/transaction-identity.d.ts +62 -0
  126. package/dist/transaction-identity.d.ts.map +1 -0
  127. package/dist/transaction-identity.js +138 -0
  128. package/dist/transaction.d.ts +50 -5
  129. package/dist/transaction.d.ts.map +1 -1
  130. package/dist/transaction.js +28 -0
  131. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  132. package/dist/wasm/package.json +1 -1
  133. package/dist/workspace/local-development-authority.d.ts +19 -0
  134. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  135. package/dist/workspace/local-development-authority.js +335 -15
  136. package/package.json +1 -1
  137. package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
  138. package/dist/studio-app/assets/feltdb_wasm_bg-BejThWID.wasm +0 -0
  139. package/dist/studio-app/assets/index-CoZaxnlm.js +0 -30
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FeltDB Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -5,7 +5,18 @@ second state store: every HTTP mutation calls `FeltDb::insert`, `FeltDb::update`
5
5
  `FeltDb::delete`, producing the same operations, vector-clock updates, durable log entries, and
6
6
  reactive events as an embedded mutation.
7
7
 
8
- Run it with:
8
+ Install it from crates.io (any Rust 1.96+ toolchain):
9
+
10
+ ```sh
11
+ cargo install feltdb-server --locked
12
+ feltdb-server --host 127.0.0.1 --port 7700 --namespace my-app --data ./data/feltdb.log
13
+ ```
14
+
15
+ The command line and the HTTP API are the supported contract. The library half of this crate exposes
16
+ the server's building blocks without a compatibility promise; for embedded state in your own process
17
+ depend on [`feltdb`](https://crates.io/crates/feltdb) instead.
18
+
19
+ From a checkout of the repository, run it with:
9
20
 
10
21
  ```sh
11
22
  cargo run -p feltdb-server -- \
@@ -0,0 +1,511 @@
1
+ //! Authority, completion, and failure rules for runs the autonomous worker executes.
2
+ //!
3
+ //! Agent and workflow runs are created by an authenticated request and executed
4
+ //! later by the worker, which runs with the node's own authority. Without the
5
+ //! rules here, every capability a run invokes would read with that node
6
+ //! authority, and a run would end `completed` whenever a capability returned.
7
+ //!
8
+ //! - **Authority.** When a run is created, the server signs a grant that binds
9
+ //! the requesting principal's scopes to that one run ([`issue_run_authority`]).
10
+ //! The worker verifies it before executing ([`verify_run_authority`]) and
11
+ //! authorizes every collection read against it. The grant is signed with the
12
+ //! server's existing [`GrantSigner`], so a run record written or replayed by
13
+ //! anything other than the server — including a grant copied from another
14
+ //! run — is refused rather than trusted.
15
+ //! - **Completion.** An agent run completes only when criteria declared at
16
+ //! creation are satisfied by the result the runtime itself observed
17
+ //! ([`CompletionCriteria`]). The goal text is never evaluated. A run without
18
+ //! verifiable criteria ends `failed` with [`COMPLETION_UNVERIFIABLE`]; nothing
19
+ //! a client posts can complete a run.
20
+ //! - **Failure.** Permanent errors end the run at once; transient errors retry
21
+ //! at most [`max_run_attempts`] times, under the workload retry policy's bound.
22
+
23
+ use std::collections::BTreeSet;
24
+
25
+ use feltdb::authorization::{
26
+ Grant, GrantConstraints, GrantSigner, GrantStatus, SignedGrant, Subject,
27
+ AUTHORIZATION_CONTRACT_VERSION,
28
+ };
29
+ use feltdb::workload::WorkloadRetryPolicy;
30
+ use serde::{Deserialize, Serialize};
31
+ use serde_json::{json, Value};
32
+
33
+ /// The issuer recorded on every run authority grant.
34
+ pub const RUN_AUTHORITY_ISSUER: &str = "feltdb-run-authority";
35
+ /// How long a run's delegated authority stays valid after the run is created.
36
+ ///
37
+ /// A run that has not finished by then fails with [`AUTHORIZATION_DENIED`]
38
+ /// rather than executing on a delegation its caller may no longer intend.
39
+ pub const RUN_AUTHORITY_TTL_SECONDS: i64 = 24 * 60 * 60;
40
+
41
+ /// Run statuses written by the runtime.
42
+ pub const STATUS_READY: &str = "ready";
43
+ pub const STATUS_RUNNING: &str = "running";
44
+ pub const STATUS_COMPLETED: &str = "completed";
45
+ pub const STATUS_FAILED: &str = "failed";
46
+ /// Non-terminal annotations an external participant may set on an agent run.
47
+ pub const STATUS_DELIBERATING: &str = "deliberating";
48
+ pub const STATUS_ACTING: &str = "acting";
49
+
50
+ /// Stable failure codes stored in a failed run's `failure.code`.
51
+ pub const AUTHORIZATION_DENIED: &str = "AUTHORIZATION_DENIED";
52
+ pub const AGENT_NOT_FOUND: &str = "AGENT_NOT_FOUND";
53
+ pub const CAPABILITY_UNKNOWN: &str = "CAPABILITY_UNKNOWN";
54
+ pub const CAPABILITY_INPUT_INVALID: &str = "CAPABILITY_INPUT_INVALID";
55
+ pub const RETRY_EXHAUSTED: &str = "RETRY_EXHAUSTED";
56
+ pub const COMPLETION_CRITERIA_UNMET: &str = "COMPLETION_CRITERIA_UNMET";
57
+ pub const COMPLETION_UNVERIFIABLE: &str = "COMPLETION_UNVERIFIABLE";
58
+ pub const RUN_INVALID: &str = "RUN_INVALID";
59
+
60
+ /// Which kind of run a grant was issued for. It is part of the grant subject,
61
+ /// so an agent run's authority cannot be presented by a workflow run.
62
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
63
+ pub enum RunKind {
64
+ Agent,
65
+ Workflow,
66
+ }
67
+
68
+ impl RunKind {
69
+ fn subject(self, run_id: &str) -> Subject {
70
+ match self {
71
+ Self::Agent => Subject::Agent(format!("agent-run:{run_id}")),
72
+ Self::Workflow => Subject::Workload(format!("workflow-run:{run_id}")),
73
+ }
74
+ }
75
+ }
76
+
77
+ /// The principal a run acts for, as the server authenticated it.
78
+ #[derive(Debug, Clone, PartialEq, Eq)]
79
+ pub struct RunPrincipal {
80
+ pub id: String,
81
+ /// `service`, `worker`, or `human`, exactly as the authenticator decided.
82
+ pub kind: String,
83
+ pub scopes: BTreeSet<String>,
84
+ }
85
+
86
+ /// Sign the authority a run executes under.
87
+ pub fn issue_run_authority(
88
+ signer: &GrantSigner,
89
+ kind: RunKind,
90
+ run_id: &str,
91
+ tenant_id: &str,
92
+ principal: &RunPrincipal,
93
+ issued_at: i64,
94
+ ) -> Result<SignedGrant, String> {
95
+ let mut constraints = GrantConstraints::default();
96
+ constraints
97
+ .subject_attributes
98
+ .insert("principal_id".into(), principal.id.clone());
99
+ constraints
100
+ .subject_attributes
101
+ .insert("principal_kind".into(), principal.kind.clone());
102
+ signer
103
+ .sign(Grant {
104
+ contract_version: AUTHORIZATION_CONTRACT_VERSION,
105
+ id: format!("run-authority:{run_id}"),
106
+ issuer: Subject::Service(RUN_AUTHORITY_ISSUER.into()),
107
+ subject: kind.subject(run_id),
108
+ tenant_id: tenant_id.into(),
109
+ application_id: tenant_id.into(),
110
+ capabilities: principal.scopes.clone(),
111
+ resources: Vec::new(),
112
+ constraints,
113
+ issued_at,
114
+ expires_at: issued_at.saturating_add(RUN_AUTHORITY_TTL_SECONDS),
115
+ parent_grant: None,
116
+ revision: 1,
117
+ status: GrantStatus::Active,
118
+ })
119
+ .map_err(|error| error.message)
120
+ }
121
+
122
+ /// Verify a run's stored authority and return the principal it delegates.
123
+ ///
124
+ /// Every refusal is [`AUTHORIZATION_DENIED`] and permanent: retrying cannot
125
+ /// change a signature, a binding, or an expiry.
126
+ pub fn verify_run_authority(
127
+ signer: &GrantSigner,
128
+ stored: &Value,
129
+ kind: RunKind,
130
+ run_id: &str,
131
+ tenant_id: &str,
132
+ now: i64,
133
+ ) -> Result<RunPrincipal, RunFailure> {
134
+ let denied = |message: &str| RunFailure::permanent(AUTHORIZATION_DENIED, message);
135
+ if stored.is_null() {
136
+ return Err(denied("run carries no delegated authority"));
137
+ }
138
+ let signed: SignedGrant =
139
+ serde_json::from_value(stored.clone()).map_err(|_| denied("run authority is malformed"))?;
140
+ signer
141
+ .verify_signature(&signed)
142
+ .map_err(|_| denied("run authority signature is invalid"))?;
143
+ let grant = &signed.grant;
144
+ if grant.issuer != Subject::Service(RUN_AUTHORITY_ISSUER.into())
145
+ || grant.subject != kind.subject(run_id)
146
+ || grant.id != format!("run-authority:{run_id}")
147
+ {
148
+ return Err(denied("run authority was issued for a different run"));
149
+ }
150
+ if grant.tenant_id != tenant_id || grant.application_id != tenant_id {
151
+ return Err(denied("run authority belongs to a different tenant"));
152
+ }
153
+ if grant.status != GrantStatus::Active || now >= grant.expires_at || now < grant.issued_at {
154
+ return Err(denied("run authority is not current"));
155
+ }
156
+ let attribute = |name: &str| grant.constraints.subject_attributes.get(name).cloned();
157
+ let (Some(id), Some(kind)) = (attribute("principal_id"), attribute("principal_kind")) else {
158
+ return Err(denied("run authority names no principal"));
159
+ };
160
+ Ok(RunPrincipal {
161
+ id,
162
+ kind,
163
+ scopes: grant.capabilities.clone(),
164
+ })
165
+ }
166
+
167
+ /// A machine-checkable condition the runtime evaluates against the result it
168
+ /// observed. The set is closed: anything else is refused when the run starts.
169
+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
170
+ #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
171
+ pub enum CompletionCriteria {
172
+ /// The result is an array with at least `min` (and at most `max`) items.
173
+ ResultCount { min: usize, max: Option<usize> },
174
+ /// The result is an array with an item whose `field` equals `equals`.
175
+ ResultContains { field: String, equals: Value },
176
+ }
177
+
178
+ impl CompletionCriteria {
179
+ /// Reject criteria that would be satisfied by any outcome.
180
+ pub fn validate(&self) -> Result<(), String> {
181
+ match self {
182
+ Self::ResultCount { min, max } => {
183
+ if *min == 0 {
184
+ return Err("result_count requires min >= 1; a criterion every outcome satisfies verifies nothing".into());
185
+ }
186
+ if max.is_some_and(|max| max < *min) {
187
+ return Err("result_count max must be >= min".into());
188
+ }
189
+ }
190
+ Self::ResultContains { field, .. } => {
191
+ if field.is_empty() || field.len() > 128 {
192
+ return Err("result_contains requires a field name of 1..128 characters".into());
193
+ }
194
+ }
195
+ }
196
+ Ok(())
197
+ }
198
+
199
+ /// Whether `result`, as observed by the runtime, satisfies the criterion.
200
+ pub fn satisfied_by(&self, result: &Value) -> bool {
201
+ let Some(items) = result.as_array() else {
202
+ return false;
203
+ };
204
+ match self {
205
+ Self::ResultCount { min, max } => {
206
+ items.len() >= *min && max.is_none_or(|max| items.len() <= max)
207
+ }
208
+ Self::ResultContains { field, equals } => {
209
+ items.iter().any(|item| item.get(field) == Some(equals))
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ /// Why a run did not complete. `retryable` is decided by the runtime, never by
216
+ /// the run record.
217
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
218
+ pub struct RunFailure {
219
+ pub code: &'static str,
220
+ pub message: String,
221
+ pub retryable: bool,
222
+ }
223
+
224
+ impl RunFailure {
225
+ pub fn permanent(code: &'static str, message: impl Into<String>) -> Self {
226
+ Self {
227
+ code,
228
+ message: message.into(),
229
+ retryable: false,
230
+ }
231
+ }
232
+
233
+ pub fn to_record(&self, attempt: u64) -> Value {
234
+ json!({ "code": self.code, "message": self.message, "retryable": self.retryable, "attempt": attempt })
235
+ }
236
+ }
237
+
238
+ /// Classify a capability error by its HTTP status.
239
+ ///
240
+ /// A capability reads committed state and nothing else, so a client error
241
+ /// (bad input, missing record, refused authority) fails the same way on every
242
+ /// attempt and is permanent. Only a server-side error can succeed on retry.
243
+ pub fn classify_capability_error(status: u16, message: &str) -> RunFailure {
244
+ match status {
245
+ 401 | 403 => RunFailure::permanent(AUTHORIZATION_DENIED, message),
246
+ 404 => RunFailure::permanent(CAPABILITY_UNKNOWN, message),
247
+ 400..=499 => RunFailure::permanent(CAPABILITY_INPUT_INVALID, message),
248
+ _ => RunFailure {
249
+ code: "CAPABILITY_FAILED",
250
+ message: message.into(),
251
+ retryable: true,
252
+ },
253
+ }
254
+ }
255
+
256
+ /// The scopes a run may use: those granted when it was created that the key
257
+ /// still holds now. `*` on either side stands for the other side's set, so a
258
+ /// wildcard never widens a narrowed key, and an empty set authorizes nothing.
259
+ pub fn effective_scopes(
260
+ granted: &BTreeSet<String>,
261
+ current: &BTreeSet<String>,
262
+ ) -> BTreeSet<String> {
263
+ if granted.contains("*") {
264
+ current.clone()
265
+ } else if current.contains("*") {
266
+ granted.clone()
267
+ } else {
268
+ granted.intersection(current).cloned().collect()
269
+ }
270
+ }
271
+
272
+ /// The most attempts a run gets, shared with durable workloads.
273
+ pub fn max_run_attempts() -> u64 {
274
+ u64::from(WorkloadRetryPolicy::default().max_attempts)
275
+ }
276
+
277
+ /// Delay before attempt `next_attempt` (2 or later), exponential from the
278
+ /// workload policy's initial delay and capped at its maximum.
279
+ pub fn retry_delay_ms(next_attempt: u64) -> u64 {
280
+ let policy = WorkloadRetryPolicy::default();
281
+ let exponent = next_attempt.saturating_sub(2).min(20) as u32;
282
+ policy
283
+ .initial_delay_ms
284
+ .saturating_mul(2u64.saturating_pow(exponent))
285
+ .min(policy.max_delay_ms)
286
+ }
287
+
288
+ /// Collections reserved for the server. No capability reads them.
289
+ pub fn is_system_collection(collection: &str) -> bool {
290
+ collection.starts_with('_')
291
+ }
292
+
293
+ /// Collections the runtime alone writes. A raw write could otherwise set a
294
+ /// run's status or retry state directly, or — for durable work, which the
295
+ /// workload store loads from this collection at startup — inject a workload
296
+ /// whose authorization snapshot was never verified.
297
+ pub fn is_runtime_owned_collection(collection: &str) -> bool {
298
+ matches!(
299
+ collection,
300
+ "_flow_agent_runs" | "_flow_workflow_runs" | feltdb::workload::DURABLE_WORK_COLLECTION
301
+ )
302
+ }
303
+
304
+ pub fn is_terminal(status: &str) -> bool {
305
+ matches!(status, STATUS_COMPLETED | STATUS_FAILED)
306
+ }
307
+
308
+ /// Whether the worker may claim an agent run in `status`.
309
+ ///
310
+ /// External annotations (`deliberating`, `acting`) leave a run eligible, so a
311
+ /// client posting an observation cannot strand it outside the worker's reach.
312
+ pub fn agent_run_claimable(status: &str, lease_expired: bool) -> bool {
313
+ match status {
314
+ STATUS_READY | STATUS_DELIBERATING | STATUS_ACTING => true,
315
+ STATUS_RUNNING => lease_expired,
316
+ _ => false,
317
+ }
318
+ }
319
+
320
+ #[cfg(test)]
321
+ mod tests {
322
+ use super::*;
323
+
324
+ fn signer(seed: u8) -> GrantSigner {
325
+ GrantSigner::new("feltdb-primary-v1", vec![seed; 32]).unwrap()
326
+ }
327
+
328
+ fn principal() -> RunPrincipal {
329
+ RunPrincipal {
330
+ id: "key-1".into(),
331
+ kind: "service".into(),
332
+ scopes: ["agents:run".to_string(), "state:read".to_string()].into(),
333
+ }
334
+ }
335
+
336
+ fn issued(kind: RunKind, run: &str) -> Value {
337
+ serde_json::to_value(
338
+ issue_run_authority(&signer(7), kind, run, "tenant", &principal(), 1_000).unwrap(),
339
+ )
340
+ .unwrap()
341
+ }
342
+
343
+ #[test]
344
+ fn a_run_authority_round_trips_for_its_own_run() {
345
+ let verified = verify_run_authority(
346
+ &signer(7),
347
+ &issued(RunKind::Agent, "agent-1"),
348
+ RunKind::Agent,
349
+ "agent-1",
350
+ "tenant",
351
+ 1_001,
352
+ )
353
+ .unwrap();
354
+ assert_eq!(verified, principal());
355
+ }
356
+
357
+ #[test]
358
+ fn a_run_authority_is_refused_for_any_other_binding() {
359
+ let grant = issued(RunKind::Agent, "agent-1");
360
+ let refuse = |value: &Value, kind, run: &str, tenant: &str, now| {
361
+ let failure =
362
+ verify_run_authority(&signer(7), value, kind, run, tenant, now).unwrap_err();
363
+ assert_eq!(failure.code, AUTHORIZATION_DENIED);
364
+ assert!(!failure.retryable);
365
+ };
366
+ refuse(&grant, RunKind::Agent, "agent-2", "tenant", 1_001);
367
+ refuse(&grant, RunKind::Workflow, "agent-1", "tenant", 1_001);
368
+ refuse(&grant, RunKind::Agent, "agent-1", "other-tenant", 1_001);
369
+ refuse(
370
+ &grant,
371
+ RunKind::Agent,
372
+ "agent-1",
373
+ "tenant",
374
+ 1_000 + RUN_AUTHORITY_TTL_SECONDS,
375
+ );
376
+ refuse(&Value::Null, RunKind::Agent, "agent-1", "tenant", 1_001);
377
+ assert_eq!(
378
+ verify_run_authority(
379
+ &signer(8),
380
+ &grant,
381
+ RunKind::Agent,
382
+ "agent-1",
383
+ "tenant",
384
+ 1_001
385
+ )
386
+ .unwrap_err()
387
+ .code,
388
+ AUTHORIZATION_DENIED,
389
+ "a grant from another signer is refused"
390
+ );
391
+ }
392
+
393
+ #[test]
394
+ fn widening_a_signed_run_authority_breaks_its_signature() {
395
+ let mut grant = issued(RunKind::Agent, "agent-1");
396
+ grant["grant"]["capabilities"] = json!(["*"]);
397
+ assert!(verify_run_authority(
398
+ &signer(7),
399
+ &grant,
400
+ RunKind::Agent,
401
+ "agent-1",
402
+ "tenant",
403
+ 1_001
404
+ )
405
+ .is_err());
406
+ let mut grant = issued(RunKind::Agent, "agent-1");
407
+ grant["grant"]["constraints"]["subject_attributes"]["principal_id"] = json!("admin");
408
+ assert!(verify_run_authority(
409
+ &signer(7),
410
+ &grant,
411
+ RunKind::Agent,
412
+ "agent-1",
413
+ "tenant",
414
+ 1_001
415
+ )
416
+ .is_err());
417
+ }
418
+
419
+ #[test]
420
+ fn completion_criteria_verify_the_observed_result_only() {
421
+ let count = CompletionCriteria::ResultCount { min: 1, max: None };
422
+ assert!(count.satisfied_by(&json!([{ "id": "a" }])));
423
+ assert!(!count.satisfied_by(&json!([])));
424
+ assert!(!count.satisfied_by(&json!({ "items": [1] })));
425
+ let bounded = CompletionCriteria::ResultCount {
426
+ min: 1,
427
+ max: Some(1),
428
+ };
429
+ assert!(!bounded.satisfied_by(&json!([1, 2])));
430
+ let contains = CompletionCriteria::ResultContains {
431
+ field: "id".into(),
432
+ equals: json!("doc-1"),
433
+ };
434
+ assert!(contains.satisfied_by(&json!([{ "id": "doc-1" }])));
435
+ assert!(!contains.satisfied_by(&json!([{ "id": "doc-2" }])));
436
+ assert!(CompletionCriteria::ResultCount { min: 0, max: None }
437
+ .validate()
438
+ .is_err());
439
+ assert!(CompletionCriteria::ResultCount {
440
+ min: 2,
441
+ max: Some(1)
442
+ }
443
+ .validate()
444
+ .is_err());
445
+ assert!(
446
+ serde_json::from_value::<CompletionCriteria>(json!({ "kind": "goal_achieved" }))
447
+ .is_err()
448
+ );
449
+ assert!(serde_json::from_value::<CompletionCriteria>(
450
+ json!({ "kind": "result_count", "min": 1, "trust": true })
451
+ )
452
+ .is_err());
453
+ }
454
+
455
+ #[test]
456
+ fn effective_scopes_never_exceed_either_side() {
457
+ let set = |values: &[&str]| {
458
+ values
459
+ .iter()
460
+ .map(|v| v.to_string())
461
+ .collect::<BTreeSet<_>>()
462
+ };
463
+ let wide = set(&["*"]);
464
+ let reader = set(&["agents:run", "state:read"]);
465
+ let runner = set(&["agents:run"]);
466
+ assert_eq!(
467
+ effective_scopes(&wide, &runner),
468
+ runner,
469
+ "a wildcard grant narrows to the current key"
470
+ );
471
+ assert_eq!(
472
+ effective_scopes(&runner, &wide),
473
+ runner,
474
+ "a wildcard key does not widen the grant"
475
+ );
476
+ assert_eq!(effective_scopes(&reader, &runner), runner);
477
+ assert_eq!(effective_scopes(&wide, &wide), wide);
478
+ assert!(effective_scopes(&reader, &set(&[])).is_empty());
479
+ assert!(effective_scopes(&set(&[]), &wide).is_empty());
480
+ assert!(effective_scopes(&set(&["state:read"]), &set(&["state:write"])).is_empty());
481
+ }
482
+
483
+ #[test]
484
+ fn only_server_errors_are_retryable() {
485
+ assert!(!classify_capability_error(403, "denied").retryable);
486
+ assert_eq!(
487
+ classify_capability_error(404, "missing").code,
488
+ CAPABILITY_UNKNOWN
489
+ );
490
+ assert_eq!(
491
+ classify_capability_error(400, "bad").code,
492
+ CAPABILITY_INPUT_INVALID
493
+ );
494
+ assert!(classify_capability_error(500, "storage").retryable);
495
+ assert_eq!(max_run_attempts(), 3);
496
+ assert_eq!(retry_delay_ms(2), 1_000);
497
+ assert_eq!(retry_delay_ms(3), 2_000);
498
+ assert_eq!(retry_delay_ms(99), 300_000);
499
+ }
500
+
501
+ #[test]
502
+ fn terminal_runs_are_never_claimable() {
503
+ for status in [STATUS_COMPLETED, STATUS_FAILED] {
504
+ assert!(is_terminal(status));
505
+ assert!(!agent_run_claimable(status, true));
506
+ }
507
+ assert!(agent_run_claimable(STATUS_DELIBERATING, false));
508
+ assert!(!agent_run_claimable(STATUS_RUNNING, false));
509
+ assert!(agent_run_claimable(STATUS_RUNNING, true));
510
+ }
511
+ }
@@ -181,6 +181,34 @@ impl KeyStore {
181
181
  })
182
182
  }
183
183
 
184
+ /// The current scopes and subject type of an active key, by id.
185
+ ///
186
+ /// Work executed after its request — an autonomous run — re-reads its
187
+ /// principal here, so a key revoked or narrowed since the run was created
188
+ /// is not honoured at its old breadth. `None` when the key is unknown,
189
+ /// revoked, or not valid for `namespace`.
190
+ pub fn active_scopes(&self, key_id: &str, namespace: &str) -> Option<(Vec<String>, String)> {
191
+ let records = self.records.read().ok()?;
192
+ records
193
+ .iter()
194
+ .find(|record| {
195
+ record.id == key_id
196
+ && !record.revoked
197
+ && record
198
+ .namespaces
199
+ .iter()
200
+ .any(|allowed| allowed == "*" || allowed == namespace)
201
+ })
202
+ .map(|record| {
203
+ let kind = if record.worker_id.is_some() {
204
+ "worker"
205
+ } else {
206
+ "service"
207
+ };
208
+ (record.scopes.clone(), kind.to_string())
209
+ })
210
+ }
211
+
184
212
  pub fn create(
185
213
  &self,
186
214
  name: String,
@@ -411,11 +411,15 @@ fn copy_source(
411
411
  if let Some(parent) = destination.parent() {
412
412
  fs::create_dir_all(parent)?;
413
413
  }
414
- fs::copy(source, destination)?;
414
+ // Record what was copied, not what the source measured before the
415
+ // copy. An online backup copies files the server is still appending
416
+ // to (the audit log): a length taken from the source before copying
417
+ // would disagree with the copy's hash and fail verification.
418
+ let copied = fs::copy(source, destination)?;
415
419
  sync_file(destination)?;
416
420
  entries.push(BackupEntry {
417
421
  path: relative.into(),
418
- bytes: metadata.len(),
422
+ bytes: copied,
419
423
  sha256: hash_file(destination)?,
420
424
  });
421
425
  }
@@ -1,3 +1,24 @@
1
+ //! # feltdb-server
2
+ //!
3
+ //! The FeltDB server: the managed FeltDB authority over HTTP. Install and run it with
4
+ //!
5
+ //! ```sh
6
+ //! cargo install feltdb-server --locked
7
+ //! feltdb-server --host 127.0.0.1 --port 7700 --namespace my-app --data ./data/feltdb.log
8
+ //! ```
9
+ //!
10
+ //! Its supported contract is the binary's command line and its HTTP API (`feltdb.public.substrate@1.0`
11
+ //! and the `/v1` Service API, specified in the repository's
12
+ //! [`docs/reference/public-api.md`](https://github.com/rkendel1/flow_db/blob/main/docs/reference/public-api.md)).
13
+ //!
14
+ //! The modules below are the server's building blocks. They are public so the binary and the
15
+ //! repository's tests can use them, and **no compatibility promise covers them**: any release may
16
+ //! change them. For embedded state in your own process, depend on [`feltdb`] directly; it is
17
+ //! re-exported here at the exact version this server is built against.
18
+
19
+ pub use feltdb;
20
+
21
+ pub mod agent_execution;
1
22
  pub mod app_state;
2
23
  pub mod application_contract;
3
24
  pub mod artifacts;