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.
- {aac_cli-0.2.10/aac_cli.egg-info → aac_cli-0.2.12}/PKG-INFO +44 -16
- {aac_cli-0.2.10 → aac_cli-0.2.12}/README.md +43 -15
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/__init__.py +1 -1
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/cli.py +5 -2
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/data/renderer-source.json +7 -7
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/from_sidecar.py +4 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/inputs.py +2 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/model.py +97 -6
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/provenance.py +22 -6
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/visualizer.py +51 -8
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/admin_key_pem.py +3 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/api_key_rotation_state.py +4 -1
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/cli.py +320 -240
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/config.py +44 -14
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/idp_recovery.py +6 -3
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/registration_state.py +30 -18
- aac_cli-0.2.12/aac_cli/safe_output.py +311 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/sso_login.py +363 -58
- {aac_cli-0.2.10 → aac_cli-0.2.12/aac_cli.egg-info}/PKG-INFO +44 -16
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli.egg-info/SOURCES.txt +1 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/pyproject.toml +1 -1
- {aac_cli-0.2.10 → aac_cli-0.2.12}/LICENSE +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/MANIFEST.in +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/NOTICE +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/THIRD_PARTY_NOTICES.md +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/__main__.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/__init__.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/data/action-taken-v1.json +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/data/predicates-v1.json +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/narrative.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/schemas.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/temporal.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/theme.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/_aeg/timeline_source.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/aeg_cli.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/aeg_list.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/agent_cli.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/agent_config.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/agent_health.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/agent_layout.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/agent_record.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/chain_list.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/config_render.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/dev_material.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/init_cli.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/interrupts.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/material_cases.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/profiles.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/reference.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/secure_files.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/supplied_material.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli/trace_query.py +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli.egg-info/dependency_links.txt +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli.egg-info/requires.txt +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/aac_cli.egg-info/top_level.txt +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/launchers/aac +0 -0
- {aac_cli-0.2.10 → aac_cli-0.2.12}/launchers/aeg +0 -0
- {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.
|
|
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
|
|
|
@@ -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
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
861
|
-
|
|
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.
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
834
|
-
|
|
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.
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
|
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
|
|
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
|
|
@@ -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(
|