Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ milestone.
| **Every Ubuntu LTS validated** — 22.04, 24.04 and 26.04 all at 79/79, each with a replay twin that reproduces it | ✅ |
| Telegram approval interface | 📋 roadmap |

**1,873 Rust tests and 72 frontend tests** form the current deterministic
**1,883 Rust tests and 72 frontend tests** form the current deterministic
release baseline.

## Configure your LLM
Expand Down
448 changes: 448 additions & 0 deletions crates/sysknife-daemon/src/audit_chain.rs

Large diffs are not rendered by default.

67 changes: 54 additions & 13 deletions crates/sysknife-daemon/src/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1626,7 +1626,11 @@ async fn handle_approve(
{
return Ok(());
}
let receipt = match state.audit.approve_transaction(transaction_id).await {
let receipt = match state
.audit
.approve_transaction(transaction_id, caller.principal())
.await
{
Ok(receipt) => receipt,
// A `DatabaseInvariant` here means the stored approval commitment does
// not match the signed preview (tamper / key mismatch) — a fail-closed
Expand Down Expand Up @@ -1671,7 +1675,11 @@ async fn handle_approve(
)
.await;
if response.is_err() {
if let Err(e) = state.audit.revoke_unconsumed_approval(transaction_id).await {
if let Err(e) = state
.audit
.revoke_unconsumed_approval(transaction_id, caller.principal())
.await
{
eprintln!(
"[sysknife-daemon] failed to revoke undelivered approval for \
{transaction_id}: {e}"
Expand Down Expand Up @@ -1816,7 +1824,11 @@ async fn handle_cancel(
{
return Ok(());
}
match state.audit.cancel_queued(transaction_id).await {
match state
.audit
.cancel_queued(transaction_id, caller.principal())
.await
{
Ok(true) => {
send_response(
framed,
Expand Down Expand Up @@ -2865,7 +2877,11 @@ async fn handle_execute(

let claimed = match state
.audit
.claim_approved_for_execution(transaction_id, &receipt_digest(approval_receipt))
.claim_approved_for_execution(
transaction_id,
&receipt_digest(approval_receipt),
caller.principal(),
)
.await
{
Ok(c) => c,
Expand Down Expand Up @@ -4764,7 +4780,11 @@ mod tests {
// Claim it (Queued -> Running) so it is in-flight from the store's view.
assert!(state
.audit
.claim_approved_for_execution(&transaction_id, &receipt_digest(&receipt))
.claim_approved_for_execution(
&transaction_id,
&receipt_digest(&receipt),
CallerPrincipal::Uid(1000),
)
.await
.unwrap());

Expand Down Expand Up @@ -4894,7 +4914,11 @@ mod tests {
preview_and_approve(&mut framed, "GetMemoryInfo", json!({})).await;
assert!(state
.audit
.claim_approved_for_execution(&transaction_id, &receipt_digest(&receipt))
.claim_approved_for_execution(
&transaction_id,
&receipt_digest(&receipt),
CallerPrincipal::Uid(1000),
)
.await
.unwrap());

Expand Down Expand Up @@ -5579,7 +5603,11 @@ mod tests {
);
assert!(state
.audit
.claim_approved_for_execution(&transaction_id, &receipt_digest(&receipt))
.claim_approved_for_execution(
&transaction_id,
&receipt_digest(&receipt),
CallerPrincipal::Uid(1000),
)
.await
.unwrap());
}
Expand Down Expand Up @@ -5633,7 +5661,11 @@ mod tests {
);
assert!(state
.audit
.claim_approved_for_execution(&transaction_id, &receipt_digest(&receipt))
.claim_approved_for_execution(
&transaction_id,
&receipt_digest(&receipt),
CallerPrincipal::Uid(1000),
)
.await
.unwrap());
}
Expand Down Expand Up @@ -5843,27 +5875,36 @@ mod tests {
async fn approve_transaction(
&self,
id: &str,
approver: CallerPrincipal,
) -> Result<Option<String>, TransactionStoreError> {
self.0.approve_transaction(id).await
self.0.approve_transaction(id, approver).await
}
async fn revoke_unconsumed_approval(
&self,
id: &str,
revoker: CallerPrincipal,
) -> Result<bool, TransactionStoreError> {
self.0.revoke_unconsumed_approval(id).await
self.0.revoke_unconsumed_approval(id, revoker).await
}
async fn claim_approved_for_execution(
&self,
id: &str,
digest: &str,
executor: CallerPrincipal,
) -> Result<bool, TransactionStoreError> {
self.0.claim_approved_for_execution(id, digest).await
self.0
.claim_approved_for_execution(id, digest, executor)
.await
}
async fn cleanup_stale_queued(&self) -> Result<u64, TransactionStoreError> {
self.0.cleanup_stale_queued().await
}
async fn cancel_queued(&self, id: &str) -> Result<bool, TransactionStoreError> {
self.0.cancel_queued(id).await
async fn cancel_queued(
&self,
id: &str,
canceller: CallerPrincipal,
) -> Result<bool, TransactionStoreError> {
self.0.cancel_queued(id, canceller).await
}
async fn list_transactions(
&self,
Expand Down
27 changes: 21 additions & 6 deletions crates/sysknife-daemon/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ use std::sync::Arc;
use sysknife_types::{JobState, PreviewEnvelope, TransactionRecord};

use crate::audit_chain::{AuditKey, ChainRow, EventRow, VerifyOutcome};
use crate::auth::CallerPrincipal;
use crate::transactions::{
NewTransaction, RecordedPreviewedTransaction, TransactionStore, TransactionStoreError,
};
Expand Down Expand Up @@ -110,17 +111,20 @@ pub trait AuditStore: Send + Sync + std::fmt::Debug {
async fn approve_transaction(
&self,
transaction_id: &str,
approver: CallerPrincipal,
) -> Result<Option<String>, TransactionStoreError>;

async fn revoke_unconsumed_approval(
&self,
transaction_id: &str,
revoker: CallerPrincipal,
) -> Result<bool, TransactionStoreError>;

async fn claim_approved_for_execution(
&self,
transaction_id: &str,
receipt_digest: &str,
executor: CallerPrincipal,
) -> Result<bool, TransactionStoreError>;

async fn cleanup_stale_queued(&self) -> Result<u64, TransactionStoreError>;
Expand Down Expand Up @@ -167,7 +171,11 @@ pub trait AuditStore: Send + Sync + std::fmt::Debug {
/// `true` iff a queued row was transitioned; a `Running` (in-flight) or
/// terminal transaction is never cancelled. See
/// [`crate::transactions::TransactionStore::cancel_queued`].
async fn cancel_queued(&self, transaction_id: &str) -> Result<bool, TransactionStoreError>;
async fn cancel_queued(
&self,
transaction_id: &str,
canceller: CallerPrincipal,
) -> Result<bool, TransactionStoreError>;

async fn list_transactions(
&self,
Expand Down Expand Up @@ -321,41 +329,48 @@ impl AuditStore for SqliteStore {
async fn approve_transaction(
&self,
transaction_id: &str,
approver: CallerPrincipal,
) -> Result<Option<String>, TransactionStoreError> {
let inner = Arc::clone(&self.inner);
let id = transaction_id.to_string();
blocking(move || inner.approve_transaction(&id)).await
blocking(move || inner.approve_transaction(&id, approver)).await
}

async fn revoke_unconsumed_approval(
&self,
transaction_id: &str,
revoker: CallerPrincipal,
) -> Result<bool, TransactionStoreError> {
let inner = Arc::clone(&self.inner);
let id = transaction_id.to_string();
blocking(move || inner.revoke_unconsumed_approval(&id)).await
blocking(move || inner.revoke_unconsumed_approval(&id, revoker)).await
}

async fn claim_approved_for_execution(
&self,
transaction_id: &str,
receipt_digest: &str,
executor: CallerPrincipal,
) -> Result<bool, TransactionStoreError> {
let inner = Arc::clone(&self.inner);
let id = transaction_id.to_string();
let digest = receipt_digest.to_string();
blocking(move || inner.claim_approved_for_execution(&id, &digest)).await
blocking(move || inner.claim_approved_for_execution(&id, &digest, executor)).await
}

async fn cleanup_stale_queued(&self) -> Result<u64, TransactionStoreError> {
let inner = Arc::clone(&self.inner);
blocking(move || inner.cleanup_stale_queued()).await
}

async fn cancel_queued(&self, transaction_id: &str) -> Result<bool, TransactionStoreError> {
async fn cancel_queued(
&self,
transaction_id: &str,
canceller: CallerPrincipal,
) -> Result<bool, TransactionStoreError> {
let inner = Arc::clone(&self.inner);
let id = transaction_id.to_string();
blocking(move || inner.cancel_queued(&id)).await
blocking(move || inner.cancel_queued(&id, canceller)).await
}

async fn list_transactions(
Expand Down
Loading
Loading