aac-cli 0.2.11__tar.gz → 0.2.12__tar.gz
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.
- {aac_cli-0.2.11/aac_cli.egg-info → aac_cli-0.2.12}/PKG-INFO +9 -8
- {aac_cli-0.2.11 → aac_cli-0.2.12}/README.md +8 -7
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/__init__.py +1 -1
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/cli.py +5 -2
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/data/renderer-source.json +7 -7
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/from_sidecar.py +4 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/inputs.py +2 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/model.py +97 -6
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/provenance.py +22 -6
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/visualizer.py +51 -8
- {aac_cli-0.2.11 → aac_cli-0.2.12/aac_cli.egg-info}/PKG-INFO +9 -8
- {aac_cli-0.2.11 → aac_cli-0.2.12}/pyproject.toml +1 -1
- {aac_cli-0.2.11 → aac_cli-0.2.12}/LICENSE +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/MANIFEST.in +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/NOTICE +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/THIRD_PARTY_NOTICES.md +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/__main__.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/__init__.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/data/action-taken-v1.json +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/data/predicates-v1.json +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/narrative.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/schemas.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/temporal.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/theme.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/timeline_source.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/admin_key_pem.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/aeg_cli.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/aeg_list.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/agent_cli.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/agent_config.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/agent_health.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/agent_layout.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/agent_record.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/api_key_rotation_state.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/chain_list.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/cli.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/config.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/config_render.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/dev_material.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/idp_recovery.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/init_cli.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/interrupts.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/material_cases.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/profiles.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/reference.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/registration_state.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/safe_output.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/secure_files.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/sso_login.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/supplied_material.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/trace_query.py +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli.egg-info/SOURCES.txt +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli.egg-info/dependency_links.txt +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli.egg-info/requires.txt +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli.egg-info/top_level.txt +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/launchers/aac +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/launchers/aeg +0 -0
- {aac_cli-0.2.11 → aac_cli-0.2.12}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aac-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.12
|
|
4
4
|
Summary: The aac platform CLI — headless operator interface to the AAC control plane (tenant registration, chain audit).
|
|
5
5
|
Author: Agent Authority Cloud Project
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -28,9 +28,9 @@ Dynamic: license-file
|
|
|
28
28
|
# aac-cli — the `aac` command
|
|
29
29
|
|
|
30
30
|
`aac` is the command-line tool for tenants of Agent Authority Cloud (AAC).
|
|
31
|
-
AAC lets an
|
|
32
|
-
which tenant they belong to and what they were
|
|
33
|
-
**tenant** is one
|
|
31
|
+
AAC lets an organization's AI agents and services prove, on every request,
|
|
32
|
+
which tenant they belong to and what they were authorized to do. A
|
|
33
|
+
**tenant** is one organization registered with AAC. Each of its workloads
|
|
34
34
|
runs beside an AAC sidecar that signs outgoing requests and verifies
|
|
35
35
|
incoming ones against public trust material the tenant publishes.
|
|
36
36
|
|
|
@@ -69,7 +69,8 @@ pip install aac-cli
|
|
|
69
69
|
aac --version
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
Supported platforms are macOS, Linux and Linux Docker containers. Python 3.11
|
|
73
|
+
to 3.14 is supported. Release checks test all four versions.
|
|
73
74
|
|
|
74
75
|
> **Mind the name.** `pip install aac` installs an unrelated project that
|
|
75
76
|
> happens to share the acronym. The AAC package is **`aac-cli`**; the
|
|
@@ -756,7 +757,7 @@ stored value.
|
|
|
756
757
|
|
|
757
758
|
`tenant_id` is managed for you. Profile commands display it but never ask
|
|
758
759
|
for it; only `aac tenant register`, `aac init` and `aac sso login` write it.
|
|
759
|
-
It is your
|
|
760
|
+
It is your organization's identifier in the federation, allocated by AAC at
|
|
760
761
|
registration in the form `tnt-<uuid>`, and it never changes.
|
|
761
762
|
A binding makes that tenant the profile's stored default; it does not grant
|
|
762
763
|
membership or configure an identity provider. Successful sign-in to an existing
|
|
@@ -953,12 +954,12 @@ code.
|
|
|
953
954
|
Enterprise tenants are registered through an onboarding ceremony: AAC
|
|
954
955
|
operations opens a registration window and hands the tenant operator a
|
|
955
956
|
bootstrap token, which the commands below read from `--bootstrap-token` or
|
|
956
|
-
the `AAC_BOOTSTRAP_TOKEN` environment variable. The token
|
|
957
|
+
the `AAC_BOOTSTRAP_TOKEN` environment variable. The token authorizes only
|
|
957
958
|
the onboarding steps shown with it; every other administration command runs
|
|
958
959
|
under a signed-in session.
|
|
959
960
|
|
|
960
961
|
For enterprise sign-in, AAC supports Microsoft Entra ID (also known as
|
|
961
|
-
Azure Active Directory or Azure AD). If your
|
|
962
|
+
Azure Active Directory or Azure AD). If your organization needs another
|
|
962
963
|
identity provider, contact support@cascadeauth.com.
|
|
963
964
|
|
|
964
965
|
### Register the tenant
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# aac-cli — the `aac` command
|
|
2
2
|
|
|
3
3
|
`aac` is the command-line tool for tenants of Agent Authority Cloud (AAC).
|
|
4
|
-
AAC lets an
|
|
5
|
-
which tenant they belong to and what they were
|
|
6
|
-
**tenant** is one
|
|
4
|
+
AAC lets an organization's AI agents and services prove, on every request,
|
|
5
|
+
which tenant they belong to and what they were authorized to do. A
|
|
6
|
+
**tenant** is one organization registered with AAC. Each of its workloads
|
|
7
7
|
runs beside an AAC sidecar that signs outgoing requests and verifies
|
|
8
8
|
incoming ones against public trust material the tenant publishes.
|
|
9
9
|
|
|
@@ -42,7 +42,8 @@ pip install aac-cli
|
|
|
42
42
|
aac --version
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Supported platforms are macOS, Linux and Linux Docker containers. Python 3.11
|
|
46
|
+
to 3.14 is supported. Release checks test all four versions.
|
|
46
47
|
|
|
47
48
|
> **Mind the name.** `pip install aac` installs an unrelated project that
|
|
48
49
|
> happens to share the acronym. The AAC package is **`aac-cli`**; the
|
|
@@ -729,7 +730,7 @@ stored value.
|
|
|
729
730
|
|
|
730
731
|
`tenant_id` is managed for you. Profile commands display it but never ask
|
|
731
732
|
for it; only `aac tenant register`, `aac init` and `aac sso login` write it.
|
|
732
|
-
It is your
|
|
733
|
+
It is your organization's identifier in the federation, allocated by AAC at
|
|
733
734
|
registration in the form `tnt-<uuid>`, and it never changes.
|
|
734
735
|
A binding makes that tenant the profile's stored default; it does not grant
|
|
735
736
|
membership or configure an identity provider. Successful sign-in to an existing
|
|
@@ -926,12 +927,12 @@ code.
|
|
|
926
927
|
Enterprise tenants are registered through an onboarding ceremony: AAC
|
|
927
928
|
operations opens a registration window and hands the tenant operator a
|
|
928
929
|
bootstrap token, which the commands below read from `--bootstrap-token` or
|
|
929
|
-
the `AAC_BOOTSTRAP_TOKEN` environment variable. The token
|
|
930
|
+
the `AAC_BOOTSTRAP_TOKEN` environment variable. The token authorizes only
|
|
930
931
|
the onboarding steps shown with it; every other administration command runs
|
|
931
932
|
under a signed-in session.
|
|
932
933
|
|
|
933
934
|
For enterprise sign-in, AAC supports Microsoft Entra ID (also known as
|
|
934
|
-
Azure Active Directory or Azure AD). If your
|
|
935
|
+
Azure Active Directory or Azure AD). If your organization needs another
|
|
935
936
|
identity provider, contact support@cascadeauth.com.
|
|
936
937
|
|
|
937
938
|
### Register the tenant
|
|
@@ -204,15 +204,18 @@ def _render(args, events, query: Query, sources: list[SourceInput]) -> int:
|
|
|
204
204
|
if args.output.resolve() in {path.resolve() for path in protected}:
|
|
205
205
|
raise InputError("--output must not overwrite an evidence input")
|
|
206
206
|
visual = build_visual_aeg(graph)
|
|
207
|
-
visual.provenance = summarize_sources(
|
|
207
|
+
visual.provenance = summarize_sources(
|
|
208
|
+
sources, selected, central_status=central_status, evidence_complete=graph.evidence_complete
|
|
209
|
+
)
|
|
208
210
|
with tempfile.TemporaryDirectory(prefix="aac-aeg-") as directory:
|
|
209
211
|
rendered = write_html_aeg(visual, root, out_dir=Path(directory))
|
|
210
212
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
211
213
|
args.output.write_text(rendered.read_text(encoding="utf-8"), encoding="utf-8")
|
|
212
214
|
for diagnostic in diagnostics:
|
|
213
215
|
print(diagnostic, file=sys.stderr)
|
|
216
|
+
coverage = "" if graph.evidence_complete else "; partial evidence"
|
|
214
217
|
print(
|
|
215
|
-
f"AEG written to {args.output} ({len(graph.tokens)} observed/referenced tokens
|
|
218
|
+
f"AEG written to {args.output} ({len(graph.tokens)} observed/referenced tokens{coverage})",
|
|
216
219
|
file=sys.stderr,
|
|
217
220
|
)
|
|
218
221
|
return 4 if query_failed else 0
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
|
-
"content_sha256": "
|
|
2
|
+
"content_sha256": "f45099fcfa1d1ceca5f31244dfa526ea096094ced6cfc7faf27cc1babe482163",
|
|
3
3
|
"files": {
|
|
4
4
|
"__init__.py": "80d80a1ba35353d271cddf02532801c60b57f5ec06fb268ae823747742ce8979",
|
|
5
|
-
"cli.py": "
|
|
5
|
+
"cli.py": "a180a171b4d1f819fd96d0821394eee19b45fafddddbd50c74778af6ac3d5fb0",
|
|
6
6
|
"data/action-taken-v1.json": "d863343afe7f314646a73cf063480a7e650f93857d2ed5ec3a7b1f9838671fdf",
|
|
7
7
|
"data/predicates-v1.json": "35e83a29feada10e8cfbef7009245146a9717d4f4fd58d74124e0ec91ae922c8",
|
|
8
|
-
"from_sidecar.py": "
|
|
9
|
-
"inputs.py": "
|
|
10
|
-
"model.py": "
|
|
8
|
+
"from_sidecar.py": "8671c3e0efe9bfe9ecf9be057b70e245ca8d0d645b6bad8819f6fb1b882e255e",
|
|
9
|
+
"inputs.py": "047a6b7dd788e04c49e10f090de9336ed3870e754b547c469371369d27b55b8b",
|
|
10
|
+
"model.py": "715d0e92c3aafd9e0a194a74b4df7354bd24d1bf815767d590cf245da3d60018",
|
|
11
11
|
"narrative.py": "8ac4b12add84cc8be907cff5f25e58731ab1144888aec8d376e286de461a6179",
|
|
12
|
-
"provenance.py": "
|
|
12
|
+
"provenance.py": "3f611a8c129a2856e2cf76561bde29dc1884719083dfdddea72810e026946b90",
|
|
13
13
|
"schemas.py": "c47e0138fb8bfeaf1db1725a00d4a37ff4a136a9e8f5907356b1006f301a9ded",
|
|
14
14
|
"temporal.py": "b69081e9280d578dc1261a276da3178e979a02efaf1137f720b4520ed543a36d",
|
|
15
15
|
"theme.py": "ded47be13f5c591da95a1bb91ee1b665c1570c929d842bdaa1a41325d0b40caa",
|
|
16
16
|
"timeline_source.py": "dc875458d69c6135b3ffe0db9c35e511da603b5d52409f780f3e1b74dc464520",
|
|
17
|
-
"visualizer.py": "
|
|
17
|
+
"visualizer.py": "3213a1cbfe901f320c687b5eafcc09bed46bd2270c6cd427a7f74472c80a3bfe"
|
|
18
18
|
},
|
|
19
19
|
"legal": {
|
|
20
20
|
"LICENSE": "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594",
|
|
@@ -463,6 +463,9 @@ def _translate_mint(
|
|
|
463
463
|
"tenant_id": sidecar_event.get("tenant_id"),
|
|
464
464
|
"presenter_spiffe_id": sidecar_event.get("actor_spiffe_id"),
|
|
465
465
|
"originator_human_id_hash": None,
|
|
466
|
+
# The sidecar records the sign-in issuer, never the person's identity;
|
|
467
|
+
# the originator node shows it when no identity hash is available.
|
|
468
|
+
"originator_issuer": sidecar_event.get("originator_issuer"),
|
|
466
469
|
# T0 has no parent and no parent_token_ids_list (not a composite).
|
|
467
470
|
}
|
|
468
471
|
t0_event = UnifiedEvent(
|
|
@@ -624,6 +627,7 @@ def _translate_composite_mint(
|
|
|
624
627
|
"parent_root_token_ids": sidecar_event.get("parent_root_token_ids") or [],
|
|
625
628
|
"parent_tenant_ids": sidecar_event.get("parent_tenant_ids") or [],
|
|
626
629
|
"originator_human_id_hash": sidecar_event.get("originator_human_id_hash"),
|
|
630
|
+
"originator_issuer": sidecar_event.get("originator_issuer"),
|
|
627
631
|
"task_ref": sidecar_event.get("task_ref"),
|
|
628
632
|
}
|
|
629
633
|
t_comp_0_event = UnifiedEvent(
|
|
@@ -100,6 +100,7 @@ def _sidecar_types(row: dict, label: str) -> None:
|
|
|
100
100
|
"agent_decision_action",
|
|
101
101
|
"destination",
|
|
102
102
|
"terminal_attestation_verification",
|
|
103
|
+
"originator_issuer",
|
|
103
104
|
):
|
|
104
105
|
if row.get(key) is not None and not isinstance(row[key], str):
|
|
105
106
|
raise InputError(f"{label}: {key} must be a string or null")
|
|
@@ -232,6 +233,7 @@ def read_trace(body, source: str) -> tuple[str, list[UnifiedEvent]]:
|
|
|
232
233
|
"terminal_attestation_verification",
|
|
233
234
|
"destination",
|
|
234
235
|
"agent_decision_action",
|
|
236
|
+
"originator_issuer",
|
|
235
237
|
):
|
|
236
238
|
if raw.get(key) is not None and not isinstance(raw[key], str):
|
|
237
239
|
raise InputError(f"{label}: {key} must be a string or null")
|
|
@@ -215,6 +215,11 @@ class TerminalNode:
|
|
|
215
215
|
action_payload: dict = field(default_factory=dict)
|
|
216
216
|
action_taken_by_spiffe_id: str | None = None
|
|
217
217
|
action_taken_timestamp: str | None = None
|
|
218
|
+
# True only when an action record's actor is the workload the audience
|
|
219
|
+
# names (exact match, or a match of the audience pattern). Any other
|
|
220
|
+
# token-correlated action stays an observation and never proves that the
|
|
221
|
+
# addressed recipient acted.
|
|
222
|
+
recipient_acted: bool = False
|
|
218
223
|
# Canonical associations observed for this leaf and exact receiver workload.
|
|
219
224
|
# A trust-domain match, another token, or a wildcard is not such evidence.
|
|
220
225
|
observed_tenant_ids: tuple[str, ...] = ()
|
|
@@ -274,6 +279,21 @@ class TokenRelationGraph:
|
|
|
274
279
|
# become unknown. Receiving/reporting observations cannot supply this map.
|
|
275
280
|
creator_tenant_claims: dict[str, tuple[str, ...]] = field(default_factory=dict)
|
|
276
281
|
|
|
282
|
+
@property
|
|
283
|
+
def evidence_complete(self) -> bool:
|
|
284
|
+
"""True when the supplied records leave no structural gap.
|
|
285
|
+
|
|
286
|
+
Every referenced token was observed (no placeholder stands in for a
|
|
287
|
+
token only named by another), and each recipient's own records show
|
|
288
|
+
its action. A complete set is still a set of supplied observations,
|
|
289
|
+
not independently verified business truth.
|
|
290
|
+
"""
|
|
291
|
+
if not self.tokens or not self.terminals:
|
|
292
|
+
return False
|
|
293
|
+
if any(token.placeholder for token in self.tokens.values()):
|
|
294
|
+
return False
|
|
295
|
+
return all(terminal.recipient_acted for terminal in self.terminals)
|
|
296
|
+
|
|
277
297
|
|
|
278
298
|
# ---------------------------------------------------------------------------
|
|
279
299
|
# Extraction — events → TokenRelationGraph
|
|
@@ -729,14 +749,49 @@ def _extract_originators(
|
|
|
729
749
|
if e.source.startswith(TENANT_SOURCE_PREFIX) and e.event_type == "human_authenticated"
|
|
730
750
|
}
|
|
731
751
|
|
|
732
|
-
|
|
752
|
+
# Every observation of the same root (local sidecar record, central
|
|
753
|
+
# metadata, a second file) describes ONE originator. Merge them per root so
|
|
754
|
+
# the canvas label and the details are built from the same facts: a SIEM
|
|
755
|
+
# match on any observed hash wins, otherwise the first issuer any
|
|
756
|
+
# observation carries.
|
|
757
|
+
by_root: dict[str, list[UnifiedEvent]] = {}
|
|
733
758
|
for mint in mint_root_events:
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
759
|
+
by_root.setdefault(mint.root_token_id, []).append(mint)
|
|
760
|
+
|
|
761
|
+
out: list[OriginatorNode] = []
|
|
762
|
+
for root_tid, observations in by_root.items():
|
|
763
|
+
h_hash = next(
|
|
764
|
+
(
|
|
765
|
+
str(o.raw_event.get("originator_human_id_hash"))
|
|
766
|
+
for o in observations
|
|
767
|
+
if o.raw_event.get("originator_human_id_hash")
|
|
768
|
+
),
|
|
769
|
+
"",
|
|
770
|
+
)
|
|
771
|
+
issuer = next(
|
|
772
|
+
(
|
|
773
|
+
o.raw_event.get("originator_issuer")
|
|
774
|
+
for o in observations
|
|
775
|
+
if isinstance(o.raw_event.get("originator_issuer"), str)
|
|
776
|
+
and o.raw_event.get("originator_issuer")
|
|
777
|
+
),
|
|
778
|
+
None,
|
|
779
|
+
)
|
|
780
|
+
token = next((tokens[o.token_id] for o in observations if o.token_id in tokens), None)
|
|
781
|
+
tenant_id = (
|
|
782
|
+
token.tenant_id
|
|
783
|
+
if token
|
|
784
|
+
else next(
|
|
785
|
+
(
|
|
786
|
+
str(o.raw_event.get("tenant_id"))
|
|
787
|
+
for o in observations
|
|
788
|
+
if o.raw_event.get("tenant_id")
|
|
789
|
+
),
|
|
790
|
+
"",
|
|
791
|
+
)
|
|
792
|
+
)
|
|
738
793
|
# Optional enrichment from tenant SIEM.
|
|
739
|
-
siem = siem_human_events.get(h_hash)
|
|
794
|
+
siem = siem_human_events.get(h_hash) if h_hash else None
|
|
740
795
|
if siem:
|
|
741
796
|
raw = siem.raw_event
|
|
742
797
|
out.append(
|
|
@@ -757,6 +812,9 @@ def _extract_originators(
|
|
|
757
812
|
human_id_hash=h_hash,
|
|
758
813
|
root_token_id=root_tid,
|
|
759
814
|
tenant_id=tenant_id,
|
|
815
|
+
# The sidecar's mint record names the sign-in issuer even
|
|
816
|
+
# when no identity hash reaches these records.
|
|
817
|
+
human_iss=issuer,
|
|
760
818
|
)
|
|
761
819
|
)
|
|
762
820
|
return out
|
|
@@ -812,6 +870,35 @@ def _extract_auth_summary(
|
|
|
812
870
|
# ---------------------------------------------------------------------------
|
|
813
871
|
|
|
814
872
|
|
|
873
|
+
# The AAC audience grammar (AAC Design, "SPIFFE-ID pattern matching for
|
|
874
|
+
# audience"), as the verifier implements it: a concrete SPIFFE ID, or a trailing
|
|
875
|
+
# `/*` that stands for exactly one non-empty path segment. Trust domains match
|
|
876
|
+
# exactly; a SPIFFE ID itself never contains a wildcard.
|
|
877
|
+
_AUDIENCE_PATTERN = re.compile(r"^spiffe://[A-Za-z0-9.\-_]+(?:/[^/*]+)*(?:/\*)?$")
|
|
878
|
+
_CONCRETE_SPIFFE_ID = re.compile(r"^spiffe://[A-Za-z0-9.\-_]+(?:/[^/*]+)+$")
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _actor_matches_audience(actor: object, audience: str) -> bool:
|
|
882
|
+
"""Whether an action's actor is the workload the audience constraint names.
|
|
883
|
+
|
|
884
|
+
Only a concrete SPIFFE ID can be the addressed recipient, and only under
|
|
885
|
+
the audience grammar above. Anything else (an opaque identity, a pattern
|
|
886
|
+
used as an actor, an unsupported audience shape) is kept as an observation
|
|
887
|
+
without claiming a match. A shared token or tenant never counts.
|
|
888
|
+
"""
|
|
889
|
+
if not isinstance(actor, str) or not _CONCRETE_SPIFFE_ID.fullmatch(actor):
|
|
890
|
+
return False
|
|
891
|
+
if any(character.isspace() for character in actor) or not _AUDIENCE_PATTERN.fullmatch(audience):
|
|
892
|
+
return False
|
|
893
|
+
if audience.endswith("/*"):
|
|
894
|
+
prefix = audience[:-2]
|
|
895
|
+
if not actor.startswith(prefix + "/"):
|
|
896
|
+
return False
|
|
897
|
+
tail = actor[len(prefix) + 1 :]
|
|
898
|
+
return bool(tail) and "/" not in tail
|
|
899
|
+
return actor == audience
|
|
900
|
+
|
|
901
|
+
|
|
815
902
|
def _extract_terminals(
|
|
816
903
|
events: list[UnifiedEvent],
|
|
817
904
|
tokens: dict[str, TokenNode],
|
|
@@ -882,6 +969,9 @@ def _extract_terminals(
|
|
|
882
969
|
action_by_token.get(tid, []),
|
|
883
970
|
key=lambda e: (e.timestamp_iso, json.dumps(e.raw_event, sort_keys=True)),
|
|
884
971
|
)
|
|
972
|
+
recipient_acted = any(
|
|
973
|
+
_actor_matches_audience(e.raw_event.get("actor_spiffe_id"), audience) for e in actions
|
|
974
|
+
)
|
|
885
975
|
if actions:
|
|
886
976
|
action = actions[0]
|
|
887
977
|
raw: dict = (
|
|
@@ -911,6 +1001,7 @@ def _extract_terminals(
|
|
|
911
1001
|
# alias is needed.
|
|
912
1002
|
action_taken_by_spiffe_id=raw.get("actor_spiffe_id"),
|
|
913
1003
|
action_taken_timestamp=action.timestamp_iso,
|
|
1004
|
+
recipient_acted=recipient_acted,
|
|
914
1005
|
)
|
|
915
1006
|
)
|
|
916
1007
|
else:
|
|
@@ -50,10 +50,16 @@ class SourceSummary:
|
|
|
50
50
|
class Provenance:
|
|
51
51
|
sources: tuple[SourceSummary, ...]
|
|
52
52
|
central_status: str
|
|
53
|
+
# Structural completeness of the supplied records (TokenRelationGraph.evidence_complete).
|
|
54
|
+
evidence_complete: bool = False
|
|
53
55
|
|
|
54
56
|
|
|
55
57
|
def summarize_sources(
|
|
56
|
-
sources: list[SourceInput],
|
|
58
|
+
sources: list[SourceInput],
|
|
59
|
+
selected: list[UnifiedEvent],
|
|
60
|
+
*,
|
|
61
|
+
central_status: str,
|
|
62
|
+
evidence_complete: bool = False,
|
|
57
63
|
) -> Provenance:
|
|
58
64
|
# Selection retains the original event objects. Track membership by identity,
|
|
59
65
|
# not by a filename, timestamp or caller-controlled event field that could
|
|
@@ -81,7 +87,7 @@ def summarize_sources(
|
|
|
81
87
|
times[-1] if times else None,
|
|
82
88
|
)
|
|
83
89
|
)
|
|
84
|
-
return Provenance(tuple(summaries), central_status)
|
|
90
|
+
return Provenance(tuple(summaries), central_status, evidence_complete)
|
|
85
91
|
|
|
86
92
|
|
|
87
93
|
def source_panel(provenance: Provenance | None, diagnostics: list[str]) -> str:
|
|
@@ -143,11 +149,21 @@ def source_panel(provenance: Provenance | None, diagnostics: list[str]) -> str:
|
|
|
143
149
|
"observations for hosted data, not unique business actions. Timestamp spans describe "
|
|
144
150
|
"available records, not execution start/end or continuous coverage.</p>"
|
|
145
151
|
)
|
|
152
|
+
if provenance and provenance.evidence_complete:
|
|
153
|
+
coverage = (
|
|
154
|
+
"<li>Every referenced token was observed and each recipient's own records show its "
|
|
155
|
+
"action. Records are supplied, correlated observations, not independently verified "
|
|
156
|
+
"business truth.</li>"
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
coverage = (
|
|
160
|
+
"<li>Partial evidence: only supplied, correlated observations are shown. Missing "
|
|
161
|
+
"records do not establish success or failure.</li>"
|
|
162
|
+
)
|
|
146
163
|
parts.append(
|
|
147
|
-
"<h2>Limitations for these inputs</h2><ul
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"does not prove delivery or completion. Mint/transport success does not prove a business result.</li>"
|
|
164
|
+
"<h2>Limitations for these inputs</h2><ul>" + coverage + "<li>A recipient is named by an "
|
|
165
|
+
'audience constraint ("addressed to"); that constraint alone does not prove delivery or '
|
|
166
|
+
"completion. Mint/transport success does not prove a business result.</li>"
|
|
151
167
|
)
|
|
152
168
|
if "Control-plane trace data" in used:
|
|
153
169
|
parts.append(
|
|
@@ -35,6 +35,7 @@ from datetime import ( # c-S6 fix: was per-call import inside _format_unix_time
|
|
|
35
35
|
from html import escape
|
|
36
36
|
from importlib.resources import files
|
|
37
37
|
from pathlib import Path
|
|
38
|
+
from urllib.parse import urlparse
|
|
38
39
|
|
|
39
40
|
from .model import (
|
|
40
41
|
ChainFailure,
|
|
@@ -331,20 +332,48 @@ def _build_originator_node(orig: OriginatorNode, theme: VisualTheme) -> GraphNod
|
|
|
331
332
|
label = f"Human originator\n\n{label_name}"
|
|
332
333
|
brief = f"Human originator ({label_name})"
|
|
333
334
|
detail_title = f"Human originator: {label_name}"
|
|
335
|
+
elif orig.human_iss:
|
|
336
|
+
# The sidecar records where the person signed in; the identity itself
|
|
337
|
+
# travels only as a sealed hash in the root token.
|
|
338
|
+
host = _issuer_host(orig.human_iss)
|
|
339
|
+
label = f"Originator\n\nsigned in at {host}"
|
|
340
|
+
brief = f"Originator signed in at {host} (identity hash not in these records)"
|
|
341
|
+
detail_title = f"Originator: signed in at {host}"
|
|
334
342
|
else:
|
|
335
343
|
label = "Originator\n\n(identity not recorded)"
|
|
336
344
|
brief = "Originator identity not available in these records"
|
|
337
345
|
detail_title = "Originator (identity not recorded)"
|
|
338
|
-
sections: list[dict] = [
|
|
346
|
+
sections: list[dict] = []
|
|
347
|
+
sign_in_shown = not has_human_id and bool(orig.human_iss)
|
|
348
|
+
if sign_in_shown:
|
|
349
|
+
sections.append(
|
|
350
|
+
{
|
|
351
|
+
"title": "Sign-in (from the sidecar's mint record)",
|
|
352
|
+
"rows": [
|
|
353
|
+
["issuer", orig.human_iss],
|
|
354
|
+
[
|
|
355
|
+
"identity",
|
|
356
|
+
"AAC seals a hash of the person's sign-in (issuer, subject and sign-in "
|
|
357
|
+
"time) into the root token and never stores the name.",
|
|
358
|
+
],
|
|
359
|
+
],
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
sections.append(
|
|
339
363
|
{
|
|
340
364
|
"title": "Human identity (from log data)",
|
|
341
365
|
"rows": [
|
|
342
366
|
["human_id_hash", orig.human_id_hash or "(not recorded)"],
|
|
343
|
-
[
|
|
367
|
+
[
|
|
368
|
+
"OIDC issuer",
|
|
369
|
+
"(not logged at originating tenant SIEM)"
|
|
370
|
+
if sign_in_shown or not orig.human_iss
|
|
371
|
+
else orig.human_iss,
|
|
372
|
+
],
|
|
344
373
|
["OIDC subject", orig.human_sub or "(not logged at originating tenant SIEM)"],
|
|
345
374
|
],
|
|
346
375
|
},
|
|
347
|
-
|
|
376
|
+
)
|
|
348
377
|
if orig.role:
|
|
349
378
|
sections.append({"title": "Role", "rows": [["role", orig.role]]})
|
|
350
379
|
if orig.authorization_summary:
|
|
@@ -377,6 +406,11 @@ def _build_originator_node(orig: OriginatorNode, theme: VisualTheme) -> GraphNod
|
|
|
377
406
|
)
|
|
378
407
|
|
|
379
408
|
|
|
409
|
+
def _issuer_host(issuer: str) -> str:
|
|
410
|
+
"""The host of a sign-in issuer URL, or the issuer itself when it has none."""
|
|
411
|
+
return urlparse(issuer).hostname or issuer
|
|
412
|
+
|
|
413
|
+
|
|
380
414
|
def _build_token_node(token: TokenNode, theme: VisualTheme) -> GraphNode:
|
|
381
415
|
participant = theme.creator_identity(token)
|
|
382
416
|
color = color_for(participant, theme)
|
|
@@ -593,13 +627,19 @@ def _build_terminal_node(
|
|
|
593
627
|
participant = theme.recipient_identity(terminal)
|
|
594
628
|
color = color_for(participant, theme)
|
|
595
629
|
tenant_label_text = color["label"]
|
|
630
|
+
# The audience constraint names who the authority was addressed to; only an
|
|
631
|
+
# action by that addressed workload is evidence that it acted.
|
|
632
|
+
acted = terminal.recipient_acted
|
|
596
633
|
label_lines = [
|
|
634
|
+
"Addressed to",
|
|
635
|
+
"",
|
|
597
636
|
str(color.get("node_label") or tenant_label_text),
|
|
598
637
|
terminal.workload_label,
|
|
599
|
-
"Intended recipient",
|
|
600
638
|
]
|
|
601
639
|
if "*" in terminal.audience_pattern:
|
|
602
640
|
label_lines.append("Audience pattern")
|
|
641
|
+
if acted:
|
|
642
|
+
label_lines.append("received and acted")
|
|
603
643
|
# Generic action summary line — works for any wedge, not just financial.
|
|
604
644
|
# If leaf token's action is known, display it; otherwise omit.
|
|
605
645
|
if leaf_token:
|
|
@@ -656,8 +696,11 @@ def _build_terminal_node(
|
|
|
656
696
|
return GraphNode(
|
|
657
697
|
node_id=f"terminal:{terminal.leaf_token_id}",
|
|
658
698
|
label=label,
|
|
659
|
-
brief=
|
|
660
|
-
|
|
699
|
+
brief=(
|
|
700
|
+
f"Addressed to: {tenant_label_text} {terminal.workload_label} (audience constraint)"
|
|
701
|
+
+ (", received and acted" if acted else "")
|
|
702
|
+
),
|
|
703
|
+
detail_title=f"Addressed to: {tenant_label_text} {terminal.workload_label}",
|
|
661
704
|
detail_sections=sections,
|
|
662
705
|
fill_color=color["fill"],
|
|
663
706
|
border_color=color["border"],
|
|
@@ -886,7 +929,7 @@ def _build_terminal_edge(
|
|
|
886
929
|
edge_counter: int,
|
|
887
930
|
) -> GraphEdge:
|
|
888
931
|
# An audience is a constraint, not evidence that a hand-off happened.
|
|
889
|
-
op_label = "
|
|
932
|
+
op_label = "addressed to"
|
|
890
933
|
rows = [
|
|
891
934
|
["audience-bound to", terminal.audience_pattern],
|
|
892
935
|
[
|
|
@@ -1212,7 +1255,7 @@ def _wrap_with_demo_chrome(
|
|
|
1212
1255
|
<p class="aac-root"><strong>Chain root token ID:</strong> <code>{escape(root_token_id)}</code></p>
|
|
1213
1256
|
<div class="aac-legend" tabindex="0" aria-label="Participant legend">{"".join(legend_pieces)}</div>
|
|
1214
1257
|
<div class="aac-roles" aria-label="Node role legend">
|
|
1215
|
-
<span>⬭ Originator</span><span>▭ Token</span><span>⬡
|
|
1258
|
+
<span>⬭ Originator</span><span>▭ Token</span><span>⬡ Addressed to (audience constraint)</span>
|
|
1216
1259
|
<span>◇ Composite root</span>
|
|
1217
1260
|
</div>
|
|
1218
1261
|
</header>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aac-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.12
|
|
4
4
|
Summary: The aac platform CLI — headless operator interface to the AAC control plane (tenant registration, chain audit).
|
|
5
5
|
Author: Agent Authority Cloud Project
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -28,9 +28,9 @@ Dynamic: license-file
|
|
|
28
28
|
# aac-cli — the `aac` command
|
|
29
29
|
|
|
30
30
|
`aac` is the command-line tool for tenants of Agent Authority Cloud (AAC).
|
|
31
|
-
AAC lets an
|
|
32
|
-
which tenant they belong to and what they were
|
|
33
|
-
**tenant** is one
|
|
31
|
+
AAC lets an organization's AI agents and services prove, on every request,
|
|
32
|
+
which tenant they belong to and what they were authorized to do. A
|
|
33
|
+
**tenant** is one organization registered with AAC. Each of its workloads
|
|
34
34
|
runs beside an AAC sidecar that signs outgoing requests and verifies
|
|
35
35
|
incoming ones against public trust material the tenant publishes.
|
|
36
36
|
|
|
@@ -69,7 +69,8 @@ pip install aac-cli
|
|
|
69
69
|
aac --version
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
Supported platforms are macOS, Linux and Linux Docker containers. Python 3.11
|
|
73
|
+
to 3.14 is supported. Release checks test all four versions.
|
|
73
74
|
|
|
74
75
|
> **Mind the name.** `pip install aac` installs an unrelated project that
|
|
75
76
|
> happens to share the acronym. The AAC package is **`aac-cli`**; the
|
|
@@ -756,7 +757,7 @@ stored value.
|
|
|
756
757
|
|
|
757
758
|
`tenant_id` is managed for you. Profile commands display it but never ask
|
|
758
759
|
for it; only `aac tenant register`, `aac init` and `aac sso login` write it.
|
|
759
|
-
It is your
|
|
760
|
+
It is your organization's identifier in the federation, allocated by AAC at
|
|
760
761
|
registration in the form `tnt-<uuid>`, and it never changes.
|
|
761
762
|
A binding makes that tenant the profile's stored default; it does not grant
|
|
762
763
|
membership or configure an identity provider. Successful sign-in to an existing
|
|
@@ -953,12 +954,12 @@ code.
|
|
|
953
954
|
Enterprise tenants are registered through an onboarding ceremony: AAC
|
|
954
955
|
operations opens a registration window and hands the tenant operator a
|
|
955
956
|
bootstrap token, which the commands below read from `--bootstrap-token` or
|
|
956
|
-
the `AAC_BOOTSTRAP_TOKEN` environment variable. The token
|
|
957
|
+
the `AAC_BOOTSTRAP_TOKEN` environment variable. The token authorizes only
|
|
957
958
|
the onboarding steps shown with it; every other administration command runs
|
|
958
959
|
under a signed-in session.
|
|
959
960
|
|
|
960
961
|
For enterprise sign-in, AAC supports Microsoft Entra ID (also known as
|
|
961
|
-
Azure Active Directory or Azure AD). If your
|
|
962
|
+
Azure Active Directory or Azure AD). If your organization needs another
|
|
962
963
|
identity provider, contact support@cascadeauth.com.
|
|
963
964
|
|
|
964
965
|
### Register the tenant
|
|
@@ -32,7 +32,7 @@ build-backend = "setuptools.build_meta"
|
|
|
32
32
|
|
|
33
33
|
[project]
|
|
34
34
|
name = "aac-cli"
|
|
35
|
-
version = "0.2.
|
|
35
|
+
version = "0.2.12"
|
|
36
36
|
description = "The aac platform CLI — headless operator interface to the AAC control plane (tenant registration, chain audit)."
|
|
37
37
|
readme = "README.md"
|
|
38
38
|
requires-python = ">=3.11"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|