@feltdb/core 0.4.11 → 0.4.13
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/cli/commands.js +5 -2
- package/dist/cli/index.js +1 -1
- package/dist/create/cli.js +16 -1
- package/dist/create/create.js +30 -16
- package/dist/create/docker-compose-generator.js +48 -9
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +2238 -0
- package/dist/create/server-source/Cargo.toml +7 -0
- package/dist/create/server-source/crates/feltdb/Cargo.lock +175 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +79 -0
- package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
- package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
- package/dist/create/server-source/crates/feltdb/src/acquisition.rs +286 -0
- package/dist/create/server-source/crates/feltdb/src/admission.rs +192 -0
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +475 -0
- package/dist/create/server-source/crates/feltdb/src/application.rs +2244 -0
- package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
- package/dist/create/server-source/crates/feltdb/src/authorization.rs +1030 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
- package/dist/create/server-source/crates/feltdb/src/capability.rs +903 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
- package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +465 -0
- package/dist/create/server-source/crates/feltdb/src/convergence.rs +618 -0
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +380 -0
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
- package/dist/create/server-source/crates/feltdb/src/cron.rs +294 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +316 -0
- package/dist/create/server-source/crates/feltdb/src/execution.rs +426 -0
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +779 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +2838 -0
- package/dist/create/server-source/crates/feltdb/src/materialization.rs +184 -0
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
- package/dist/create/server-source/crates/feltdb/src/observability.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/operation.rs +251 -0
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +344 -0
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +166 -0
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +400 -0
- package/dist/create/server-source/crates/feltdb/src/provenance.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +217 -0
- package/dist/create/server-source/crates/feltdb/src/references.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +160 -0
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
- package/dist/create/server-source/crates/feltdb/src/routing.rs +409 -0
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +3118 -0
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +291 -0
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
- package/dist/create/server-source/crates/feltdb/src/storage.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/submission.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/sync.rs +483 -0
- package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +822 -0
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +473 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +646 -0
- package/dist/create/server-source/crates/feltdb/src/trigger.rs +390 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
- package/dist/create/server-source/crates/feltdb/src/workflow.rs +713 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +1310 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +23 -0
- package/dist/create/server-source/crates/feltdb-server/README.md +79 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +73 -0
- package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
- package/dist/create/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
- package/dist/create/server-source/crates/feltdb-server/src/audit.rs +59 -0
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +308 -0
- package/dist/create/server-source/crates/feltdb-server/src/authorization.rs +228 -0
- package/dist/create/server-source/crates/feltdb-server/src/backup.rs +416 -0
- package/dist/create/server-source/crates/feltdb-server/src/causal.rs +218 -0
- package/dist/create/server-source/crates/feltdb-server/src/certification.rs +223 -0
- package/dist/create/server-source/crates/feltdb-server/src/clock.rs +132 -0
- package/dist/create/server-source/crates/feltdb-server/src/cluster.rs +165 -0
- package/dist/create/server-source/crates/feltdb-server/src/connections.rs +1044 -0
- package/dist/create/server-source/crates/feltdb-server/src/content.rs +111 -0
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +250 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +78 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
- package/dist/create/server-source/crates/feltdb-server/src/leases.rs +123 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +25 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +8715 -0
- package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +97 -0
- package/dist/create/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
- package/dist/create/server-source/crates/feltdb-server/src/principals.rs +510 -0
- package/dist/create/server-source/crates/feltdb-server/src/providers.rs +269 -0
- package/dist/create/server-source/crates/feltdb-server/src/releases.rs +2563 -0
- package/dist/create/server-source/crates/feltdb-server/src/sessions.rs +156 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
- package/dist/create/server-source/crates/feltdb-server/src/versions.rs +264 -0
- package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
- package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
- package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
- package/dist/flowspec.d.ts +2 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +28 -4
- package/dist/state-contract.d.ts +7 -1
- package/dist/state-contract.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-q43NSTTH.js → components-bHTARBin.js} +160 -89
- package/dist/studio/index.js +31 -28
- package/dist/studio-app/assets/{index-DZpBoVMp.js → index-BqKquLuU.js} +8 -8
- package/dist/studio-app/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "feltdb-server"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
|
|
6
|
+
[dependencies]
|
|
7
|
+
axum = { version = "0.8", features = ["json", "tokio"] }
|
|
8
|
+
async-stream = "0.3"
|
|
9
|
+
argon2 = "0.5"
|
|
10
|
+
password-hash = "0.5"
|
|
11
|
+
base64 = "0.22"
|
|
12
|
+
feltdb = { path = "../feltdb" }
|
|
13
|
+
futures-core = "0.3"
|
|
14
|
+
rand = "0.8"
|
|
15
|
+
ring = "0.17"
|
|
16
|
+
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
|
17
|
+
serde = { version = "1", features = ["derive"] }
|
|
18
|
+
serde_json = "1"
|
|
19
|
+
sha2 = "0.10"
|
|
20
|
+
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "time"] }
|
|
21
|
+
tower-http = { version = "0.6", features = ["cors", "limit", "trace"] }
|
|
22
|
+
tracing = "0.1"
|
|
23
|
+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# FeltDB Server
|
|
2
|
+
|
|
3
|
+
This crate is the first network adapter over the canonical `feltdb` runtime. It does not own a
|
|
4
|
+
second state store: every HTTP mutation calls `FeltDb::insert`, `FeltDb::update`, or
|
|
5
|
+
`FeltDb::delete`, producing the same operations, vector-clock updates, durable log entries, and
|
|
6
|
+
reactive events as an embedded mutation.
|
|
7
|
+
|
|
8
|
+
Run it with:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
cargo run -p feltdb-server -- \
|
|
12
|
+
--host 127.0.0.1 \
|
|
13
|
+
--port 7700 \
|
|
14
|
+
--namespace my-app \
|
|
15
|
+
--data ./data/feltdb.log
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For a customer-facing process, create a namespace-bound key and enable authentication:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
cargo run -p feltdb-server -- keys create \
|
|
22
|
+
--keys ./data/api-keys.json \
|
|
23
|
+
--name application \
|
|
24
|
+
--namespace my-app \
|
|
25
|
+
--scope state:read,state:write,events:read
|
|
26
|
+
|
|
27
|
+
cargo run -p feltdb-server -- \
|
|
28
|
+
--namespace my-app \
|
|
29
|
+
--data ./data/feltdb.log \
|
|
30
|
+
--keys ./data/api-keys.json \
|
|
31
|
+
--auth
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The secret is printed once; only its Argon2 hash is stored. Send it as
|
|
35
|
+
`Authorization: Bearer <key>`. Use `keys list` to inspect safe key metadata and
|
|
36
|
+
`keys revoke --id <id>` to revoke a key. Restart a running server after changing its key file.
|
|
37
|
+
|
|
38
|
+
Implemented in this slice:
|
|
39
|
+
|
|
40
|
+
- `GET /health`
|
|
41
|
+
- `GET /runtime`
|
|
42
|
+
- collection list/create/get/update/delete endpoints
|
|
43
|
+
- `GET /events` as a server-sent event stream
|
|
44
|
+
- durable, hashed API keys with namespace and `state:read`, `state:write`, or `events:read` scopes
|
|
45
|
+
- `GET /metrics` request, authorization, state, and event counters
|
|
46
|
+
- authenticated `POST /sync/pull` and `POST /sync/push` operation exchange
|
|
47
|
+
- durable, transitive operation history with content-hash verification, deduplication, and gap rejection
|
|
48
|
+
|
|
49
|
+
Peer credentials should be separate from application credentials and use the `sync:read` and
|
|
50
|
+
`sync:write` scopes. Pull requests contain the caller's per-origin `versions` map. Push requests
|
|
51
|
+
contain the target `namespace` and operations returned by pull. This version-vector exchange
|
|
52
|
+
preserves original operation identity through relays instead of translating replicated writes into
|
|
53
|
+
new local mutations.
|
|
54
|
+
|
|
55
|
+
To maintain a continuous bidirectional session, configure one or more peers and provide the peer
|
|
56
|
+
key through an environment variable (never a command-line argument):
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
export FELTDB_PEER_TOKEN='fdb_live_...'
|
|
60
|
+
feltdb-server --namespace my-app --peer https://peer-a.example --peer https://peer-b.example
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Each session pulls and pushes missing operations, advertises the successfully contacted peer to the
|
|
64
|
+
runtime registry, and retries failures with exponential backoff capped at 30 seconds. The polling
|
|
65
|
+
interval defaults to two seconds and can be changed with `--sync-interval-ms`.
|
|
66
|
+
|
|
67
|
+
Successful pushes durably persist the remote peer's per-origin acknowledgement vector. Once every
|
|
68
|
+
configured peer acknowledges an operation, compaction atomically replaces that history with a
|
|
69
|
+
content-hashed state snapshot. A pristine peer bootstraps from `GET /sync/snapshot`, installs the
|
|
70
|
+
snapshot's state and causal high-water marks, then resumes ordinary operation exchange without
|
|
71
|
+
replaying pruned history or encountering sequence gaps. Nodes with independent local history are
|
|
72
|
+
never overwritten by snapshot bootstrap.
|
|
73
|
+
- `FeltDB-Protocol` compatibility rejection
|
|
74
|
+
- 1 MiB request limit, identifier validation, graceful shutdown, and structured HTTP tracing
|
|
75
|
+
- durable deletion tombstones that survive process restart
|
|
76
|
+
|
|
77
|
+
The runtime discovery response explicitly marks acquisition, remote capabilities, workflows,
|
|
78
|
+
agents, and provenance as unavailable until those systems are connected to this same operation
|
|
79
|
+
boundary. The server must never advertise simulated functionality as healthy.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
use std::sync::{atomic::AtomicU64, Arc};
|
|
2
|
+
use std::path::PathBuf;
|
|
3
|
+
|
|
4
|
+
use feltdb::{
|
|
5
|
+
FeltDb,
|
|
6
|
+
application::ApplicationStore,
|
|
7
|
+
authorization::{GrantSigner, GrantStore},
|
|
8
|
+
sync_contract::SyncStore,
|
|
9
|
+
workload::WorkloadStore,
|
|
10
|
+
worker_mesh::WorkerMeshStore,
|
|
11
|
+
application_runtime::RuntimeStore,
|
|
12
|
+
};
|
|
13
|
+
use crate::{
|
|
14
|
+
artifacts::ArtifactStore,
|
|
15
|
+
audit::AuditLog,
|
|
16
|
+
auth::KeyStore,
|
|
17
|
+
causal::CausalStore,
|
|
18
|
+
cluster::ClusterStore,
|
|
19
|
+
connections::ConnectionStore,
|
|
20
|
+
content::ContentStore,
|
|
21
|
+
identity::IdentityStore,
|
|
22
|
+
leases::LeaseStore,
|
|
23
|
+
metrics::Metrics,
|
|
24
|
+
portable_bundle::BundleStore,
|
|
25
|
+
principals::PrincipalStore,
|
|
26
|
+
providers::ProviderStore,
|
|
27
|
+
releases::ReleaseStore,
|
|
28
|
+
sessions::SessionVerifier,
|
|
29
|
+
tenancy::TenancyStore,
|
|
30
|
+
application_contract::ApplicationContractStore,
|
|
31
|
+
clock::LeaseClock,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
#[derive(Clone)]
|
|
35
|
+
pub struct AppState {
|
|
36
|
+
pub db: FeltDb,
|
|
37
|
+
pub namespace: Arc<str>,
|
|
38
|
+
pub ids: Arc<AtomicU64>,
|
|
39
|
+
pub auth_enabled: bool,
|
|
40
|
+
pub keys: KeyStore,
|
|
41
|
+
pub metrics: Metrics,
|
|
42
|
+
pub cluster: ClusterStore,
|
|
43
|
+
pub audit: AuditLog,
|
|
44
|
+
pub peer_client: reqwest::Client,
|
|
45
|
+
pub peer_token: Option<Arc<str>>,
|
|
46
|
+
pub lifecycle_lock: Arc<tokio::sync::Mutex<()>>,
|
|
47
|
+
pub worker_enabled: bool,
|
|
48
|
+
pub leases: LeaseStore,
|
|
49
|
+
pub content: ContentStore,
|
|
50
|
+
pub artifacts: ArtifactStore,
|
|
51
|
+
pub bundles: BundleStore,
|
|
52
|
+
pub releases: ReleaseStore,
|
|
53
|
+
pub causal: CausalStore,
|
|
54
|
+
pub providers: ProviderStore,
|
|
55
|
+
pub contracts: ApplicationContractStore,
|
|
56
|
+
pub advertise_url: Option<Arc<str>>,
|
|
57
|
+
pub lease_clock: LeaseClock,
|
|
58
|
+
pub backup_barrier: Arc<tokio::sync::RwLock<()>>,
|
|
59
|
+
pub backup_artifacts: Arc<Vec<(String, PathBuf)>>,
|
|
60
|
+
pub tenancy: TenancyStore,
|
|
61
|
+
pub sessions: SessionVerifier,
|
|
62
|
+
pub principals: PrincipalStore,
|
|
63
|
+
pub connections: ConnectionStore,
|
|
64
|
+
pub identities: IdentityStore,
|
|
65
|
+
pub applications: ApplicationStore,
|
|
66
|
+
pub runtimes: RuntimeStore,
|
|
67
|
+
pub grant_signer: GrantSigner,
|
|
68
|
+
pub grants: Arc<std::sync::Mutex<GrantStore>>,
|
|
69
|
+
pub sync: Arc<std::sync::Mutex<SyncStore>>,
|
|
70
|
+
pub workloads: Arc<std::sync::Mutex<WorkloadStore>>,
|
|
71
|
+
pub mesh: Arc<std::sync::Mutex<WorkerMeshStore>>,
|
|
72
|
+
pub readiness_probe: Arc<PathBuf>,
|
|
73
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
use rand::{distributions::Alphanumeric, Rng};
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
use serde_json::Value;
|
|
4
|
+
use std::{
|
|
5
|
+
collections::BTreeMap,
|
|
6
|
+
fs,
|
|
7
|
+
path::PathBuf,
|
|
8
|
+
sync::{Arc, RwLock},
|
|
9
|
+
time::{SystemTime, UNIX_EPOCH},
|
|
10
|
+
};
|
|
11
|
+
fn now() -> u64 {
|
|
12
|
+
SystemTime::now()
|
|
13
|
+
.duration_since(UNIX_EPOCH)
|
|
14
|
+
.unwrap_or_default()
|
|
15
|
+
.as_secs()
|
|
16
|
+
}
|
|
17
|
+
fn id(p: &str) -> String {
|
|
18
|
+
format!(
|
|
19
|
+
"{p}_{}",
|
|
20
|
+
rand::thread_rng()
|
|
21
|
+
.sample_iter(&Alphanumeric)
|
|
22
|
+
.take(18)
|
|
23
|
+
.map(char::from)
|
|
24
|
+
.collect::<String>()
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
28
|
+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
|
29
|
+
pub enum EvidenceSourceType {
|
|
30
|
+
User,
|
|
31
|
+
ApplicationState,
|
|
32
|
+
ExistingSchema,
|
|
33
|
+
Connection,
|
|
34
|
+
Artifact,
|
|
35
|
+
Document,
|
|
36
|
+
Provider,
|
|
37
|
+
Agent,
|
|
38
|
+
Workload,
|
|
39
|
+
AiInference,
|
|
40
|
+
SystemRule,
|
|
41
|
+
ImportedData,
|
|
42
|
+
}
|
|
43
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
44
|
+
pub struct ContractEvidence {
|
|
45
|
+
pub evidence_id: String,
|
|
46
|
+
pub source: String,
|
|
47
|
+
pub source_type: EvidenceSourceType,
|
|
48
|
+
pub statement: String,
|
|
49
|
+
pub contract_paths: Vec<String>,
|
|
50
|
+
pub confidence: f32,
|
|
51
|
+
pub authority: String,
|
|
52
|
+
pub provenance: Value,
|
|
53
|
+
pub timestamp: u64,
|
|
54
|
+
pub supporting_refs: Vec<String>,
|
|
55
|
+
pub contradicting_refs: Vec<String>,
|
|
56
|
+
}
|
|
57
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
58
|
+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
|
59
|
+
pub enum GapType {
|
|
60
|
+
Missing,
|
|
61
|
+
Ambiguous,
|
|
62
|
+
Contradictory,
|
|
63
|
+
Unauthorized,
|
|
64
|
+
Unsupported,
|
|
65
|
+
Unvalidated,
|
|
66
|
+
DependencyMissing,
|
|
67
|
+
ProviderMissing,
|
|
68
|
+
}
|
|
69
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
70
|
+
pub struct ContractGap {
|
|
71
|
+
pub contract_path: String,
|
|
72
|
+
pub gap_type: GapType,
|
|
73
|
+
pub severity: String,
|
|
74
|
+
pub blocking: bool,
|
|
75
|
+
pub confidence: f32,
|
|
76
|
+
pub evidence_required: Vec<String>,
|
|
77
|
+
pub suggested_questions: Vec<String>,
|
|
78
|
+
pub candidate_resolutions: Vec<Value>,
|
|
79
|
+
pub affected_paths: Vec<String>,
|
|
80
|
+
}
|
|
81
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
82
|
+
pub struct ContractAssertion {
|
|
83
|
+
pub value: Value,
|
|
84
|
+
pub confidence: f32,
|
|
85
|
+
pub evidence_refs: Vec<String>,
|
|
86
|
+
pub required: bool,
|
|
87
|
+
pub validated: bool,
|
|
88
|
+
}
|
|
89
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
90
|
+
pub struct ApplicationContract {
|
|
91
|
+
pub contract_id: String,
|
|
92
|
+
pub application_id: String,
|
|
93
|
+
pub base_revision: Option<String>,
|
|
94
|
+
pub contract_revision: u64,
|
|
95
|
+
pub status: String,
|
|
96
|
+
pub assertions: BTreeMap<String, ContractAssertion>,
|
|
97
|
+
pub evidence: Vec<ContractEvidence>,
|
|
98
|
+
pub gaps: Vec<ContractGap>,
|
|
99
|
+
pub created_at: u64,
|
|
100
|
+
pub updated_at: u64,
|
|
101
|
+
}
|
|
102
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
103
|
+
pub struct ContractChange {
|
|
104
|
+
pub path: String,
|
|
105
|
+
pub value: Value,
|
|
106
|
+
pub confidence: f32,
|
|
107
|
+
pub required: bool,
|
|
108
|
+
}
|
|
109
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
110
|
+
pub struct ContractPatch {
|
|
111
|
+
pub patch_id: Option<String>,
|
|
112
|
+
pub base_revision: u64,
|
|
113
|
+
pub changes: Vec<ContractChange>,
|
|
114
|
+
pub evidence: Vec<ContractEvidence>,
|
|
115
|
+
pub affected_paths: Vec<String>,
|
|
116
|
+
pub confidence_delta: f32,
|
|
117
|
+
pub risks: Vec<String>,
|
|
118
|
+
pub provenance: Value,
|
|
119
|
+
}
|
|
120
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
121
|
+
pub struct ContractReadiness {
|
|
122
|
+
pub status: String,
|
|
123
|
+
pub readiness: f32,
|
|
124
|
+
pub blocking_gaps: usize,
|
|
125
|
+
pub gaps: Vec<ContractGap>,
|
|
126
|
+
pub confidence: BTreeMap<String, f32>,
|
|
127
|
+
pub next_action: String,
|
|
128
|
+
}
|
|
129
|
+
#[derive(Default, Serialize, Deserialize)]
|
|
130
|
+
struct Records {
|
|
131
|
+
contracts: BTreeMap<String, ApplicationContract>,
|
|
132
|
+
}
|
|
133
|
+
#[derive(Clone)]
|
|
134
|
+
pub struct ApplicationContractStore {
|
|
135
|
+
path: PathBuf,
|
|
136
|
+
records: Arc<RwLock<Records>>,
|
|
137
|
+
}
|
|
138
|
+
impl ApplicationContractStore {
|
|
139
|
+
pub fn load(path: impl Into<PathBuf>) -> Result<Self, String> {
|
|
140
|
+
let path = path.into();
|
|
141
|
+
let records = if path.exists() {
|
|
142
|
+
serde_json::from_slice(&fs::read(&path).map_err(|e| e.to_string())?)
|
|
143
|
+
.map_err(|e| e.to_string())?
|
|
144
|
+
} else {
|
|
145
|
+
Records::default()
|
|
146
|
+
};
|
|
147
|
+
Ok(Self {
|
|
148
|
+
path,
|
|
149
|
+
records: Arc::new(RwLock::new(records)),
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
fn persist(&self, r: &Records) -> Result<(), String> {
|
|
153
|
+
if let Some(p) = self.path.parent() {
|
|
154
|
+
fs::create_dir_all(p).map_err(|e| e.to_string())?
|
|
155
|
+
}
|
|
156
|
+
fs::write(
|
|
157
|
+
&self.path,
|
|
158
|
+
serde_json::to_vec_pretty(r).map_err(|e| e.to_string())?,
|
|
159
|
+
)
|
|
160
|
+
.map_err(|e| e.to_string())
|
|
161
|
+
}
|
|
162
|
+
pub fn get(&self, a: &str) -> Result<ApplicationContract, String> {
|
|
163
|
+
self.records
|
|
164
|
+
.read()
|
|
165
|
+
.map_err(|_| "contract lock poisoned")?
|
|
166
|
+
.contracts
|
|
167
|
+
.get(a)
|
|
168
|
+
.cloned()
|
|
169
|
+
.ok_or("contract not found".into())
|
|
170
|
+
}
|
|
171
|
+
pub fn evaluate(
|
|
172
|
+
&self,
|
|
173
|
+
a: &str,
|
|
174
|
+
outcome: &str,
|
|
175
|
+
actor: &str,
|
|
176
|
+
) -> Result<ApplicationContract, String> {
|
|
177
|
+
let mut r = self.records.write().map_err(|_| "contract lock poisoned")?;
|
|
178
|
+
let t = now();
|
|
179
|
+
let e = ContractEvidence {
|
|
180
|
+
evidence_id: id("evidence"),
|
|
181
|
+
source: actor.into(),
|
|
182
|
+
source_type: EvidenceSourceType::User,
|
|
183
|
+
statement: outcome.into(),
|
|
184
|
+
contract_paths: vec!["outcome".into()],
|
|
185
|
+
confidence: 0.99,
|
|
186
|
+
authority: "subject".into(),
|
|
187
|
+
provenance: serde_json::json!({"actor":actor}),
|
|
188
|
+
timestamp: t,
|
|
189
|
+
supporting_refs: vec![],
|
|
190
|
+
contradicting_refs: vec![],
|
|
191
|
+
};
|
|
192
|
+
let mut assertions = BTreeMap::new();
|
|
193
|
+
assertions.insert(
|
|
194
|
+
"outcome".into(),
|
|
195
|
+
ContractAssertion {
|
|
196
|
+
value: Value::String(outcome.into()),
|
|
197
|
+
confidence: 0.99,
|
|
198
|
+
evidence_refs: vec![e.evidence_id.clone()],
|
|
199
|
+
required: true,
|
|
200
|
+
validated: true,
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
let mut c = ApplicationContract {
|
|
204
|
+
contract_id: id("contract"),
|
|
205
|
+
application_id: a.into(),
|
|
206
|
+
base_revision: None,
|
|
207
|
+
contract_revision: 1,
|
|
208
|
+
status: "INCOMPLETE".into(),
|
|
209
|
+
assertions,
|
|
210
|
+
evidence: vec![e],
|
|
211
|
+
gaps: vec![],
|
|
212
|
+
created_at: t,
|
|
213
|
+
updated_at: t,
|
|
214
|
+
};
|
|
215
|
+
Self::recompute(&mut c);
|
|
216
|
+
r.contracts.insert(a.into(), c.clone());
|
|
217
|
+
self.persist(&r)?;
|
|
218
|
+
Ok(c)
|
|
219
|
+
}
|
|
220
|
+
pub fn apply_patch(&self, a: &str, p: ContractPatch) -> Result<ApplicationContract, String> {
|
|
221
|
+
let mut r = self.records.write().map_err(|_| "contract lock poisoned")?;
|
|
222
|
+
let c = r.contracts.get_mut(a).ok_or("contract not found")?;
|
|
223
|
+
if p.base_revision != c.contract_revision {
|
|
224
|
+
return Err("stale contract patch".into());
|
|
225
|
+
}
|
|
226
|
+
if p.risks.iter().any(|x| x == "destructive")
|
|
227
|
+
&& !p
|
|
228
|
+
.provenance
|
|
229
|
+
.get("approved")
|
|
230
|
+
.and_then(Value::as_bool)
|
|
231
|
+
.unwrap_or(false)
|
|
232
|
+
{
|
|
233
|
+
return Err("destructive patch requires explicit approval".into());
|
|
234
|
+
}
|
|
235
|
+
for x in p.changes {
|
|
236
|
+
if x.path.trim().is_empty() || !(0.0..=1.0).contains(&x.confidence) {
|
|
237
|
+
return Err("invalid contract change".into());
|
|
238
|
+
}
|
|
239
|
+
if let Some(old) = c.assertions.get(&x.path) {
|
|
240
|
+
if old.value != x.value && old.confidence > 0.8 && x.confidence > 0.8 {
|
|
241
|
+
c.gaps.push(ContractGap {
|
|
242
|
+
contract_path: x.path.clone(),
|
|
243
|
+
gap_type: GapType::Contradictory,
|
|
244
|
+
severity: "high".into(),
|
|
245
|
+
blocking: true,
|
|
246
|
+
confidence: x.confidence,
|
|
247
|
+
evidence_required: vec!["user resolution".into()],
|
|
248
|
+
suggested_questions: vec![format!("Which value should {} use?", x.path)],
|
|
249
|
+
candidate_resolutions: vec![old.value.clone(), x.value.clone()],
|
|
250
|
+
affected_paths: vec![x.path.clone()],
|
|
251
|
+
});
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
c.assertions.insert(
|
|
256
|
+
x.path,
|
|
257
|
+
ContractAssertion {
|
|
258
|
+
value: x.value,
|
|
259
|
+
confidence: x.confidence,
|
|
260
|
+
evidence_refs: p.evidence.iter().map(|e| e.evidence_id.clone()).collect(),
|
|
261
|
+
required: x.required,
|
|
262
|
+
validated: true,
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
c.evidence.extend(p.evidence);
|
|
267
|
+
c.contract_revision += 1;
|
|
268
|
+
c.updated_at = now();
|
|
269
|
+
Self::recompute(c);
|
|
270
|
+
let out = c.clone();
|
|
271
|
+
self.persist(&r)?;
|
|
272
|
+
Ok(out)
|
|
273
|
+
}
|
|
274
|
+
fn recompute(c: &mut ApplicationContract) {
|
|
275
|
+
c.gaps.retain(|g| g.gap_type == GapType::Contradictory);
|
|
276
|
+
for p in ["outcome", "actors", "entities", "actions", "permissions"] {
|
|
277
|
+
if !c.assertions.contains_key(p) {
|
|
278
|
+
c.gaps.push(ContractGap {
|
|
279
|
+
contract_path: p.into(),
|
|
280
|
+
gap_type: GapType::Missing,
|
|
281
|
+
severity: "high".into(),
|
|
282
|
+
blocking: true,
|
|
283
|
+
confidence: 0.0,
|
|
284
|
+
evidence_required: vec!["contract evidence".into()],
|
|
285
|
+
suggested_questions: vec![match p {
|
|
286
|
+
"actors" => "Who will use this application, and what roles do they have?".into(),
|
|
287
|
+
"entities" => "What information or records should the application keep track of?".into(),
|
|
288
|
+
"actions" => "What should people be able to do with those records?".into(),
|
|
289
|
+
"permissions" => "Who can view, create, edit, delete, or administer this information?".into(),
|
|
290
|
+
_ => format!("What should FeltDB understand about {p}?"),
|
|
291
|
+
}],
|
|
292
|
+
candidate_resolutions: vec![],
|
|
293
|
+
affected_paths: vec![p.into()],
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
c.status = Self::readiness_for(c).status
|
|
298
|
+
}
|
|
299
|
+
pub fn readiness(&self, a: &str) -> Result<ContractReadiness, String> {
|
|
300
|
+
Ok(Self::readiness_for(&self.get(a)?))
|
|
301
|
+
}
|
|
302
|
+
pub fn next_question(&self, a: &str) -> Result<Option<ContractGap>, String> {
|
|
303
|
+
let contract = self.get(a)?;
|
|
304
|
+
Ok(contract
|
|
305
|
+
.gaps
|
|
306
|
+
.into_iter()
|
|
307
|
+
.filter(|gap| gap.blocking)
|
|
308
|
+
.min_by(|left, right| {
|
|
309
|
+
left.confidence
|
|
310
|
+
.partial_cmp(&right.confidence)
|
|
311
|
+
.unwrap_or(std::cmp::Ordering::Equal)
|
|
312
|
+
}))
|
|
313
|
+
}
|
|
314
|
+
pub fn end_session(&self, a: &str, actor: &str) -> Result<ApplicationContract, String> {
|
|
315
|
+
let mut records = self.records.write().map_err(|_| "contract lock poisoned")?;
|
|
316
|
+
let contract = records.contracts.get_mut(a).ok_or("contract not found")?;
|
|
317
|
+
contract.status = "MANUALLY_ENDED".into();
|
|
318
|
+
contract.updated_at = now();
|
|
319
|
+
contract.evidence.push(ContractEvidence {
|
|
320
|
+
evidence_id: id("evidence"),
|
|
321
|
+
source: actor.into(),
|
|
322
|
+
source_type: EvidenceSourceType::User,
|
|
323
|
+
statement: "User ended contract completion; compose only evidenced assertions".into(),
|
|
324
|
+
contract_paths: vec![],
|
|
325
|
+
confidence: 1.0,
|
|
326
|
+
authority: "subject".into(),
|
|
327
|
+
provenance: serde_json::json!({"escape_hatch":true}),
|
|
328
|
+
timestamp: now(),
|
|
329
|
+
supporting_refs: vec![],
|
|
330
|
+
contradicting_refs: vec![],
|
|
331
|
+
});
|
|
332
|
+
let result = contract.clone();
|
|
333
|
+
self.persist(&records)?;
|
|
334
|
+
Ok(result)
|
|
335
|
+
}
|
|
336
|
+
pub fn construction_input(&self, a: &str) -> Result<BTreeMap<String, Value>, String> {
|
|
337
|
+
let contract = self.get(a)?;
|
|
338
|
+
if contract.status != "READY" && contract.status != "MANUALLY_ENDED" {
|
|
339
|
+
return Err("contract completion is still active".into());
|
|
340
|
+
}
|
|
341
|
+
Ok(contract
|
|
342
|
+
.assertions
|
|
343
|
+
.into_iter()
|
|
344
|
+
.filter(|(_, assertion)| assertion.validated && !assertion.evidence_refs.is_empty())
|
|
345
|
+
.map(|(path, assertion)| (path, assertion.value))
|
|
346
|
+
.collect())
|
|
347
|
+
}
|
|
348
|
+
fn readiness_for(c: &ApplicationContract) -> ContractReadiness {
|
|
349
|
+
let confidence = c
|
|
350
|
+
.assertions
|
|
351
|
+
.iter()
|
|
352
|
+
.map(|(p, a)| (p.clone(), a.confidence))
|
|
353
|
+
.collect::<BTreeMap<_, _>>();
|
|
354
|
+
let readiness = if confidence.is_empty() {
|
|
355
|
+
0.0
|
|
356
|
+
} else {
|
|
357
|
+
confidence.values().sum::<f32>() / confidence.len() as f32
|
|
358
|
+
};
|
|
359
|
+
let blocking = c.gaps.iter().filter(|g| g.blocking).count();
|
|
360
|
+
ContractReadiness {
|
|
361
|
+
status: if blocking > 0 {
|
|
362
|
+
"INCOMPLETE"
|
|
363
|
+
} else if readiness >= 0.75 {
|
|
364
|
+
"READY"
|
|
365
|
+
} else {
|
|
366
|
+
"UNDERSTOOD"
|
|
367
|
+
}
|
|
368
|
+
.into(),
|
|
369
|
+
readiness,
|
|
370
|
+
blocking_gaps: blocking,
|
|
371
|
+
gaps: c.gaps.clone(),
|
|
372
|
+
confidence,
|
|
373
|
+
next_action: if blocking > 0 { "CLARIFY" } else { "COMPOSE" }.into(),
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
#[cfg(test)]
|
|
378
|
+
mod tests {
|
|
379
|
+
use super::*;
|
|
380
|
+
#[test]
|
|
381
|
+
fn contradiction_blocks() {
|
|
382
|
+
let s = ApplicationContractStore::load(
|
|
383
|
+
std::env::temp_dir().join(format!("contract-{}.json", now())),
|
|
384
|
+
)
|
|
385
|
+
.unwrap();
|
|
386
|
+
s.evaluate("a", "Recruit", "u").unwrap();
|
|
387
|
+
let p = ContractPatch {
|
|
388
|
+
patch_id: None,
|
|
389
|
+
base_revision: 1,
|
|
390
|
+
changes: vec![ContractChange {
|
|
391
|
+
path: "outcome".into(),
|
|
392
|
+
value: Value::String("Sell".into()),
|
|
393
|
+
confidence: 0.95,
|
|
394
|
+
required: true,
|
|
395
|
+
}],
|
|
396
|
+
evidence: vec![],
|
|
397
|
+
affected_paths: vec![],
|
|
398
|
+
confidence_delta: 0.0,
|
|
399
|
+
risks: vec![],
|
|
400
|
+
provenance: serde_json::json!({}),
|
|
401
|
+
};
|
|
402
|
+
assert!(s
|
|
403
|
+
.apply_patch("a", p)
|
|
404
|
+
.unwrap()
|
|
405
|
+
.gaps
|
|
406
|
+
.iter()
|
|
407
|
+
.any(|g| g.gap_type == GapType::Contradictory));
|
|
408
|
+
}
|
|
409
|
+
#[test]
|
|
410
|
+
fn manual_end_constructs_only_evidenced_assertions() {
|
|
411
|
+
let store = ApplicationContractStore::load(
|
|
412
|
+
std::env::temp_dir().join(format!("contract-end-{}.json", now())),
|
|
413
|
+
)
|
|
414
|
+
.unwrap();
|
|
415
|
+
store
|
|
416
|
+
.evaluate("app", "Track approved work", "user")
|
|
417
|
+
.unwrap();
|
|
418
|
+
assert!(store.construction_input("app").is_err());
|
|
419
|
+
store.end_session("app", "user").unwrap();
|
|
420
|
+
let input = store.construction_input("app").unwrap();
|
|
421
|
+
assert_eq!(input.len(), 1);
|
|
422
|
+
assert!(input.contains_key("outcome"));
|
|
423
|
+
assert!(!input.contains_key("entities"));
|
|
424
|
+
}
|
|
425
|
+
}
|