aac-cli 0.2.10__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.10/aac_cli.egg-info → aac_cli-0.2.12}/PKG-INFO +44 -16
  2. {aac_cli-0.2.10 → aac_cli-0.2.12}/README.md +43 -15
  3. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/__init__.py +1 -1
  4. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/cli.py +5 -2
  5. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/data/renderer-source.json +7 -7
  6. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/from_sidecar.py +4 -0
  7. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/inputs.py +2 -0
  8. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/model.py +97 -6
  9. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/provenance.py +22 -6
  10. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/visualizer.py +51 -8
  11. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/admin_key_pem.py +3 -0
  12. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/api_key_rotation_state.py +4 -1
  13. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/cli.py +320 -240
  14. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/config.py +44 -14
  15. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/idp_recovery.py +6 -3
  16. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/registration_state.py +30 -18
  17. aac_cli-0.2.12/aac_cli/safe_output.py +311 -0
  18. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/sso_login.py +363 -58
  19. {aac_cli-0.2.10 → aac_cli-0.2.12/aac_cli.egg-info}/PKG-INFO +44 -16
  20. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli.egg-info/SOURCES.txt +1 -0
  21. {aac_cli-0.2.10 → aac_cli-0.2.12}/pyproject.toml +1 -1
  22. {aac_cli-0.2.10 → aac_cli-0.2.12}/LICENSE +0 -0
  23. {aac_cli-0.2.10 → aac_cli-0.2.12}/MANIFEST.in +0 -0
  24. {aac_cli-0.2.10 → aac_cli-0.2.12}/NOTICE +0 -0
  25. {aac_cli-0.2.10 → aac_cli-0.2.12}/THIRD_PARTY_NOTICES.md +0 -0
  26. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/__main__.py +0 -0
  27. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/__init__.py +0 -0
  28. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/data/action-taken-v1.json +0 -0
  29. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/data/predicates-v1.json +0 -0
  30. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/narrative.py +0 -0
  31. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/schemas.py +0 -0
  32. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/temporal.py +0 -0
  33. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/theme.py +0 -0
  34. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/timeline_source.py +0 -0
  35. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/aeg_cli.py +0 -0
  36. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/aeg_list.py +0 -0
  37. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/agent_cli.py +0 -0
  38. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/agent_config.py +0 -0
  39. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/agent_health.py +0 -0
  40. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/agent_layout.py +0 -0
  41. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/agent_record.py +0 -0
  42. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/chain_list.py +0 -0
  43. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/config_render.py +0 -0
  44. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/dev_material.py +0 -0
  45. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/init_cli.py +0 -0
  46. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/interrupts.py +0 -0
  47. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/material_cases.py +0 -0
  48. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/profiles.py +0 -0
  49. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/reference.py +0 -0
  50. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/secure_files.py +0 -0
  51. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/supplied_material.py +0 -0
  52. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/trace_query.py +0 -0
  53. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli.egg-info/dependency_links.txt +0 -0
  54. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli.egg-info/requires.txt +0 -0
  55. {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli.egg-info/top_level.txt +0 -0
  56. {aac_cli-0.2.10 → aac_cli-0.2.12}/launchers/aac +0 -0
  57. {aac_cli-0.2.10 → aac_cli-0.2.12}/launchers/aeg +0 -0
  58. {aac_cli-0.2.10 → 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.10
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
 
@@ -46,8 +46,8 @@ The `aac` command is how a tenant's operator does everything that is not
46
46
  done by the sidecar itself: register the tenant, sign in, manage keys,
47
47
  workloads and domains, and prepare a runnable local workload. Everything
48
48
  works from a terminal or a script; there is no web console to click
49
- through. Output is JSON by default, and every command that reports a
50
- result also offers `--output table`.
49
+ through. Output is JSON by default. Commands that support table output offer
50
+ `--output table`; `aac sso describe` returns JSON only.
51
51
 
52
52
  Two kinds of tenant use the same tool:
53
53
 
@@ -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
@@ -857,8 +858,16 @@ aac sso logout --profile stage
857
858
  A developer tenant signs in with `--idp github` or `--idp google`; if you
858
859
  need another sign-in provider, contact support@cascadeauth.com. An
859
860
  enterprise tenant signs in through its Microsoft Entra ID connection, named
860
- by the connection's exact URL with `--idp-url`; when several connections
861
- are registered, the error message lists the exact commands to choose one.
861
+ by the connection's exact URL with `--idp-url`. With neither flag, the
862
+ tenant's own connections take precedence over the shared developer sign-ins;
863
+ when several remain, a terminal shows a numbered list to choose from and then
864
+ prints the flag that skips the prompt next time, while a script (no terminal)
865
+ gets exit 3 and one copy-ready command per connection. The list marks which
866
+ connection the cached session was last obtained through; it never shows
867
+ which connections have members. A developer tenant admits only the provider
868
+ and account it was registered with: when the AAC STS does not accept a shared
869
+ sign-in, the CLI reminds you of that rule and lists any other shared sign-ins. GitHub connections accept the device flow
870
+ only: `--flow pkce` with a GitHub connection stops locally with exit 3.
862
871
 
863
872
  Sessions last **4 hours by default**. Your administrator may configure a
864
873
  different lifetime for the deployment or connection. You can run
@@ -868,12 +877,20 @@ with the four-hour default, signing in at 09:00 gives an expiry of 13:00;
868
877
  signing in successfully again at 12:00 gives the new session an expiry of 16:00.
869
878
  Use `aac sso whoami --profile NAME --output table` to check the current
870
879
  expiration. If the session expires, sign in again; no refresh token is issued.
880
+ A command that needs a session stops locally with exit 3 when none is cached,
881
+ before anything is sent, and prints the `aac sso login` command for the same
882
+ profile and endpoints; commands that also accept a bootstrap token name that
883
+ alternative.
871
884
 
872
885
  Sign-in uses the device flow by default, so the browser can be on another
873
- machine and headless hosts work. Google connections use a loopback flow
874
- instead, because Google's device flow is scope-restricted; `--flow`
875
- overrides the choice. `whoami` and `logout` work offline and never contact
876
- AAC.
886
+ machine and headless hosts work. On a terminal with a browser at hand
887
+ (macOS, a desktop session, or `BROWSER` set), the device flow also offers to
888
+ open the sign-in page for you: press Enter to open it in your browser, or copy
889
+ the printed link and code into a browser yourself; `--no-browser` turns the
890
+ offer off, and a script (no terminal) or a headless host never sees it. Google
891
+ connections use a loopback flow instead, because Google's device flow is
892
+ scope-restricted; `--flow` overrides the choice. `whoami` and `logout` work
893
+ offline and never contact AAC.
877
894
 
878
895
  ## Output and exit codes
879
896
 
@@ -921,17 +938,28 @@ a callback gives up after 300 seconds with exit `3`. HTTP timeouts remain exit
921
938
  If AAC asks you to slow down, the command shows the interval to wait. The
922
939
  CLI never replays a change on its own; wait and rerun deliberately.
923
940
 
941
+ The commands that can present a ceremony token (`aac tenant register`,
942
+ `reissue-api-key`, `assign-hosted-domain`, `reactivate-hosted-domain`,
943
+ `bind-trust-domain`, `list-trust-domains`, `revoke-trust-domain`,
944
+ `rotate-admin-key`, `aac sso register-idp`, `enroll-idp-recovery-key` and
945
+ `approve-idp-repair`) and `aac sso login` report a failure by HTTP status and
946
+ AAC error code only, never the server's message or response body, because
947
+ either can echo the credential the request carried; their transport failures
948
+ show the error class and the operating system's reason, not the library's
949
+ text. Other commands still print the server's message with the status and
950
+ code.
951
+
924
952
  ## Enterprise tenants
925
953
 
926
954
  Enterprise tenants are registered through an onboarding ceremony: AAC
927
955
  operations opens a registration window and hands the tenant operator a
928
956
  bootstrap token, which the commands below read from `--bootstrap-token` or
929
- the `AAC_BOOTSTRAP_TOKEN` environment variable. The token authorises only
957
+ the `AAC_BOOTSTRAP_TOKEN` environment variable. The token authorizes only
930
958
  the onboarding steps shown with it; every other administration command runs
931
959
  under a signed-in session.
932
960
 
933
961
  For enterprise sign-in, AAC supports Microsoft Entra ID (also known as
934
- Azure Active Directory or Azure AD). If your organisation needs another
962
+ Azure Active Directory or Azure AD). If your organization needs another
935
963
  identity provider, contact support@cascadeauth.com.
936
964
 
937
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
 
@@ -19,8 +19,8 @@ The `aac` command is how a tenant's operator does everything that is not
19
19
  done by the sidecar itself: register the tenant, sign in, manage keys,
20
20
  workloads and domains, and prepare a runnable local workload. Everything
21
21
  works from a terminal or a script; there is no web console to click
22
- through. Output is JSON by default, and every command that reports a
23
- result also offers `--output table`.
22
+ through. Output is JSON by default. Commands that support table output offer
23
+ `--output table`; `aac sso describe` returns JSON only.
24
24
 
25
25
  Two kinds of tenant use the same tool:
26
26
 
@@ -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
@@ -830,8 +831,16 @@ aac sso logout --profile stage
830
831
  A developer tenant signs in with `--idp github` or `--idp google`; if you
831
832
  need another sign-in provider, contact support@cascadeauth.com. An
832
833
  enterprise tenant signs in through its Microsoft Entra ID connection, named
833
- by the connection's exact URL with `--idp-url`; when several connections
834
- are registered, the error message lists the exact commands to choose one.
834
+ by the connection's exact URL with `--idp-url`. With neither flag, the
835
+ tenant's own connections take precedence over the shared developer sign-ins;
836
+ when several remain, a terminal shows a numbered list to choose from and then
837
+ prints the flag that skips the prompt next time, while a script (no terminal)
838
+ gets exit 3 and one copy-ready command per connection. The list marks which
839
+ connection the cached session was last obtained through; it never shows
840
+ which connections have members. A developer tenant admits only the provider
841
+ and account it was registered with: when the AAC STS does not accept a shared
842
+ sign-in, the CLI reminds you of that rule and lists any other shared sign-ins. GitHub connections accept the device flow
843
+ only: `--flow pkce` with a GitHub connection stops locally with exit 3.
835
844
 
836
845
  Sessions last **4 hours by default**. Your administrator may configure a
837
846
  different lifetime for the deployment or connection. You can run
@@ -841,12 +850,20 @@ with the four-hour default, signing in at 09:00 gives an expiry of 13:00;
841
850
  signing in successfully again at 12:00 gives the new session an expiry of 16:00.
842
851
  Use `aac sso whoami --profile NAME --output table` to check the current
843
852
  expiration. If the session expires, sign in again; no refresh token is issued.
853
+ A command that needs a session stops locally with exit 3 when none is cached,
854
+ before anything is sent, and prints the `aac sso login` command for the same
855
+ profile and endpoints; commands that also accept a bootstrap token name that
856
+ alternative.
844
857
 
845
858
  Sign-in uses the device flow by default, so the browser can be on another
846
- machine and headless hosts work. Google connections use a loopback flow
847
- instead, because Google's device flow is scope-restricted; `--flow`
848
- overrides the choice. `whoami` and `logout` work offline and never contact
849
- AAC.
859
+ machine and headless hosts work. On a terminal with a browser at hand
860
+ (macOS, a desktop session, or `BROWSER` set), the device flow also offers to
861
+ open the sign-in page for you: press Enter to open it in your browser, or copy
862
+ the printed link and code into a browser yourself; `--no-browser` turns the
863
+ offer off, and a script (no terminal) or a headless host never sees it. Google
864
+ connections use a loopback flow instead, because Google's device flow is
865
+ scope-restricted; `--flow` overrides the choice. `whoami` and `logout` work
866
+ offline and never contact AAC.
850
867
 
851
868
  ## Output and exit codes
852
869
 
@@ -894,17 +911,28 @@ a callback gives up after 300 seconds with exit `3`. HTTP timeouts remain exit
894
911
  If AAC asks you to slow down, the command shows the interval to wait. The
895
912
  CLI never replays a change on its own; wait and rerun deliberately.
896
913
 
914
+ The commands that can present a ceremony token (`aac tenant register`,
915
+ `reissue-api-key`, `assign-hosted-domain`, `reactivate-hosted-domain`,
916
+ `bind-trust-domain`, `list-trust-domains`, `revoke-trust-domain`,
917
+ `rotate-admin-key`, `aac sso register-idp`, `enroll-idp-recovery-key` and
918
+ `approve-idp-repair`) and `aac sso login` report a failure by HTTP status and
919
+ AAC error code only, never the server's message or response body, because
920
+ either can echo the credential the request carried; their transport failures
921
+ show the error class and the operating system's reason, not the library's
922
+ text. Other commands still print the server's message with the status and
923
+ code.
924
+
897
925
  ## Enterprise tenants
898
926
 
899
927
  Enterprise tenants are registered through an onboarding ceremony: AAC
900
928
  operations opens a registration window and hands the tenant operator a
901
929
  bootstrap token, which the commands below read from `--bootstrap-token` or
902
- the `AAC_BOOTSTRAP_TOKEN` environment variable. The token authorises only
930
+ the `AAC_BOOTSTRAP_TOKEN` environment variable. The token authorizes only
903
931
  the onboarding steps shown with it; every other administration command runs
904
932
  under a signed-in session.
905
933
 
906
934
  For enterprise sign-in, AAC supports Microsoft Entra ID (also known as
907
- Azure Active Directory or Azure AD). If your organisation needs another
935
+ Azure Active Directory or Azure AD). If your organization needs another
908
936
  identity provider, contact support@cascadeauth.com.
909
937
 
910
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.10"
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(