create-feltdb 0.11.2 → 0.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package-versions.js +1 -1
- package/dist/server-source/crates/feltdb/src/lib.rs +31 -30
- package/dist/server-source/crates/feltdb/src/semantic_decision.rs +1917 -0
- package/dist/server-source/crates/feltdb/tests/semantic_decision.rs +1165 -0
- package/dist/server-source/crates/feltdb-server/src/main.rs +232 -95
- package/dist/server-source/crates/feltdb-wasm/src/lib.rs +73 -8
- package/package.json +1 -1
|
@@ -42,6 +42,7 @@ use feltdb::{
|
|
|
42
42
|
Grant, GrantSigner, GrantStore, Subject as GrantSubject,
|
|
43
43
|
},
|
|
44
44
|
cardinality_endpoint::{CardinalityContext, CardinalityDiagnosticResponse},
|
|
45
|
+
execute_decision_transport,
|
|
45
46
|
policy_evaluation::{
|
|
46
47
|
Actor, AuthorizationState, PolicyContext, PolicyEvaluator, PolicySubject,
|
|
47
48
|
RecordAuthorizationContext,
|
|
@@ -61,7 +62,8 @@ use feltdb::{
|
|
|
61
62
|
WorkerRegistration,
|
|
62
63
|
},
|
|
63
64
|
workload::{CreateWorkload, WorkloadStore},
|
|
64
|
-
AtomicMutation, DatabaseSnapshot,
|
|
65
|
+
AtomicMutation, DatabaseSnapshot, DecisionError, DecisionPrincipal,
|
|
66
|
+
DecisionTransportExecutionRequest, FeltDb, FlowError, JsonCasResult, Operation,
|
|
65
67
|
PeerAdvertisement, PeerId, RecordPrecondition, StateRevision, StateTriggerStore, StoredRow,
|
|
66
68
|
};
|
|
67
69
|
use feltdb_server::{
|
|
@@ -77,8 +79,8 @@ use feltdb_server::{
|
|
|
77
79
|
server_artifacts, verify_bundle,
|
|
78
80
|
},
|
|
79
81
|
causal::{
|
|
80
|
-
decode_cursor, decode_scoped_cursor, encode_cursor, encode_scoped_cursor,
|
|
81
|
-
CanonicalEventAuthority,
|
|
82
|
+
decode_cursor, decode_scoped_cursor, encode_cursor, encode_scoped_cursor,
|
|
83
|
+
CanonicalEventAuthority, CausalEvent,
|
|
82
84
|
},
|
|
83
85
|
certification::default_report,
|
|
84
86
|
clock::LeaseClock,
|
|
@@ -159,14 +161,13 @@ fn api_error_body(status: StatusCode, message: String) -> Value {
|
|
|
159
161
|
// that code. Other forbidden details retain the non-disclosing canonical
|
|
160
162
|
// `FORBIDDEN` wire contract.
|
|
161
163
|
let explicit_code = object.get("code").and_then(Value::as_str);
|
|
162
|
-
let resolved_code =
|
|
163
|
-
|| explicit_code == Some("CURSOR_SCOPE_VIOLATION")
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
.to_string();
|
|
164
|
+
let resolved_code =
|
|
165
|
+
if status != StatusCode::FORBIDDEN || explicit_code == Some("CURSOR_SCOPE_VIOLATION") {
|
|
166
|
+
explicit_code.unwrap_or(code)
|
|
167
|
+
} else {
|
|
168
|
+
code
|
|
169
|
+
}
|
|
170
|
+
.to_string();
|
|
170
171
|
object.insert("error".into(), json!(resolved_code));
|
|
171
172
|
object.insert("code".into(), json!(resolved_code));
|
|
172
173
|
object.entry("message").or_insert_with(|| {
|
|
@@ -643,6 +644,42 @@ fn grant_subject(principal: &Principal) -> GrantSubject {
|
|
|
643
644
|
_ => GrantSubject::Service(id),
|
|
644
645
|
}
|
|
645
646
|
}
|
|
647
|
+
|
|
648
|
+
fn decision_subject(principal: &Principal) -> GrantSubject {
|
|
649
|
+
let id = principal
|
|
650
|
+
.identity_id
|
|
651
|
+
.clone()
|
|
652
|
+
.unwrap_or_else(|| principal.key_id.clone());
|
|
653
|
+
match principal.subject_type.as_str() {
|
|
654
|
+
"human" | "user" => GrantSubject::Human(id),
|
|
655
|
+
"agent" => GrantSubject::Agent(id),
|
|
656
|
+
"workload" => GrantSubject::Workload(id),
|
|
657
|
+
"worker" => GrantSubject::Worker(id),
|
|
658
|
+
"connection" => GrantSubject::Connection(id),
|
|
659
|
+
_ => GrantSubject::Service(id),
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
fn bind_decision_authorization_principal(
|
|
664
|
+
principal: &Principal,
|
|
665
|
+
mut input: DecisionTransportExecutionRequest,
|
|
666
|
+
) -> Result<DecisionTransportExecutionRequest, ApiError> {
|
|
667
|
+
if let Some(authorization) = input.authorization.as_mut() {
|
|
668
|
+
let requested = authorization.principal.as_ref().ok_or_else(|| {
|
|
669
|
+
semantic_decision_error(DecisionError::new(
|
|
670
|
+
"DECISION_INVALID_RESULT",
|
|
671
|
+
"authorization config requires a principal",
|
|
672
|
+
))
|
|
673
|
+
})?;
|
|
674
|
+
authorization.principal = Some(DecisionPrincipal {
|
|
675
|
+
subject: decision_subject(principal),
|
|
676
|
+
tenant_id: requested.tenant_id.clone(),
|
|
677
|
+
application_id: requested.application_id.clone(),
|
|
678
|
+
capability: "state:read".into(),
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
Ok(input)
|
|
682
|
+
}
|
|
646
683
|
fn authorization_error(error: feltdb::authorization::AuthorizationError) -> ApiError {
|
|
647
684
|
let status = if error.code == "GRANT_INVALID" {
|
|
648
685
|
StatusCode::BAD_REQUEST
|
|
@@ -1477,7 +1514,12 @@ fn worker_error(error: feltdb::worker_mesh::WorkerMeshError) -> ApiError {
|
|
|
1477
1514
|
serde_json::to_string(&error).unwrap_or(error.message),
|
|
1478
1515
|
)
|
|
1479
1516
|
}
|
|
1480
|
-
fn release_worker_claim_projection(
|
|
1517
|
+
fn release_worker_claim_projection(
|
|
1518
|
+
state: &AppState,
|
|
1519
|
+
workload_id: &str,
|
|
1520
|
+
worker_id: &str,
|
|
1521
|
+
claim_id: &str,
|
|
1522
|
+
) {
|
|
1481
1523
|
match state.mesh.lock() {
|
|
1482
1524
|
Ok(mut mesh) => {
|
|
1483
1525
|
if let Err(error) = mesh.release_claim(worker_id, claim_id) {
|
|
@@ -1793,12 +1835,7 @@ async fn complete_workload(
|
|
|
1793
1835
|
unix_seconds_i64(),
|
|
1794
1836
|
)
|
|
1795
1837
|
.map_err(workload_error)?;
|
|
1796
|
-
release_worker_claim_projection(
|
|
1797
|
-
&state,
|
|
1798
|
-
&id,
|
|
1799
|
-
&input.fence.worker_id,
|
|
1800
|
-
&input.fence.claim_id,
|
|
1801
|
-
);
|
|
1838
|
+
release_worker_claim_projection(&state, &id, &input.fence.worker_id, &input.fence.claim_id);
|
|
1802
1839
|
Ok(Json(json!(result)))
|
|
1803
1840
|
}
|
|
1804
1841
|
async fn fail_workload(
|
|
@@ -1822,12 +1859,7 @@ async fn fail_workload(
|
|
|
1822
1859
|
unix_seconds_i64(),
|
|
1823
1860
|
)
|
|
1824
1861
|
.map_err(workload_error)?;
|
|
1825
|
-
release_worker_claim_projection(
|
|
1826
|
-
&state,
|
|
1827
|
-
&id,
|
|
1828
|
-
&input.fence.worker_id,
|
|
1829
|
-
&input.fence.claim_id,
|
|
1830
|
-
);
|
|
1862
|
+
release_worker_claim_projection(&state, &id, &input.fence.worker_id, &input.fence.claim_id);
|
|
1831
1863
|
Ok(Json(json!({"state":status})))
|
|
1832
1864
|
}
|
|
1833
1865
|
async fn confirm_workload_cancelled(
|
|
@@ -3230,25 +3262,33 @@ async fn causal_chain(
|
|
|
3230
3262
|
State(state): State<AppState>,
|
|
3231
3263
|
Path(id): Path<String>,
|
|
3232
3264
|
) -> Result<Json<Value>, ApiError> {
|
|
3233
|
-
Ok(Json(
|
|
3265
|
+
Ok(Json(
|
|
3266
|
+
json!({"events":state.events.list(&id).map_err(control_error)?}),
|
|
3267
|
+
))
|
|
3234
3268
|
}
|
|
3235
3269
|
async fn workload_timeline(
|
|
3236
3270
|
State(state): State<AppState>,
|
|
3237
3271
|
Path(id): Path<String>,
|
|
3238
3272
|
) -> Result<Json<Value>, ApiError> {
|
|
3239
|
-
Ok(Json(
|
|
3273
|
+
Ok(Json(
|
|
3274
|
+
json!({"events":state.events.workload(&id).map_err(control_error)?}),
|
|
3275
|
+
))
|
|
3240
3276
|
}
|
|
3241
3277
|
async fn execution_timeline(
|
|
3242
3278
|
State(state): State<AppState>,
|
|
3243
3279
|
Path(id): Path<String>,
|
|
3244
3280
|
) -> Result<Json<Value>, ApiError> {
|
|
3245
|
-
Ok(Json(
|
|
3281
|
+
Ok(Json(
|
|
3282
|
+
json!({"events":state.events.execution(&id).map_err(control_error)?}),
|
|
3283
|
+
))
|
|
3246
3284
|
}
|
|
3247
3285
|
async fn revision_timeline(
|
|
3248
3286
|
State(state): State<AppState>,
|
|
3249
3287
|
Path(id): Path<String>,
|
|
3250
3288
|
) -> Result<Json<Value>, ApiError> {
|
|
3251
|
-
Ok(Json(
|
|
3289
|
+
Ok(Json(
|
|
3290
|
+
json!({"events":state.events.revision(&id).map_err(control_error)?}),
|
|
3291
|
+
))
|
|
3252
3292
|
}
|
|
3253
3293
|
async fn export_bundle(
|
|
3254
3294
|
State(state): State<AppState>,
|
|
@@ -7579,13 +7619,21 @@ async fn get_state_version(
|
|
|
7579
7619
|
))
|
|
7580
7620
|
}
|
|
7581
7621
|
fn require_bound_tenant(headers: &HeaderMap, tenant: &str) -> Result<(), ApiError> {
|
|
7582
|
-
let Some(bound) = headers.get("feltdb-tenant-id") else {
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7622
|
+
let Some(bound) = headers.get("feltdb-tenant-id") else {
|
|
7623
|
+
return Ok(());
|
|
7624
|
+
};
|
|
7625
|
+
if bound.to_str().ok() == Some(tenant) {
|
|
7626
|
+
return Ok(());
|
|
7627
|
+
}
|
|
7628
|
+
Err(ApiError(
|
|
7629
|
+
StatusCode::FORBIDDEN,
|
|
7630
|
+
json!({
|
|
7631
|
+
"code":"AUTHORITY_SCOPE_MISMATCH",
|
|
7632
|
+
"message":"request tenant does not match the authenticated application authority",
|
|
7633
|
+
"http_status":403
|
|
7634
|
+
})
|
|
7635
|
+
.to_string(),
|
|
7636
|
+
))
|
|
7589
7637
|
}
|
|
7590
7638
|
|
|
7591
7639
|
async fn execute_canonical_query(
|
|
@@ -7643,6 +7691,49 @@ async fn execute_canonical_query(
|
|
|
7643
7691
|
Ok(Json(json!(result)))
|
|
7644
7692
|
}
|
|
7645
7693
|
|
|
7694
|
+
fn semantic_decision_error(error: feltdb::DecisionError) -> ApiError {
|
|
7695
|
+
let status = match error.code.as_str() {
|
|
7696
|
+
"DECISION_RECORD_NOT_FOUND" => StatusCode::NOT_FOUND,
|
|
7697
|
+
"DECISION_STORAGE_ERROR" => StatusCode::SERVICE_UNAVAILABLE,
|
|
7698
|
+
"DECISION_INVALID_RESULT"
|
|
7699
|
+
| "DECISION_INVALID_OPTION"
|
|
7700
|
+
| "DECISION_INVALID_DISTRIBUTION"
|
|
7701
|
+
| "DECISION_CONTEXT_EMPTY" => StatusCode::UNPROCESSABLE_ENTITY,
|
|
7702
|
+
_ => StatusCode::BAD_REQUEST,
|
|
7703
|
+
};
|
|
7704
|
+
ApiError::structured(
|
|
7705
|
+
status,
|
|
7706
|
+
json!({
|
|
7707
|
+
"code": error.code,
|
|
7708
|
+
"message": error.message,
|
|
7709
|
+
"http_status": status.as_u16(),
|
|
7710
|
+
}),
|
|
7711
|
+
)
|
|
7712
|
+
}
|
|
7713
|
+
|
|
7714
|
+
async fn execute_semantic_decision_transport(
|
|
7715
|
+
State(state): State<AppState>,
|
|
7716
|
+
Extension(principal): Extension<Principal>,
|
|
7717
|
+
Json(input): Json<DecisionTransportExecutionRequest>,
|
|
7718
|
+
) -> Result<Json<Value>, ApiError> {
|
|
7719
|
+
let input = bind_decision_authorization_principal(&principal, input)?;
|
|
7720
|
+
let response =
|
|
7721
|
+
execute_decision_transport(&state.db, &input).map_err(semantic_decision_error)?;
|
|
7722
|
+
audit(
|
|
7723
|
+
&state,
|
|
7724
|
+
&principal.key_id,
|
|
7725
|
+
"semantic_decision.execute",
|
|
7726
|
+
&response.evaluation.provenance.cache_key,
|
|
7727
|
+
if response.evaluation.cached {
|
|
7728
|
+
"cache"
|
|
7729
|
+
} else {
|
|
7730
|
+
"runtime"
|
|
7731
|
+
},
|
|
7732
|
+
200,
|
|
7733
|
+
);
|
|
7734
|
+
Ok(Json(json!(response)))
|
|
7735
|
+
}
|
|
7736
|
+
|
|
7646
7737
|
#[derive(serde::Deserialize)]
|
|
7647
7738
|
struct RevisionRecoveryRequest {
|
|
7648
7739
|
#[serde(rename = "applicationId")]
|
|
@@ -7813,11 +7904,15 @@ async fn execute_canonical_transaction(
|
|
|
7813
7904
|
&input.environment,
|
|
7814
7905
|
)?;
|
|
7815
7906
|
if !input.transaction.tenant_id.is_empty() && input.transaction.tenant_id != tenant {
|
|
7816
|
-
return Err(ApiError(
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7907
|
+
return Err(ApiError(
|
|
7908
|
+
StatusCode::FORBIDDEN,
|
|
7909
|
+
json!({
|
|
7910
|
+
"code":"AUTHORITY_SCOPE_MISMATCH",
|
|
7911
|
+
"message":"transaction tenant does not match the bound authority",
|
|
7912
|
+
"http_status":403
|
|
7913
|
+
})
|
|
7914
|
+
.to_string(),
|
|
7915
|
+
));
|
|
7821
7916
|
}
|
|
7822
7917
|
input.transaction.tenant_id = tenant.clone();
|
|
7823
7918
|
input.transaction.application_id = input.application_id.clone();
|
|
@@ -7983,12 +8078,18 @@ fn catchup_status(
|
|
|
7983
8078
|
authority_position: HashMap<String, u64>,
|
|
7984
8079
|
retained_from: HashMap<String, u64>,
|
|
7985
8080
|
) -> CatchupStatus {
|
|
7986
|
-
let lag = authority_position
|
|
7987
|
-
|
|
7988
|
-
|
|
8081
|
+
let lag = authority_position
|
|
8082
|
+
.iter()
|
|
8083
|
+
.map(|(origin, sequence)| {
|
|
8084
|
+
sequence.saturating_sub(peer_position.get(origin).copied().unwrap_or(0))
|
|
8085
|
+
})
|
|
8086
|
+
.sum();
|
|
7989
8087
|
let recovery_required = authority_position.iter().any(|(origin, through)| {
|
|
7990
8088
|
let position = peer_position.get(origin).copied().unwrap_or(0);
|
|
7991
|
-
let floor = retained_from
|
|
8089
|
+
let floor = retained_from
|
|
8090
|
+
.get(origin)
|
|
8091
|
+
.copied()
|
|
8092
|
+
.unwrap_or(through.saturating_add(1));
|
|
7992
8093
|
position < *through && position.saturating_add(1) < floor
|
|
7993
8094
|
});
|
|
7994
8095
|
let catchup_mode = if recovery_required {
|
|
@@ -8005,7 +8106,12 @@ fn catchup_status(
|
|
|
8005
8106
|
authority_position,
|
|
8006
8107
|
retained_from,
|
|
8007
8108
|
lag,
|
|
8008
|
-
recoverability: if recovery_required {
|
|
8109
|
+
recoverability: if recovery_required {
|
|
8110
|
+
"rebootstrap_required"
|
|
8111
|
+
} else {
|
|
8112
|
+
"recoverable"
|
|
8113
|
+
}
|
|
8114
|
+
.into(),
|
|
8009
8115
|
catchup_mode: catchup_mode.into(),
|
|
8010
8116
|
recovery_required,
|
|
8011
8117
|
}
|
|
@@ -8487,7 +8593,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
8487
8593
|
let provider_store = ProviderStore::load(config.data.with_extension("providers.json"))?;
|
|
8488
8594
|
let readiness_probe = config.data.with_extension("readiness");
|
|
8489
8595
|
let tenancy_store = TenancyStore::load(config.data.with_extension("platform.json"))?;
|
|
8490
|
-
let application_store =
|
|
8596
|
+
let application_store =
|
|
8597
|
+
ApplicationStore::load(config.data.with_extension("applications.json"))?;
|
|
8491
8598
|
let principal_authority =
|
|
8492
8599
|
feltdb_server::canonical_principals::CanonicalPrincipalAuthority::new(db.clone());
|
|
8493
8600
|
let legacy_principals = config.data.with_extension("principals.json");
|
|
@@ -8835,7 +8942,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
8835
8942
|
get(control_plane_record_provenance),
|
|
8836
8943
|
)
|
|
8837
8944
|
.route("/v1/control-plane/agents", get(control_plane_agents))
|
|
8838
|
-
.route(
|
|
8945
|
+
.route(
|
|
8946
|
+
"/v1/control-plane/agents/{agent_id}",
|
|
8947
|
+
get(control_plane_agent),
|
|
8948
|
+
)
|
|
8839
8949
|
.route("/v1/state/resources", get(state_resources))
|
|
8840
8950
|
.route(
|
|
8841
8951
|
"/v1/state/resources/{resource}/history",
|
|
@@ -8912,6 +9022,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
8912
9022
|
)
|
|
8913
9023
|
.route("/v1/state/version", get(get_state_version))
|
|
8914
9024
|
.route("/v1/query", axum::routing::post(execute_canonical_query))
|
|
9025
|
+
.route(
|
|
9026
|
+
"/v1/experimental/semantic-decisions",
|
|
9027
|
+
axum::routing::post(execute_semantic_decision_transport),
|
|
9028
|
+
)
|
|
8915
9029
|
.route(
|
|
8916
9030
|
"/v1/transactions",
|
|
8917
9031
|
axum::routing::post(execute_canonical_transaction),
|
|
@@ -9510,6 +9624,7 @@ async fn authenticate(
|
|
|
9510
9624
|
} else if path == "/v1/state/diff"
|
|
9511
9625
|
|| path == "/v1/state/conflicts"
|
|
9512
9626
|
|| path == "/v1/state/reconciliation/plan"
|
|
9627
|
+
|| path == "/v1/experimental/semantic-decisions"
|
|
9513
9628
|
{
|
|
9514
9629
|
// Describing state is reading it. These are POSTs because they take
|
|
9515
9630
|
// three revision identities and a plan, not because they change
|
|
@@ -9647,8 +9762,7 @@ fn audit(
|
|
|
9647
9762
|
// change that policy. What changed is that the failure is no longer
|
|
9648
9763
|
// invisible — `AuditLog::record` has already counted it, timestamped it and
|
|
9649
9764
|
// degraded the audit health state by the time this returns.
|
|
9650
|
-
let _span =
|
|
9651
|
-
feltdb::workload_diagnostics::span(feltdb::workload_diagnostics::Phase::AuditWrite);
|
|
9765
|
+
let _span = feltdb::workload_diagnostics::span(feltdb::workload_diagnostics::Phase::AuditWrite);
|
|
9652
9766
|
if let Err(error) = state.audit.record(AuditEvent {
|
|
9653
9767
|
timestamp_ms: 0,
|
|
9654
9768
|
namespace: &state.namespace,
|
|
@@ -10447,14 +10561,21 @@ async fn sync_pull(
|
|
|
10447
10561
|
let peer_position = request.versions;
|
|
10448
10562
|
let authority_position = state.db.operation_versions()?;
|
|
10449
10563
|
let retained_from = state.db.retained_operation_floors()?;
|
|
10450
|
-
let catchup = catchup_status(
|
|
10564
|
+
let catchup = catchup_status(
|
|
10565
|
+
request.requester,
|
|
10566
|
+
peer_position.clone(),
|
|
10567
|
+
authority_position.clone(),
|
|
10568
|
+
retained_from,
|
|
10569
|
+
);
|
|
10451
10570
|
if catchup.recovery_required {
|
|
10452
10571
|
return Err(ApiError::structured(
|
|
10453
10572
|
StatusCode::CONFLICT,
|
|
10454
|
-
serde_json::to_value(&catchup).unwrap_or_else(|_|
|
|
10455
|
-
|
|
10456
|
-
|
|
10457
|
-
|
|
10573
|
+
serde_json::to_value(&catchup).unwrap_or_else(|_| {
|
|
10574
|
+
json!({
|
|
10575
|
+
"catchup_mode": "snapshot_rebootstrap_required",
|
|
10576
|
+
"recovery_required": true
|
|
10577
|
+
})
|
|
10578
|
+
}),
|
|
10458
10579
|
));
|
|
10459
10580
|
}
|
|
10460
10581
|
let operations = state.db.operations_since(&peer_position)?;
|
|
@@ -12624,7 +12745,7 @@ async fn get_revision(State(state): State<AppState>) -> Result<Json<RevisionResp
|
|
|
12624
12745
|
}))
|
|
12625
12746
|
}
|
|
12626
12747
|
|
|
12627
|
-
const MAX_BOUNDED_QUERY_LIMIT: usize =
|
|
12748
|
+
const MAX_BOUNDED_QUERY_LIMIT: usize = 1_000;
|
|
12628
12749
|
const BOUNDED_CURSOR_TTL_SECONDS: u64 = 300;
|
|
12629
12750
|
|
|
12630
12751
|
#[derive(Clone, Deserialize, Serialize)]
|
|
@@ -12786,7 +12907,11 @@ fn declare_configured_equality_indexes(db: &FeltDb) -> Result<Vec<String>, Strin
|
|
|
12786
12907
|
return Ok(Vec::new());
|
|
12787
12908
|
};
|
|
12788
12909
|
let mut declared = Vec::new();
|
|
12789
|
-
for entry in declaration
|
|
12910
|
+
for entry in declaration
|
|
12911
|
+
.split(',')
|
|
12912
|
+
.map(str::trim)
|
|
12913
|
+
.filter(|entry| !entry.is_empty())
|
|
12914
|
+
{
|
|
12790
12915
|
let Some((collection, field)) = entry.split_once('.') else {
|
|
12791
12916
|
return Err(format!(
|
|
12792
12917
|
"FELTDB_EQUALITY_INDEXES entry {entry:?} must be written as collection.field"
|
|
@@ -12880,11 +13005,12 @@ fn bounded_query_records(
|
|
|
12880
13005
|
) -> Result<Vec<feltdb::StoredRow>, ApiError> {
|
|
12881
13006
|
if execution != BoundedQueryExecution::Scan {
|
|
12882
13007
|
let equalities = bounded_query_equalities(&request.conditions);
|
|
12883
|
-
let indexed =
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
13008
|
+
let indexed =
|
|
13009
|
+
state
|
|
13010
|
+
.db
|
|
13011
|
+
.query_collection_by_equality(&request.collection, &equalities, |row| {
|
|
13012
|
+
bounded_query_matches(row, &request.conditions)
|
|
13013
|
+
})?;
|
|
12888
13014
|
if let Some(records) = indexed {
|
|
12889
13015
|
return Ok(records);
|
|
12890
13016
|
}
|
|
@@ -12897,15 +13023,18 @@ fn bounded_query_records(
|
|
|
12897
13023
|
));
|
|
12898
13024
|
}
|
|
12899
13025
|
}
|
|
12900
|
-
Ok(state
|
|
12901
|
-
|
|
12902
|
-
|
|
13026
|
+
Ok(state
|
|
13027
|
+
.db
|
|
13028
|
+
.query_collection(&request.collection, None, |row| {
|
|
13029
|
+
bounded_query_matches(row, &request.conditions)
|
|
13030
|
+
})?)
|
|
12903
13031
|
}
|
|
12904
13032
|
|
|
12905
13033
|
fn issue_bounded_cursor(
|
|
12906
13034
|
state: &AppState,
|
|
12907
13035
|
mut cursor: BoundedQueryCursor,
|
|
12908
|
-
) -> Result<String, ApiError> {
|
|
13036
|
+
) -> Result<String, ApiError> {
|
|
13037
|
+
let token = uuid::Uuid::new_v4().simple().to_string();
|
|
12909
13038
|
let now = unix_seconds_i64().max(0) as u64;
|
|
12910
13039
|
cursor.created_at = now;
|
|
12911
13040
|
let mut cursors = state.bounded_query_cursors.lock().map_err(|_| {
|
|
@@ -13356,9 +13485,10 @@ async fn commit_transaction(
|
|
|
13356
13485
|
// mutations and retained across compaction.
|
|
13357
13486
|
let payload_hash = format!(
|
|
13358
13487
|
"sha256:{:x}",
|
|
13359
|
-
Sha256::digest(
|
|
13360
|
-
|
|
13361
|
-
|
|
13488
|
+
Sha256::digest(
|
|
13489
|
+
serde_json::to_vec(&request)
|
|
13490
|
+
.map_err(|error| { ApiError(StatusCode::BAD_REQUEST, error.to_string()) })?
|
|
13491
|
+
)
|
|
13362
13492
|
);
|
|
13363
13493
|
|
|
13364
13494
|
// Validate every operation before any of them is turned into a mutation, so
|
|
@@ -14276,7 +14406,9 @@ async fn control_plane_rebuild_index(
|
|
|
14276
14406
|
"{}:{}",
|
|
14277
14407
|
contract.environment.state_namespace, request.collection
|
|
14278
14408
|
);
|
|
14279
|
-
let created = state
|
|
14409
|
+
let created = state
|
|
14410
|
+
.db
|
|
14411
|
+
.create_equality_index(&capability, &request.field)?;
|
|
14280
14412
|
Ok(Json(json!({
|
|
14281
14413
|
"collection": request.collection,
|
|
14282
14414
|
"field": request.field,
|
|
@@ -14324,10 +14456,9 @@ async fn control_plane_operations(
|
|
|
14324
14456
|
let matched = operations
|
|
14325
14457
|
.iter()
|
|
14326
14458
|
.filter(|operation| {
|
|
14327
|
-
query
|
|
14328
|
-
.collection
|
|
14329
|
-
|
|
14330
|
-
.is_none_or(|collection| control_plane::collection_of(&operation.capability) == collection)
|
|
14459
|
+
query.collection.as_deref().is_none_or(|collection| {
|
|
14460
|
+
control_plane::collection_of(&operation.capability) == collection
|
|
14461
|
+
})
|
|
14331
14462
|
})
|
|
14332
14463
|
.filter(|operation| {
|
|
14333
14464
|
query.kind.as_deref().is_none_or(|kind| {
|
|
@@ -14353,9 +14484,7 @@ async fn control_plane_operations(
|
|
|
14353
14484
|
}
|
|
14354
14485
|
|
|
14355
14486
|
/// Peers, causal position, and what replication still owes.
|
|
14356
|
-
async fn control_plane_replication(
|
|
14357
|
-
State(state): State<AppState>,
|
|
14358
|
-
) -> Result<Json<Value>, ApiError> {
|
|
14487
|
+
async fn control_plane_replication(State(state): State<AppState>) -> Result<Json<Value>, ApiError> {
|
|
14359
14488
|
let db = state.db.clone();
|
|
14360
14489
|
let namespace = state.namespace.to_string();
|
|
14361
14490
|
let scan_namespace = namespace.clone();
|
|
@@ -14408,8 +14537,7 @@ async fn control_plane_applications(
|
|
|
14408
14537
|
.iter()
|
|
14409
14538
|
.map(|(environment, revision_id)| {
|
|
14410
14539
|
let instance = instances.iter().find(|instance| {
|
|
14411
|
-
instance.environment == *environment
|
|
14412
|
-
&& instance.revision_id == *revision_id
|
|
14540
|
+
instance.environment == *environment && instance.revision_id == *revision_id
|
|
14413
14541
|
});
|
|
14414
14542
|
json!({
|
|
14415
14543
|
"environment": environment,
|
|
@@ -14722,10 +14850,7 @@ async fn control_plane_record_provenance(
|
|
|
14722
14850
|
/// Computed from the two things that actually decide it: the capabilities the
|
|
14723
14851
|
/// principal holds, and the actions and policies the contract declares. Nothing
|
|
14724
14852
|
/// is inferred from the agent's name or history.
|
|
14725
|
-
fn agent_access(
|
|
14726
|
-
contract: &ApplicationRuntimeContract,
|
|
14727
|
-
capabilities: &[String],
|
|
14728
|
-
) -> Vec<Value> {
|
|
14853
|
+
fn agent_access(contract: &ApplicationRuntimeContract, capabilities: &[String]) -> Vec<Value> {
|
|
14729
14854
|
let holds = |required: &[String]| required.iter().all(|value| capabilities.contains(value));
|
|
14730
14855
|
contract
|
|
14731
14856
|
.state
|
|
@@ -15101,7 +15226,10 @@ async fn state_diff(
|
|
|
15101
15226
|
object.insert("to".into(), control_plane::revision_summary_to_json(&to));
|
|
15102
15227
|
object.insert(
|
|
15103
15228
|
"relationship".into(),
|
|
15104
|
-
json!(match (
|
|
15229
|
+
json!(match (
|
|
15230
|
+
from.resource == to.resource,
|
|
15231
|
+
to.parent_id.as_ref() == Some(&from.id)
|
|
15232
|
+
) {
|
|
15105
15233
|
(true, true) => "parent",
|
|
15106
15234
|
(true, false) => "same-resource",
|
|
15107
15235
|
(false, _) => "cross-resource",
|
|
@@ -15146,7 +15274,15 @@ async fn state_conflicts(
|
|
|
15146
15274
|
fn control_plane_reconciliation_inputs(
|
|
15147
15275
|
store: &feltdb::StateStore,
|
|
15148
15276
|
request: &control_plane::ReconciliationPlanRequest,
|
|
15149
|
-
) -> Result<
|
|
15277
|
+
) -> Result<
|
|
15278
|
+
(
|
|
15279
|
+
StateRevision,
|
|
15280
|
+
StateRevision,
|
|
15281
|
+
StateRevision,
|
|
15282
|
+
feltdb::ReconciliationPlan,
|
|
15283
|
+
),
|
|
15284
|
+
ApiError,
|
|
15285
|
+
> {
|
|
15150
15286
|
let plan = request.to_plan().map_err(|message| {
|
|
15151
15287
|
control_plane_error(StatusCode::UNPROCESSABLE_ENTITY, "invalid_plan", message)
|
|
15152
15288
|
})?;
|
|
@@ -15169,11 +15305,10 @@ async fn state_reconciliation_plan(
|
|
|
15169
15305
|
let store = control_plane_state_store(&state)?;
|
|
15170
15306
|
let (base, left, right, plan) = control_plane_reconciliation_inputs(&store, &request)?;
|
|
15171
15307
|
let classification = feltdb::ConflictClassification::classify(&base, &left, &right);
|
|
15172
|
-
let preview =
|
|
15173
|
-
|message| {
|
|
15308
|
+
let preview =
|
|
15309
|
+
feltdb::apply_reconciliation_plan(&base, &left, &right, &plan).map_err(|message| {
|
|
15174
15310
|
control_plane_error(StatusCode::UNPROCESSABLE_ENTITY, "plan_refused", message)
|
|
15175
|
-
}
|
|
15176
|
-
)?;
|
|
15311
|
+
})?;
|
|
15177
15312
|
let mut content = serde_json::from_str::<Value>(&preview).unwrap_or(Value::Null);
|
|
15178
15313
|
redact_inspection_value(&mut content);
|
|
15179
15314
|
Ok(Json(json!({
|
|
@@ -15221,11 +15356,7 @@ async fn state_reconciliation_execute(
|
|
|
15221
15356
|
outcome.materialized_state.authority.clone(),
|
|
15222
15357
|
)
|
|
15223
15358
|
.map_err(|error| {
|
|
15224
|
-
control_plane_error(
|
|
15225
|
-
StatusCode::CONFLICT,
|
|
15226
|
-
"revision_refused",
|
|
15227
|
-
error.to_string(),
|
|
15228
|
-
)
|
|
15359
|
+
control_plane_error(StatusCode::CONFLICT, "revision_refused", error.to_string())
|
|
15229
15360
|
})?;
|
|
15230
15361
|
// The two must agree. They are computed by the same constructor from the
|
|
15231
15362
|
// same content, parent and authority, so a disagreement means the store and
|
|
@@ -15359,7 +15490,10 @@ mod worker_identity_tests {
|
|
|
15359
15490
|
|
|
15360
15491
|
#[cfg(test)]
|
|
15361
15492
|
mod authority_gate_tests {
|
|
15362
|
-
use super::{
|
|
15493
|
+
use super::{
|
|
15494
|
+
api_error_body, authorize_transaction_collections, require_bound_tenant,
|
|
15495
|
+
state_authorization,
|
|
15496
|
+
};
|
|
15363
15497
|
use axum::http::{HeaderMap, HeaderValue, StatusCode};
|
|
15364
15498
|
use feltdb::{
|
|
15365
15499
|
application::{
|
|
@@ -15476,7 +15610,10 @@ mod authority_gate_tests {
|
|
|
15476
15610
|
let error = require_bound_tenant(&headers, "tenant-b").unwrap_err();
|
|
15477
15611
|
assert_eq!(error.0, StatusCode::FORBIDDEN);
|
|
15478
15612
|
let body: serde_json::Value = serde_json::from_str(&error.1).unwrap();
|
|
15479
|
-
assert_eq!(
|
|
15613
|
+
assert_eq!(
|
|
15614
|
+
body.get("code").and_then(serde_json::Value::as_str),
|
|
15615
|
+
Some("AUTHORITY_SCOPE_MISMATCH")
|
|
15616
|
+
);
|
|
15480
15617
|
}
|
|
15481
15618
|
|
|
15482
15619
|
#[test]
|