blob: f29853ca25cd091420a73089b278b58d5ba6f19d [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use maka_agent_facts::interaction::ClosureReason;
use maka_agent_facts::interaction::Decision;
use maka_agent_facts::interaction::GrantCapability;
use maka_agent_facts::interaction::GrantScope;
use maka_agent_facts::interaction::GrantTarget;
use maka_agent_facts::interaction::InteractionOutcome;
use maka_agent_facts::interaction::InteractionRecord;
use maka_agent_facts::interaction::InteractionRequest;
use maka_event_log::EventLog;
use sqlx::Connection;
#[tokio::test]
async fn permission_receipts_preserve_partial_scope_and_revoke_by_boundary_revision() {
use maka_agent_facts::event::Invocation;
use maka_agent_facts::interaction::PermissionRequest;
use maka_sandbox::{
Network,
filesystem::{Access, Rule},
grant,
};
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("permissions.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session(
"session",
"create",
&serde_json::json!({"boundary_revision": 4}),
1,
)
.await
.unwrap();
let invocation = Invocation {
session_id: "session".into(),
turn_id: "turn".into(),
run_id: "run".into(),
invocation_id: "invocation".into(),
};
let requested = grant::Permissions {
filesystem: vec![Rule::subtree(temp.path(), Access::Write)],
network: Network::Allowed,
};
let partial = grant::Permissions {
filesystem: vec![Rule::exact(temp.path().join("result"), Access::Read)],
network: Network::Denied,
};
let make_request = |id: &str| InteractionRecord {
request: InteractionRequest::Permissions {
tool_use_id: Some("tool-use".into()),
base_revision: 4,
request: PermissionRequest {
reason: "Read the requested result".into(),
command: None,
permissions: requested.clone(),
},
},
..request(id)
};
let mut complete = make_request("complete_receipt");
let InteractionRequest::Permissions { request, .. } = &mut complete.request else {
unreachable!()
};
request.permissions.filesystem = (0..32)
.map(|index| {
Rule::exact(
temp.path().join(format!("{index}{}", "x".repeat(300))),
Access::Read,
)
})
.collect();
let complete_permissions = request.permissions.clone();
// Keep this accepted receipt separate from the three scope-grant oracles below.
let InteractionRequest::Permissions { tool_use_id, .. } = &mut complete.request else {
unreachable!()
};
*tool_use_id = Some("complete-tool".into());
assert!(log.establish_interaction(&complete).await.unwrap().matches);
let complete_outcome = InteractionOutcome::PermissionsDecision {
decision: grant::Decision::Allow {
permissions: complete_permissions,
scope: grant::Scope::Once,
},
committed_at: 19,
};
assert!(
log.commit_interaction_outcome("complete_receipt", complete_outcome.clone())
.await
.unwrap()
.matches
);
assert!(
log.commit_interaction_outcome("complete_receipt", complete_outcome.clone())
.await
.unwrap()
.matches
);
let complete_record = log.interaction("complete_receipt").await.unwrap().unwrap();
assert_eq!(complete_record.request, complete.request);
assert_eq!(complete_record.outcome, Some(complete_outcome));
for (id, scope) in [
("once", grant::Scope::Once),
("turn_grant", grant::Scope::Turn),
("session_grant", grant::Scope::Session),
] {
log.establish_interaction(&make_request(id)).await.unwrap();
let outcome = InteractionOutcome::PermissionsDecision {
decision: grant::Decision::Allow {
permissions: partial.clone(),
scope,
},
committed_at: 20,
};
let committed = log.commit_interaction_outcome(id, outcome).await.unwrap();
assert!(committed.matches);
let retry = InteractionOutcome::PermissionsDecision {
decision: grant::Decision::Allow {
permissions: requested.clone(),
scope,
},
committed_at: 21,
};
assert!(
!log.commit_interaction_outcome(id, retry)
.await
.unwrap()
.matches,
"lost reply cannot broaden a partial approval"
);
}
let all = log
.permission_grants(&invocation, Some("tool-use"), 4)
.await
.unwrap();
assert_eq!(all.len(), 3);
assert!(all.iter().all(|grant| grant.permissions == partial));
assert_eq!(
log.permission_grants(&invocation, None, 4)
.await
.unwrap()
.len(),
2
);
assert_eq!(
log.permission_grants(&invocation, Some("other-tool"), 4)
.await
.unwrap()
.len(),
2
);
let other_run = Invocation {
run_id: "other-run".into(),
..invocation.clone()
};
assert_eq!(
log.permission_grants(&other_run, Some("tool-use"), 4)
.await
.unwrap()
.len(),
2
);
let other_turn = Invocation {
turn_id: "other-turn".into(),
..other_run
};
assert_eq!(
log.permission_grants(&other_turn, Some("tool-use"), 4)
.await
.unwrap()
.len(),
1
);
let other_session = Invocation {
session_id: "other-session".into(),
..invocation.clone()
};
assert!(
log.permission_grants(&other_session, Some("tool-use"), 4)
.await
.unwrap()
.is_empty()
);
log.establish_interaction(&make_request("pending"))
.await
.unwrap();
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_eq!(
log.interaction("complete_receipt").await.unwrap().unwrap(),
complete_record
);
assert_eq!(log.close_abandoned_interactions(30).await.unwrap(), 1);
assert_eq!(
log.permission_grants(&invocation, Some("tool-use"), 4)
.await
.unwrap(),
all
);
let late = InteractionOutcome::PermissionsDecision {
decision: grant::Decision::Allow {
permissions: requested.clone(),
scope: grant::Scope::Session,
},
committed_at: 31,
};
assert!(
!log.commit_interaction_outcome("pending", late)
.await
.unwrap()
.matches
);
log.establish_interaction(&make_request("stale"))
.await
.unwrap();
let session = log
.get_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap();
log.update_session_metadata(
"session",
session.revision,
|value: &mut serde_json::Value| {
value["boundary_revision"] = serde_json::json!(5);
Ok(())
},
)
.await
.unwrap();
let stale = InteractionOutcome::PermissionsDecision {
decision: grant::Decision::Allow {
permissions: partial,
scope: grant::Scope::Session,
},
committed_at: 32,
};
assert!(
log.commit_interaction_outcome("stale", stale)
.await
.is_err()
);
assert!(
log.interaction("stale")
.await
.unwrap()
.unwrap()
.outcome
.is_none()
);
assert!(
log.permission_grants(&invocation, Some("tool-use"), 4)
.await
.unwrap()
.is_empty()
);
assert!(
log.permission_grants(&invocation, Some("tool-use"), 5)
.await
.unwrap()
.is_empty()
);
assert!(
log.establish_interaction(&make_request("stale_request"))
.await
.is_err()
);
log.close().await.unwrap();
}
#[tokio::test]
async fn capability_approval_and_dispatch_share_the_original_permission_boundary() {
use maka_agent_facts::event::{
CommitError, EventWrite, Fact, Invocation, InvocationInput, RuntimeEvent,
};
use maka_agent_facts::tool_call::ToolCallIdentity;
use serde_json::{Value, json};
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("capability.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("session", "create", &json!({"boundary_revision": 0}), 1)
.await
.unwrap();
let invocation = Invocation {
session_id: "session".into(),
turn_id: "turn".into(),
run_id: "run".into(),
invocation_id: "invocation".into(),
};
let event = |fact| EventWrite::plain(RuntimeEvent::new(invocation.clone(), fact)).unwrap();
log.append(&event(Fact::InvocationOpened {
input: InvocationInput::Message {
content: "Computer Use".into(),
source_messages: vec![],
request_fingerprint: None,
},
configuration: None,
}))
.await
.unwrap();
let make_request = |id: &str, revision| {
let mut record = request(id);
if let InteractionRequest::ClientCapability { base_revision, .. } = &mut record.request {
*base_revision = Some(revision);
}
record
};
let original = make_request("original", 0);
let target = target(&original).clone();
log.establish_run_interaction(&invocation, &original)
.await
.unwrap();
assert!(
log.commit_interaction_outcome("original", decision(Decision::Allow, 20))
.await
.unwrap()
.matches
);
let dispatch = |operation: &str| {
event(Fact::ToolDispatched {
presentation: None,
title: None,
operation_id: operation.into(),
call: ToolCallIdentity::standalone(format!("{operation}-call")),
name: "cua_operation".into(),
input: json!({}),
})
};
log.append_capability_dispatch(&dispatch("before"), &target, 0)
.await
.unwrap();
log.establish_run_interaction(&invocation, &make_request("pending", 0))
.await
.unwrap();
let session = log.get_session::<Value>("session").await.unwrap().unwrap();
assert!(matches!(
log.update_session_permissions("session", session.revision, 30, |value: &mut Value| {
value["boundary_revision"] = json!(1);
Ok(())
})
.await
.unwrap(),
maka_event_log::sessions::SessionMutation::Committed(_)
));
assert!(
log.client_capability_grant("session", &target)
.await
.unwrap()
.is_none()
);
assert!(matches!(
log.append_capability_dispatch(&dispatch("revoked"), &target, 0)
.await,
Err(CommitError::PermissionRevoked)
));
let late = log
.commit_interaction_outcome("pending", decision(Decision::Allow, 31))
.await
.unwrap();
assert!(!late.matches);
assert!(matches!(
late.record.outcome,
Some(InteractionOutcome::Closure { .. })
));
assert!(
log.establish_run_interaction(&invocation, &make_request("stale", 0))
.await
.is_err()
);
assert!(
matches!(
log.interaction("original").await.unwrap().unwrap().outcome,
Some(InteractionOutcome::ClientCapabilityDecision {
decision: Decision::Allow,
..
})
),
"revocation does not rewrite the original answer"
);
log.establish_run_interaction(&invocation, &make_request("renewed", 1))
.await
.unwrap();
assert!(
log.commit_interaction_outcome("renewed", decision(Decision::Allow, 40))
.await
.unwrap()
.matches
);
assert_eq!(
log.client_capability_grant("session", &target)
.await
.unwrap()
.unwrap()
.base_revision,
Some(1)
);
assert!(
matches!(
log.append_capability_dispatch(&dispatch("stale_worker"), &target, 0)
.await,
Err(CommitError::PermissionRevoked)
),
"renewal cannot revive a worker captured under the old boundary"
);
log.append_capability_dispatch(&dispatch("after"), &target, 1)
.await
.unwrap();
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_eq!(
log.client_capability_grant("session", &target)
.await
.unwrap()
.unwrap()
.base_revision,
Some(1)
);
let events = log
.scoped_prefix(
maka_agent_facts::event::LogScope::Session {
id: "session".into(),
},
64,
1024 * 1024,
)
.await
.unwrap()
.events;
let admitted: Vec<_> = events
.iter()
.filter_map(|event| match &event.event.fact {
Fact::ToolDispatched { operation_id, .. } => Some(operation_id.as_str()),
_ => None,
})
.collect();
assert_eq!(admitted, ["before", "after"]);
log.close().await.unwrap();
}
fn request(id: &str) -> InteractionRecord {
InteractionRecord {
session_id: "session".into(),
turn_id: "turn".into(),
run_id: "run".into(),
request_id: id.into(),
created_at: 10,
request: InteractionRequest::ClientCapability {
tool_use_id: "tool-use".into(),
base_revision: None,
target: GrantTarget {
provider_id: "provider".into(),
contract_id: "contract".into(),
server_id: "desktop_browser".into(),
tool_name: "browser_navigate".into(),
capability: GrantCapability::Browser,
scope: GrantScope::BrowserOrigin {
origin: "https://example.com".into(),
},
},
},
outcome: None,
}
}
fn target(record: &InteractionRecord) -> &GrantTarget {
let InteractionRequest::ClientCapability { target, .. } = &record.request else {
panic!("expected capability request")
};
target
}
fn decision(value: Decision, time: u64) -> InteractionOutcome {
InteractionOutcome::ClientCapabilityDecision {
decision: value,
committed_at: time,
}
}
#[tokio::test]
async fn pending_session_pages_use_canonical_outcomes_and_isolate_continuation_scope() {
use maka_event_log::{StoreError, sessions::CatalogScope};
use serde_json::{Value, json};
let directory = tempfile::tempdir().unwrap();
let log = EventLog::open(&directory.path().join("pending.sqlite"))
.await
.unwrap();
for index in 0..35 {
let id = format!("session-{index:02}");
log.create_session(&id, &id, &json!({}), 1).await.unwrap();
if index < 34 {
let mut pending = request(&format!("request-{index}"));
pending.session_id = id;
log.establish_interaction(&pending).await.unwrap();
}
}
let mut second_request = request("second-request");
second_request.session_id = "session-33".into();
log.establish_interaction(&second_request).await.unwrap();
let page = log
.scoped_sessions::<Value>(
CatalogScope::Profile,
None,
None.into(),
maka_event_log::sessions::CatalogFilter {
archived_only: false,
include_archived: true,
pending_only: true,
},
)
.await
.unwrap();
assert_eq!(page.sessions.len(), 32);
assert!(
page.sessions
.iter()
.all(|row| row.pending_interaction_since == Some(10))
);
let cursor = page.next_cursor.as_deref();
for (scope, include_archived) in [
(CatalogScope::Profile, false),
(CatalogScope::IdPrefix("session-".into()), true),
(CatalogScope::Workspace("/other".into()), true),
] {
assert!(matches!(
log.scoped_sessions::<Value>(
scope,
Some(&page.revision),
cursor.into(),
maka_event_log::sessions::CatalogFilter {
include_archived,
archived_only: false,
pending_only: true
}
)
.await,
Err(StoreError::RevisionConflict { .. })
));
}
let tail = log
.scoped_sessions::<Value>(
CatalogScope::Profile,
Some(&page.revision),
cursor.into(),
maka_event_log::sessions::CatalogFilter {
archived_only: false,
include_archived: true,
pending_only: true,
},
)
.await
.unwrap();
assert_eq!(
tail.sessions
.iter()
.map(|row| row.id.as_str())
.collect::<Vec<_>>(),
["session-32", "session-33"]
);
assert!(tail.next_cursor.is_none());
assert!(matches!(
log.list_sessions::<Value>(Some(&page.revision), cursor, 32)
.await,
Err(StoreError::RevisionConflict { .. })
));
log.commit_interaction_outcome("request-0", decision(Decision::Deny, 20))
.await
.unwrap();
assert!(matches!(
log.scoped_sessions::<Value>(
CatalogScope::Profile,
Some(&page.revision),
cursor.into(),
maka_event_log::sessions::CatalogFilter {
archived_only: false,
include_archived: true,
pending_only: true
}
)
.await,
Err(StoreError::RevisionConflict { .. })
));
log.commit_interaction_outcome("request-33", decision(Decision::Deny, 20))
.await
.unwrap();
assert!(
log.get_session::<Value>("session-33")
.await
.unwrap()
.unwrap()
.pending_interaction_since
.is_some()
);
log.commit_interaction_outcome("second-request", decision(Decision::Deny, 20))
.await
.unwrap();
let fresh = log
.scoped_sessions::<Value>(
CatalogScope::Profile,
None,
None.into(),
maka_event_log::sessions::CatalogFilter {
archived_only: false,
include_archived: true,
pending_only: true,
},
)
.await
.unwrap();
assert_eq!(fresh.sessions.len(), 32);
assert!(fresh.next_cursor.is_none());
assert_eq!(fresh.sessions.first().unwrap().id, "session-01");
assert_eq!(fresh.sessions.last().unwrap().id, "session-32");
log.close().await.unwrap();
}
#[tokio::test]
async fn first_outcome_and_atomic_grant_survive_conflicts_faults_and_reopen() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("runtime.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("session", "create", &serde_json::json!({}), 1)
.await
.unwrap();
let mut wake = log.subscribe_commits();
let initial = log
.get_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap();
let candidate = request("request");
assert!(log.establish_interaction(&candidate).await.unwrap().matches);
let waiting = log
.get_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap();
assert_eq!(waiting.revision, initial.revision + 1);
let version = log.observation_versions(&["session".into()]).await.unwrap()["session"];
assert_eq!(
version.metadata, waiting.revision,
"pending interactions must invalidate observation without an execution event"
);
assert_eq!(version.event, 0);
assert_eq!(
waiting.pending_interaction_since,
Some(candidate.created_at)
);
wake.changed().await.unwrap();
assert_eq!(
*wake.borrow_and_update(),
0,
"interaction is not an execution event"
);
let mut conflict = candidate.clone();
conflict.run_id = "other_run".into();
let result = log.establish_interaction(&conflict).await.unwrap();
assert!(!result.matches);
assert_eq!(result.record, candidate);
assert_eq!(
log.get_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap(),
waiting
);
assert_eq!(
log.pending_interactions("session").await.unwrap(),
std::slice::from_ref(&candidate)
);
let observation = log
.observe_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap();
assert_eq!(
observation.pending_interactions,
std::slice::from_ref(&candidate)
);
assert_eq!(observation.through_sequence, 0);
// Failure between outcome insertion and grant insertion must roll back both.
let mut observer = sqlx::SqliteConnection::connect_with(
&sqlx::sqlite::SqliteConnectOptions::new().filename(&path),
)
.await
.unwrap();
sqlx::raw_sql(
"CREATE TRIGGER fail_grant BEFORE INSERT ON client_capability_session_grants
BEGIN SELECT RAISE(ABORT, 'injected grant failure'); END;",
)
.execute(&mut observer)
.await
.unwrap();
assert!(
log.commit_interaction_outcome("request", decision(Decision::Allow, 20))
.await
.is_err()
);
assert_eq!(
log.interaction("request").await.unwrap(),
Some(candidate.clone())
);
assert!(
log.client_capability_grant("session", target(&candidate))
.await
.unwrap()
.is_none()
);
assert!(!wake.has_changed().unwrap());
assert_eq!(
log.get_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap(),
waiting
);
sqlx::raw_sql("DROP TRIGGER fail_grant")
.execute(&mut observer)
.await
.unwrap();
let (allow, deny) = tokio::join!(
log.commit_interaction_outcome("request", decision(Decision::Allow, 21)),
log.commit_interaction_outcome("request", decision(Decision::Deny, 22)),
);
let (allow, deny) = (allow.unwrap(), deny.unwrap());
assert_ne!(allow.matches, deny.matches);
assert_eq!(allow.record, deny.record);
let canonical = allow.record;
let settled = log
.get_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap();
assert_eq!(settled.revision, waiting.revision + 1);
assert_eq!(settled.pending_interaction_since, None);
let allowed = matches!(
canonical.outcome,
Some(InteractionOutcome::ClientCapabilityDecision {
decision: Decision::Allow,
..
})
);
let grant = log
.client_capability_grant("session", target(&candidate))
.await
.unwrap();
assert_eq!(grant.is_some(), allowed);
if let Some(grant) = &grant {
assert_eq!(
grant.granted_at,
canonical.outcome.as_ref().unwrap().committed_at()
);
assert_eq!(grant.target, *target(&candidate));
}
let repeat = log
.commit_interaction_outcome(
"request",
decision(
if allowed {
Decision::Allow
} else {
Decision::Deny
},
99,
),
)
.await
.unwrap();
assert!(
repeat.matches,
"timestamps do not change semantic answer identity"
);
assert_eq!(repeat.record, canonical);
assert!(log.establish_interaction(&candidate).await.unwrap().matches);
assert!(
log.pending_interactions("session")
.await
.unwrap()
.is_empty()
);
assert!(log.prefix(32, 65536).await.unwrap().events.is_empty());
let projection = log
.session_projection::<serde_json::Value>("session")
.await
.unwrap()
.unwrap();
assert!(projection.pending_interactions.is_empty());
assert_eq!(projection.through_sequence, observation.through_sequence);
observer.close().await.unwrap();
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_eq!(log.interaction("request").await.unwrap(), Some(canonical));
assert_eq!(
log.client_capability_grant("session", target(&candidate))
.await
.unwrap(),
grant
);
log.close().await.unwrap();
}
#[tokio::test]
async fn closure_cannot_grant_and_browser_authority_is_session_provider_contract_origin() {
let temp = tempfile::tempdir().unwrap();
let log = EventLog::open(&temp.path().join("runtime.sqlite"))
.await
.unwrap();
let closed = request("closed");
log.establish_interaction(&closed).await.unwrap();
let closure = InteractionOutcome::Closure {
reason: ClosureReason::ProviderDisconnected,
committed_at: 30,
};
log.commit_interaction_outcome("closed", closure.clone())
.await
.unwrap();
let late = log
.commit_interaction_outcome("closed", decision(Decision::Allow, 31))
.await
.unwrap();
assert!(!late.matches);
assert_eq!(late.record.outcome, Some(closure));
assert!(
log.client_capability_grant("session", target(&closed))
.await
.unwrap()
.is_none()
);
let allowed = request("allowed");
log.establish_interaction(&allowed).await.unwrap();
log.commit_interaction_outcome("allowed", decision(Decision::Allow, 40))
.await
.unwrap();
let mut other_tool = target(&allowed).clone();
other_tool.tool_name = "browser_snapshot".into();
assert!(
log.client_capability_grant("session", &other_tool)
.await
.unwrap()
.is_some()
);
assert!(
log.client_capability_grant("other_session", &other_tool)
.await
.unwrap()
.is_none()
);
for variant in 0..3 {
let mut changed = other_tool.clone();
match variant {
0 => changed.provider_id = "other_provider".into(),
1 => changed.contract_id = "other_contract".into(),
_ => {
changed.scope = GrantScope::BrowserOrigin {
origin: "https://other.example".into(),
}
}
}
assert!(
log.client_capability_grant("session", &changed)
.await
.unwrap()
.is_none()
);
}
let later = request("later");
log.establish_interaction(&later).await.unwrap();
log.commit_interaction_outcome("later", decision(Decision::Allow, 50))
.await
.unwrap();
assert_eq!(
log.client_capability_grant("session", target(&later))
.await
.unwrap()
.unwrap()
.granted_at,
40
);
let mut invalid = request("invalid");
invalid.outcome = Some(decision(Decision::Allow, 60));
assert!(log.establish_interaction(&invalid).await.is_err());
assert!(log.interaction("invalid").await.unwrap().is_none());
log.close().await.unwrap();
}
#[tokio::test]
async fn permission_metadata_cas_closes_only_old_pending_requests_and_notifies_waiters() {
use maka_agent_facts::interaction::PermissionRequest;
use maka_event_log::sessions::SessionMutation;
use maka_sandbox::{Network, grant};
use serde_json::{Value, json};
let temp = tempfile::tempdir().unwrap();
let log = EventLog::open(&temp.path().join("permission-cas.sqlite"))
.await
.unwrap();
for session in ["session", "other"] {
log.create_session(session, session, &json!({"boundary_revision": 4}), 1)
.await
.unwrap();
}
let permissions = |id: &str, session: &str| InteractionRecord {
session_id: session.into(),
request: InteractionRequest::Permissions {
tool_use_id: Some("tool-use".into()),
base_revision: 4,
request: PermissionRequest {
reason: "Exact write".into(),
command: None,
permissions: grant::Permissions {
filesystem: vec![],
network: Network::Denied,
},
},
},
..request(id)
};
for record in [
permissions("waiting", "session"),
permissions("settled", "session"),
permissions("foreign", "other"),
request("capability"),
] {
log.establish_interaction(&record).await.unwrap();
}
let winner = InteractionOutcome::PermissionsDecision {
decision: grant::Decision::Deny,
committed_at: 12,
};
log.commit_interaction_outcome("settled", winner.clone())
.await
.unwrap();
let current = log.get_session::<Value>("session").await.unwrap().unwrap();
assert!(matches!(
log.update_session_permissions::<Value, _>(
"session",
current.revision + 1,
20,
|_| panic!("CAS conflict must not run update")
)
.await
.unwrap(),
SessionMutation::RevisionConflict { .. }
));
log.update_session_permissions::<Value, _>("session", current.revision, 20, |_| Ok(()))
.await
.unwrap();
assert!(
log.interaction("waiting")
.await
.unwrap()
.unwrap()
.outcome
.is_none()
);
let mut commits = log.subscribe_commits();
let changed = log
.update_session_permissions::<Value, _>("session", current.revision, 21, |value| {
value["boundary_revision"] = json!(5);
Ok(())
})
.await
.unwrap();
assert!(
matches!(changed, SessionMutation::Committed(ref session) if session.configuration["boundary_revision"] == 5)
);
tokio::time::timeout(std::time::Duration::from_secs(1), commits.changed())
.await
.unwrap()
.unwrap();
let closed = log.interaction("waiting").await.unwrap().unwrap();
assert_eq!(
closed.outcome,
Some(InteractionOutcome::Closure {
reason: ClosureReason::ProducerCancelled,
committed_at: 21
})
);
assert_eq!(
log.interaction("settled").await.unwrap().unwrap().outcome,
Some(winner)
);
for id in ["foreign", "capability"] {
assert!(
log.interaction(id)
.await
.unwrap()
.unwrap()
.outcome
.is_none()
);
}
// A malformed boundary update rolls back configuration and outcome changes together.
let current = log.get_session::<Value>("session").await.unwrap().unwrap();
assert!(
log.update_session_permissions::<Value, _>("session", current.revision, 22, |value| {
value["boundary_revision"] = json!(3);
Ok(())
})
.await
.is_err()
);
assert_eq!(
log.get_session::<Value>("session")
.await
.unwrap()
.unwrap()
.configuration["boundary_revision"],
5
);
log.create_session("missing", "missing", &json!({}), 1)
.await
.unwrap();
let missing = log.get_session::<Value>("missing").await.unwrap().unwrap();
assert!(
log.update_session_permissions::<Value, _>("missing", missing.revision, 22, |value| {
value["boundary_revision"] = json!(1);
Ok(())
})
.await
.is_err()
);
assert_eq!(
log.get_session::<Value>("missing")
.await
.unwrap()
.unwrap()
.configuration,
json!({})
);
}