1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//! `RuntimeScope` — per-execution caller identity surface for the
//! multi-tenant work tracked in Parslee-ai/car#187.
//!
//! ## What this is
//!
//! A small value the dispatcher attaches to each `Runtime::execute_*`
//! invocation so the runtime knows **on whose behalf** an
//! `ActionProposal` is being executed. The fields mirror — and are
//! sourced from — the proposal-context surfaces #187 phase 1 and
//! phase 2 already populate:
//!
//! - `caller_id` — typically `proposal.context["a2a_caller_verified"].subject`
//! when an `AuthValidator` returns an `Identity`, else
//! `proposal.context["a2a_caller"].caller_id` (cooperative-peer hint).
//! - `tenant_id` — verified-tenant claim if present, else the
//! cooperative `a2a_caller.tenant_id` hint, else `None`.
//! - `claims` — bag of verified token claims the dispatcher chose to
//! forward (subject is already on `caller_id`; this carries the
//! surrounding metadata like `iss`, `aud`, `org_id`, etc.).
//!
//! ## What this PR enforces vs. what's deferred
//!
//! This first PR of #187 phase 3 is **foundation only**:
//!
//! - ✅ `Runtime::execute_scoped` / `execute_scoped_with_cancel` accept
//! a `&RuntimeScope` and record it on the per-execution event log
//! (each `ActionInvoked` event carries `caller_id` / `tenant_id`
//! so downstream audit / log analysis sees who issued what).
//! - ✅ The car-a2a dispatcher derives a scope from the verified
//! `Identity` (when present) and the cooperative `a2a_caller`
//! metadata, and calls the scoped entry point. Default-deny
//! activates when a non-`NoAuth` validator is configured but no
//! `Identity` surfaces (e.g. token validated but subject missing).
//!
//! - ❌ **Memgine queries are NOT yet scoped.** Facts, skills, and
//! working-set retrieval still hit the global graph regardless of
//! `scope.tenant_id`. Tracked as the next PR in #187 phase 3.
//! - ❌ **State keys are NOT yet namespaced.** `state.get` /
//! `state.set` still live in a flat KV space. Tracked separately;
//! needs careful migration design for existing persisted state.
//! - ❌ **FFI parity is partial.** WS dispatches that flow through
//! the car-a2a bridge get scope automatically; the NAPI / PyO3
//! `execute_proposal` standalone functions don't accept a scope
//! parameter yet. Adding that is one of the issue's acceptance
//! criteria and lands in a follow-up.
//!
//! Tools and policies that want per-tenant behaviour today can still
//! read `proposal.context["a2a_caller_verified"]` directly — the
//! phase 1/2 surfaces remain. `RuntimeScope` is the structured
//! handle the runtime itself uses; tool handlers should keep reading
//! the proposal context.
use BTreeMap;
use ;
use Value;
/// Per-execution identity surface (Parslee-ai/car#187 phase 3).
///
/// Constructed by the dispatcher from caller-identity surfaces on
/// the inbound `ActionProposal`; threaded through `Runtime::execute_*`
/// so downstream layers (memgine, state, audit log) can route on
/// `caller_id` / `tenant_id`. See the module docstring for what's
/// enforced in this PR vs. deferred.
///
/// Designed to be cheap to clone — small fixed fields plus a
/// `BTreeMap` of arbitrary claims. The dispatcher typically builds
/// one per inbound message, so allocations are bounded.
///
/// `Default` returns the unscoped (all-`None`) value. The runtime
/// treats this as "no identity" — same legacy behaviour as the
/// pre-#187 path. The non-scoped `Runtime::execute_with_cancel`
/// entry point internally passes `&RuntimeScope::default()` so
/// existing in-process callers see no behaviour change.