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.
Files changed (58) hide show
  1. {aac_cli-0.2.11/aac_cli.egg-info → aac_cli-0.2.12}/PKG-INFO +9 -8
  2. {aac_cli-0.2.11 → aac_cli-0.2.12}/README.md +8 -7
  3. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/__init__.py +1 -1
  4. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/cli.py +5 -2
  5. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/data/renderer-source.json +7 -7
  6. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/from_sidecar.py +4 -0
  7. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/inputs.py +2 -0
  8. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/model.py +97 -6
  9. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/provenance.py +22 -6
  10. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/visualizer.py +51 -8
  11. {aac_cli-0.2.11 → aac_cli-0.2.12/aac_cli.egg-info}/PKG-INFO +9 -8
  12. {aac_cli-0.2.11 → aac_cli-0.2.12}/pyproject.toml +1 -1
  13. {aac_cli-0.2.11 → aac_cli-0.2.12}/LICENSE +0 -0
  14. {aac_cli-0.2.11 → aac_cli-0.2.12}/MANIFEST.in +0 -0
  15. {aac_cli-0.2.11 → aac_cli-0.2.12}/NOTICE +0 -0
  16. {aac_cli-0.2.11 → aac_cli-0.2.12}/THIRD_PARTY_NOTICES.md +0 -0
  17. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/__main__.py +0 -0
  18. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/__init__.py +0 -0
  19. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/data/action-taken-v1.json +0 -0
  20. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/data/predicates-v1.json +0 -0
  21. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/narrative.py +0 -0
  22. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/schemas.py +0 -0
  23. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/temporal.py +0 -0
  24. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/theme.py +0 -0
  25. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/_aeg/timeline_source.py +0 -0
  26. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/admin_key_pem.py +0 -0
  27. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/aeg_cli.py +0 -0
  28. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/aeg_list.py +0 -0
  29. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/agent_cli.py +0 -0
  30. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/agent_config.py +0 -0
  31. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/agent_health.py +0 -0
  32. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/agent_layout.py +0 -0
  33. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/agent_record.py +0 -0
  34. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/api_key_rotation_state.py +0 -0
  35. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/chain_list.py +0 -0
  36. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/cli.py +0 -0
  37. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/config.py +0 -0
  38. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/config_render.py +0 -0
  39. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/dev_material.py +0 -0
  40. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/idp_recovery.py +0 -0
  41. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/init_cli.py +0 -0
  42. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/interrupts.py +0 -0
  43. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/material_cases.py +0 -0
  44. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/profiles.py +0 -0
  45. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/reference.py +0 -0
  46. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/registration_state.py +0 -0
  47. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/safe_output.py +0 -0
  48. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/secure_files.py +0 -0
  49. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/sso_login.py +0 -0
  50. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/supplied_material.py +0 -0
  51. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli/trace_query.py +0 -0
  52. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli.egg-info/SOURCES.txt +0 -0
  53. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli.egg-info/dependency_links.txt +0 -0
  54. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli.egg-info/requires.txt +0 -0
  55. {aac_cli-0.2.11 → aac_cli-0.2.12}/aac_cli.egg-info/top_level.txt +0 -0
  56. {aac_cli-0.2.11 → aac_cli-0.2.12}/launchers/aac +0 -0
  57. {aac_cli-0.2.11 → aac_cli-0.2.12}/launchers/aeg +0 -0
  58. {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.11
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 organisation's AI agents and services prove, on every request,
32
- which tenant they belong to and what they were authorised to do. A
33
- **tenant** is one organisation registered with AAC. Each of its workloads
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
- Python 3.11 to 3.14 is supported. Release checks test all four versions.
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 organisation's identifier in the federation, allocated by AAC at
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 authorises only
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 organisation needs another
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 organisation's AI agents and services prove, on every request,
5
- which tenant they belong to and what they were authorised to do. A
6
- **tenant** is one organisation registered with AAC. Each of its workloads
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
- Python 3.11 to 3.14 is supported. Release checks test all four versions.
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 organisation's identifier in the federation, allocated by AAC at
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 authorises only
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 organisation needs another
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
@@ -10,4 +10,4 @@ entrypoint (`aac_cli.cli:entrypoint`).
10
10
  # test in bin/tests/test_cli_version_parity.py. A literal keeps the
11
11
  # shipped code free of import-time metadata lookups (and their
12
12
  # not-installed failure mode).
13
- __version__ = "0.2.11"
13
+ __version__ = "0.2.12"
@@ -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(sources, selected, central_status=central_status)
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; partial evidence)",
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": "6e8bcd00bd94c415e04af65deb527be4d1581a6a5396d74c95708d6dda97fca4",
2
+ "content_sha256": "f45099fcfa1d1ceca5f31244dfa526ea096094ced6cfc7faf27cc1babe482163",
3
3
  "files": {
4
4
  "__init__.py": "80d80a1ba35353d271cddf02532801c60b57f5ec06fb268ae823747742ce8979",
5
- "cli.py": "98aa2b0c528fefb5f1f90a16584a6fbda1f075ebfcf61605f52948cdb940a1af",
5
+ "cli.py": "a180a171b4d1f819fd96d0821394eee19b45fafddddbd50c74778af6ac3d5fb0",
6
6
  "data/action-taken-v1.json": "d863343afe7f314646a73cf063480a7e650f93857d2ed5ec3a7b1f9838671fdf",
7
7
  "data/predicates-v1.json": "35e83a29feada10e8cfbef7009245146a9717d4f4fd58d74124e0ec91ae922c8",
8
- "from_sidecar.py": "6ac023f6925a13e94e0fee762656a02ca98981f2add194b052e9014ca48e2219",
9
- "inputs.py": "ebf1ca77957d29d59683348f6916bac3c21e2037f087f26ff7c7242d75edc77c",
10
- "model.py": "b631dcd7af587dcc89c18e1058bc81fb1043f04250a0d6ea585f9c595fd75cfc",
8
+ "from_sidecar.py": "8671c3e0efe9bfe9ecf9be057b70e245ca8d0d645b6bad8819f6fb1b882e255e",
9
+ "inputs.py": "047a6b7dd788e04c49e10f090de9336ed3870e754b547c469371369d27b55b8b",
10
+ "model.py": "715d0e92c3aafd9e0a194a74b4df7354bd24d1bf815767d590cf245da3d60018",
11
11
  "narrative.py": "8ac4b12add84cc8be907cff5f25e58731ab1144888aec8d376e286de461a6179",
12
- "provenance.py": "d310e13bfd45cb16edcc72e7b2bc28c916e3ff45208e2b44240e589f0ec0558d",
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": "5eb7b863652c51a4a5751bcd8ae735b688e6e3e61089f0c158473ebb52ae6655"
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
- out: list[OriginatorNode] = []
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
- h_hash = mint.raw_event.get("originator_human_id_hash") or ""
735
- root_tid = mint.root_token_id
736
- token = tokens.get(mint.token_id)
737
- tenant_id = token.tenant_id if token else (mint.raw_event.get("tenant_id") or "")
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], selected: list[UnifiedEvent], *, central_status: str
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><li>Partial evidence: only supplied, "
148
- "correlated observations are shown. Missing records do not establish success or failure.</li>"
149
- "<li>An intended recipient is named by an audience constraint; that constraint alone "
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
- ["OIDC issuer", orig.human_iss or "(not logged at originating tenant SIEM)"],
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=f"Intended recipient (audience constraint): {tenant_label_text}",
660
- detail_title=f"Intended recipient: {tenant_label_text} {terminal.workload_label}",
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 = "intended for"
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>⬡ Intended recipient (audience constraint)</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.11
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 organisation's AI agents and services prove, on every request,
32
- which tenant they belong to and what they were authorised to do. A
33
- **tenant** is one organisation registered with AAC. Each of its workloads
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
- Python 3.11 to 3.14 is supported. Release checks test all four versions.
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 organisation's identifier in the federation, allocated by AAC at
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 authorises only
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 organisation needs another
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.11"
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