aac-cli 0.2.7__tar.gz → 0.2.9__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.7/aac_cli.egg-info → aac_cli-0.2.9}/PKG-INFO +12 -11
- {aac_cli-0.2.7 → aac_cli-0.2.9}/README.md +10 -9
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/__init__.py +1 -1
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/_aeg/cli.py +59 -17
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/_aeg/data/renderer-source.json +6 -4
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/_aeg/model.py +57 -0
- aac_cli-0.2.9/aac_cli/_aeg/provenance.py +194 -0
- aac_cli-0.2.9/aac_cli/_aeg/theme.py +136 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/_aeg/visualizer.py +205 -267
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/agent_cli.py +4 -4
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/config_render.py +20 -4
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/material_cases.py +1 -1
- {aac_cli-0.2.7 → aac_cli-0.2.9/aac_cli.egg-info}/PKG-INFO +12 -11
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli.egg-info/SOURCES.txt +2 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/pyproject.toml +2 -2
- {aac_cli-0.2.7 → aac_cli-0.2.9}/LICENSE +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/MANIFEST.in +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/NOTICE +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/THIRD_PARTY_NOTICES.md +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/__main__.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/_aeg/__init__.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/_aeg/data/action-taken-v1.json +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/_aeg/data/predicates-v1.json +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/_aeg/from_sidecar.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/_aeg/inputs.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/_aeg/narrative.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/_aeg/schemas.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/_aeg/temporal.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/_aeg/timeline_source.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/admin_key_pem.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/aeg_cli.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/aeg_list.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/agent_config.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/agent_health.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/agent_layout.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/agent_record.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/api_key_rotation_state.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/chain_list.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/cli.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/config.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/dev_material.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/idp_recovery.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/init_cli.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/profiles.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/reference.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/registration_state.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/secure_files.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/sso_login.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/supplied_material.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli/trace_query.py +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli.egg-info/dependency_links.txt +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli.egg-info/requires.txt +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/aac_cli.egg-info/top_level.txt +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/launchers/aac +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/launchers/aeg +0 -0
- {aac_cli-0.2.7 → aac_cli-0.2.9}/setup.cfg +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aac-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
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
|
|
7
|
-
Project-URL: Documentation, https://cascadeauth.
|
|
7
|
+
Project-URL: Documentation, https://docs.cascadeauth.com/cli/
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.11
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -34,12 +34,13 @@ which tenant they belong to and what they were authorised to do. A
|
|
|
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
|
|
|
37
|
-
New to AAC? [What AAC is for](https://cascadeauth.
|
|
37
|
+
New to AAC? [What AAC is for](https://docs.cascadeauth.com/overview/#what-aac-is-for)
|
|
38
38
|
explains what delegated authority between agents buys you, and what it does not.
|
|
39
39
|
|
|
40
|
-
The [CLI user guide and complete command reference](https://cascadeauth.
|
|
40
|
+
The [CLI user guide and complete command reference](https://docs.cascadeauth.com/cli/)
|
|
41
41
|
cover developer and enterprise onboarding, everyday administration and recovery.
|
|
42
|
-
|
|
42
|
+
These pages describe the latest supported release. Upgrade the CLI to use the
|
|
43
|
+
documented commands, and check your installation with `aac --version`.
|
|
43
44
|
|
|
44
45
|
The `aac` command is how a tenant's operator does everything that is not
|
|
45
46
|
done by the sidecar itself: register the tenant, sign in, manage keys,
|
|
@@ -77,7 +78,7 @@ Python 3.11 to 3.14 is supported. Release checks test all four versions.
|
|
|
77
78
|
## Use `aeg` to view agent execution graphs
|
|
78
79
|
|
|
79
80
|
Installing or upgrading `aac-cli` supplies both `aac` and `aeg`.
|
|
80
|
-
Use the [AEG guide and command reference](https://cascadeauth.
|
|
81
|
+
Use the [AEG guide and command reference](https://docs.cascadeauth.com/aeg/)
|
|
81
82
|
for listing local and central observations, rendering evidence, and selecting a profile.
|
|
82
83
|
|
|
83
84
|
`aeg list --profile PROFILE --since 7d` discovers participant-visible central
|
|
@@ -111,7 +112,7 @@ Google developer sign-in. See “Setting up aac-cli on a new computer” below
|
|
|
111
112
|
for the existing-tenant sign-in and material preparation steps.
|
|
112
113
|
|
|
113
114
|
Setup uses the tenant's assigned trust domain and writes the files the
|
|
114
|
-
trust
|
|
115
|
+
trust anchor publisher and Docker Compose need, as well as the sidecar
|
|
115
116
|
configuration. By default it generates development keys and certificates;
|
|
116
117
|
the certificate choices below explain how to supply your own instead.
|
|
117
118
|
|
|
@@ -341,7 +342,7 @@ and destination maps allow at most 128 entries each.
|
|
|
341
342
|
| `destinations.<name>.audience_pattern` | Required exact registered SPIFFE identity; no wildcard. | None |
|
|
342
343
|
| `destinations.<name>.predicates`, `.valid_for` | Same value/duration contract as classes. | Empty predicates; duration required |
|
|
343
344
|
| `destinations.<name>.timeout_ms` | Integer 1–2147483647 without A2A; 1–60000 for every destination when A2A is enabled, matching its generated deadline. | `10000` |
|
|
344
|
-
| `a2a.public_base_url`, `a2a.local_handler_url` | Optional A2A block; both required if present. Public URL is HTTPS with no path except optional `/`; the HTTP(S) local handler path must be exactly `/a2a/v1`, with no escaped spelling. Neither URL permits query or fragment markers, even empty ones. Managed A2A state paths
|
|
345
|
+
| `a2a.public_base_url`, `a2a.local_handler_url` | Optional A2A block; both required if present. Public URL is HTTPS with no path except optional `/`; the HTTP(S) local handler path must be exactly `/a2a/v1`, with no escaped spelling. Neither URL permits query or fragment markers, even empty ones. Managed A2A state paths, a 60-second deadline and retry-state capacity are generated; the capacity holds the acknowledgement of the largest reply, which carries the peer's reply to your agent. No A2A self-route is added. | A2A disabled |
|
|
345
346
|
|
|
346
347
|
URL hosts are ASCII DNS names or IP addresses; URL fields reject whitespace,
|
|
347
348
|
backslashes, embedded credentials and fragments. Basic replay history
|
|
@@ -438,7 +439,7 @@ The laptop case. The CLI creates a certificate authority on this machine and sig
|
|
|
438
439
|
|
|
439
440
|
**Renewal.** `aac agent renew --agent <name>` issues fresh certificates from the same CA.
|
|
440
441
|
|
|
441
|
-
#### I bring my own CA
|
|
442
|
+
#### I bring my own CA certificate
|
|
442
443
|
|
|
443
444
|
Your own issuer has signed the agent's certificates, and your CA private key never reaches this machine. Certificate source alone does not qualify a production deployment.
|
|
444
445
|
|
|
@@ -507,7 +508,7 @@ name and `<tenant-id>` the tenant id AAC allocated.
|
|
|
507
508
|
For what each of these proves, who else ever gets a copy, what never leaves you
|
|
508
509
|
and how the pieces fit together — including when your own certificate authority
|
|
509
510
|
signs instead of this tool — see
|
|
510
|
-
[Keys and certificates](https://cascadeauth.
|
|
511
|
+
[Keys and certificates](https://docs.cascadeauth.com/overview/keys-and-certificates/).
|
|
511
512
|
|
|
512
513
|
<!-- inventory-table:start -->
|
|
513
514
|
| No. | Material | Where | Used for | Lifetime | Back up? |
|
|
@@ -902,7 +903,7 @@ are described by their `--help`.
|
|
|
902
903
|
### Keys
|
|
903
904
|
|
|
904
905
|
Replacing the tenant-admin key is immediate and forward-only: the previous
|
|
905
|
-
key stops verifying at once and can never be reinstated. The trust
|
|
906
|
+
key stops verifying at once and can never be reinstated. The trust anchor
|
|
906
907
|
publisher signs its uploads with this key and loads it once, at start, so
|
|
907
908
|
sequence the change: stop the publisher, register the new public half, put
|
|
908
909
|
the new private key where the publisher's configuration points, start the
|
|
@@ -7,12 +7,13 @@ which tenant they belong to and what they were authorised to do. A
|
|
|
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
|
|
|
10
|
-
New to AAC? [What AAC is for](https://cascadeauth.
|
|
10
|
+
New to AAC? [What AAC is for](https://docs.cascadeauth.com/overview/#what-aac-is-for)
|
|
11
11
|
explains what delegated authority between agents buys you, and what it does not.
|
|
12
12
|
|
|
13
|
-
The [CLI user guide and complete command reference](https://cascadeauth.
|
|
13
|
+
The [CLI user guide and complete command reference](https://docs.cascadeauth.com/cli/)
|
|
14
14
|
cover developer and enterprise onboarding, everyday administration and recovery.
|
|
15
|
-
|
|
15
|
+
These pages describe the latest supported release. Upgrade the CLI to use the
|
|
16
|
+
documented commands, and check your installation with `aac --version`.
|
|
16
17
|
|
|
17
18
|
The `aac` command is how a tenant's operator does everything that is not
|
|
18
19
|
done by the sidecar itself: register the tenant, sign in, manage keys,
|
|
@@ -50,7 +51,7 @@ Python 3.11 to 3.14 is supported. Release checks test all four versions.
|
|
|
50
51
|
## Use `aeg` to view agent execution graphs
|
|
51
52
|
|
|
52
53
|
Installing or upgrading `aac-cli` supplies both `aac` and `aeg`.
|
|
53
|
-
Use the [AEG guide and command reference](https://cascadeauth.
|
|
54
|
+
Use the [AEG guide and command reference](https://docs.cascadeauth.com/aeg/)
|
|
54
55
|
for listing local and central observations, rendering evidence, and selecting a profile.
|
|
55
56
|
|
|
56
57
|
`aeg list --profile PROFILE --since 7d` discovers participant-visible central
|
|
@@ -84,7 +85,7 @@ Google developer sign-in. See “Setting up aac-cli on a new computer” below
|
|
|
84
85
|
for the existing-tenant sign-in and material preparation steps.
|
|
85
86
|
|
|
86
87
|
Setup uses the tenant's assigned trust domain and writes the files the
|
|
87
|
-
trust
|
|
88
|
+
trust anchor publisher and Docker Compose need, as well as the sidecar
|
|
88
89
|
configuration. By default it generates development keys and certificates;
|
|
89
90
|
the certificate choices below explain how to supply your own instead.
|
|
90
91
|
|
|
@@ -314,7 +315,7 @@ and destination maps allow at most 128 entries each.
|
|
|
314
315
|
| `destinations.<name>.audience_pattern` | Required exact registered SPIFFE identity; no wildcard. | None |
|
|
315
316
|
| `destinations.<name>.predicates`, `.valid_for` | Same value/duration contract as classes. | Empty predicates; duration required |
|
|
316
317
|
| `destinations.<name>.timeout_ms` | Integer 1–2147483647 without A2A; 1–60000 for every destination when A2A is enabled, matching its generated deadline. | `10000` |
|
|
317
|
-
| `a2a.public_base_url`, `a2a.local_handler_url` | Optional A2A block; both required if present. Public URL is HTTPS with no path except optional `/`; the HTTP(S) local handler path must be exactly `/a2a/v1`, with no escaped spelling. Neither URL permits query or fragment markers, even empty ones. Managed A2A state paths
|
|
318
|
+
| `a2a.public_base_url`, `a2a.local_handler_url` | Optional A2A block; both required if present. Public URL is HTTPS with no path except optional `/`; the HTTP(S) local handler path must be exactly `/a2a/v1`, with no escaped spelling. Neither URL permits query or fragment markers, even empty ones. Managed A2A state paths, a 60-second deadline and retry-state capacity are generated; the capacity holds the acknowledgement of the largest reply, which carries the peer's reply to your agent. No A2A self-route is added. | A2A disabled |
|
|
318
319
|
|
|
319
320
|
URL hosts are ASCII DNS names or IP addresses; URL fields reject whitespace,
|
|
320
321
|
backslashes, embedded credentials and fragments. Basic replay history
|
|
@@ -411,7 +412,7 @@ The laptop case. The CLI creates a certificate authority on this machine and sig
|
|
|
411
412
|
|
|
412
413
|
**Renewal.** `aac agent renew --agent <name>` issues fresh certificates from the same CA.
|
|
413
414
|
|
|
414
|
-
#### I bring my own CA
|
|
415
|
+
#### I bring my own CA certificate
|
|
415
416
|
|
|
416
417
|
Your own issuer has signed the agent's certificates, and your CA private key never reaches this machine. Certificate source alone does not qualify a production deployment.
|
|
417
418
|
|
|
@@ -480,7 +481,7 @@ name and `<tenant-id>` the tenant id AAC allocated.
|
|
|
480
481
|
For what each of these proves, who else ever gets a copy, what never leaves you
|
|
481
482
|
and how the pieces fit together — including when your own certificate authority
|
|
482
483
|
signs instead of this tool — see
|
|
483
|
-
[Keys and certificates](https://cascadeauth.
|
|
484
|
+
[Keys and certificates](https://docs.cascadeauth.com/overview/keys-and-certificates/).
|
|
484
485
|
|
|
485
486
|
<!-- inventory-table:start -->
|
|
486
487
|
| No. | Material | Where | Used for | Lifetime | Back up? |
|
|
@@ -875,7 +876,7 @@ are described by their `--help`.
|
|
|
875
876
|
### Keys
|
|
876
877
|
|
|
877
878
|
Replacing the tenant-admin key is immediate and forward-only: the previous
|
|
878
|
-
key stops verifying at once and can never be reinstated. The trust
|
|
879
|
+
key stops verifying at once and can never be reinstated. The trust anchor
|
|
879
880
|
publisher signs its uploads with this key and loads it once, at start, so
|
|
880
881
|
sequence the change: stop the publisher, register the new public half, put
|
|
881
882
|
the new private key where the publisher's configuration points, start the
|
|
@@ -23,6 +23,7 @@ from .inputs import (
|
|
|
23
23
|
unjoinable_diagnostics,
|
|
24
24
|
)
|
|
25
25
|
from .model import extract_token_relation_graph
|
|
26
|
+
from .provenance import SourceInput, summarize_sources
|
|
26
27
|
from .visualizer import build_visual_aeg, write_html_aeg
|
|
27
28
|
|
|
28
29
|
Query = Callable[[str, str], tuple[str, list]]
|
|
@@ -98,7 +99,7 @@ def _query(profile: str, root: str):
|
|
|
98
99
|
raise InputError("online rendering requires the aeg command supplied by aac-cli")
|
|
99
100
|
|
|
100
101
|
|
|
101
|
-
def _render(args, events, query: Query) -> int:
|
|
102
|
+
def _render(args, events, query: Query, sources: list[SourceInput]) -> int:
|
|
102
103
|
if args.profile is not None and not args.profile.strip():
|
|
103
104
|
raise InputError("--profile must name an existing AAC CLI profile")
|
|
104
105
|
if not (args.events or args.actions or args.profile or args.trace_json):
|
|
@@ -117,6 +118,14 @@ def _render(args, events, query: Query) -> int:
|
|
|
117
118
|
if args.trace_json:
|
|
118
119
|
trace_root, trace_events = read_trace(read_json(args.trace_json), str(args.trace_json))
|
|
119
120
|
events.extend(trace_events)
|
|
121
|
+
sources.append(
|
|
122
|
+
SourceInput(
|
|
123
|
+
"Control-plane trace data",
|
|
124
|
+
str(args.trace_json),
|
|
125
|
+
"Saved export; original fetch time/status unavailable",
|
|
126
|
+
trace_events,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
120
129
|
if root and root != trace_root:
|
|
121
130
|
raise InputError("selected root does not match --trace-json root_token_id")
|
|
122
131
|
if root is None:
|
|
@@ -138,13 +147,47 @@ def _render(args, events, query: Query) -> int:
|
|
|
138
147
|
if queried_root != root:
|
|
139
148
|
raise InputError("central response root does not match selected root")
|
|
140
149
|
events.extend(central)
|
|
141
|
-
|
|
150
|
+
sources.append(
|
|
151
|
+
SourceInput(
|
|
152
|
+
"Control-plane trace data",
|
|
153
|
+
f"Profile: {args.profile}",
|
|
154
|
+
"Live API query",
|
|
155
|
+
central,
|
|
156
|
+
"succeeded",
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
central_status = (
|
|
160
|
+
"Live API query succeeded"
|
|
161
|
+
if central
|
|
162
|
+
else "Live API query succeeded with no observations"
|
|
163
|
+
)
|
|
142
164
|
except InputError as exc:
|
|
143
165
|
query_failed = True
|
|
144
|
-
central_status =
|
|
145
|
-
|
|
166
|
+
central_status = (
|
|
167
|
+
"Live API query failed; local graph is partial. See the command diagnostic."
|
|
168
|
+
)
|
|
169
|
+
# Preserve stable public codes without copying transport URLs/secrets.
|
|
170
|
+
for code in (
|
|
171
|
+
"ERR_TRACE_NOT_FOUND",
|
|
172
|
+
"ERR_API_KEY_INVALID",
|
|
173
|
+
"ERR_DATABASE_UNAVAILABLE",
|
|
174
|
+
"ERR_RATE_LIMITED",
|
|
175
|
+
"ERR_RATE_LIMITER_UNAVAILABLE",
|
|
176
|
+
):
|
|
177
|
+
if re.search(r"\b" + code + r"\b", str(exc)):
|
|
178
|
+
central_status += f" {code}."
|
|
179
|
+
sources.append(
|
|
180
|
+
SourceInput(
|
|
181
|
+
"Control-plane trace data",
|
|
182
|
+
f"Profile: {args.profile}",
|
|
183
|
+
"Live API query",
|
|
184
|
+
status="failed",
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
# Transport diagnostics may contain URLs; never copy them into a shareable HTML artifact.
|
|
188
|
+
print(str(exc), file=sys.stderr)
|
|
146
189
|
elif args.trace_json:
|
|
147
|
-
central_status =
|
|
190
|
+
central_status = "Saved export read offline; original fetch time/status unavailable"
|
|
148
191
|
selected, diagnostics = select_events(events, root)
|
|
149
192
|
graph = extract_token_relation_graph(selected)
|
|
150
193
|
if not graph.tokens:
|
|
@@ -152,16 +195,6 @@ def _render(args, events, query: Query) -> int:
|
|
|
152
195
|
"no reconstructable token observations for the selected root; supply sidecar evidence or an available central trace"
|
|
153
196
|
)
|
|
154
197
|
graph.diagnostics.extend(diagnostics)
|
|
155
|
-
graph.diagnostics.extend(
|
|
156
|
-
[
|
|
157
|
-
f"Selected root: {root}",
|
|
158
|
-
f"Central evidence: {central_status}",
|
|
159
|
-
"Partial evidence: only supplied observations are shown. Missing participants, ancestors, fields and outcomes are not success or failure.",
|
|
160
|
-
"Central metadata intentionally omits private business details, authority caps, exact expiry and full receipts. Forwarding may also omit or delay events.",
|
|
161
|
-
"Application records are reports, not verified business truth. A mint/receive success does not prove business completion. Receipt verdicts are observations, not re-verification by this tool.",
|
|
162
|
-
"Distinct action records are retained; these formats do not establish exactly-once business-action counts.",
|
|
163
|
-
]
|
|
164
|
-
)
|
|
165
198
|
protected = [
|
|
166
199
|
*args.events,
|
|
167
200
|
*args.actions,
|
|
@@ -171,6 +204,7 @@ def _render(args, events, query: Query) -> int:
|
|
|
171
204
|
if args.output.resolve() in {path.resolve() for path in protected}:
|
|
172
205
|
raise InputError("--output must not overwrite an evidence input")
|
|
173
206
|
visual = build_visual_aeg(graph)
|
|
207
|
+
visual.provenance = summarize_sources(sources, selected, central_status=central_status)
|
|
174
208
|
with tempfile.TemporaryDirectory(prefix="aac-aeg-") as directory:
|
|
175
209
|
rendered = write_html_aeg(visual, root, out_dir=Path(directory))
|
|
176
210
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -334,9 +368,17 @@ def main(
|
|
|
334
368
|
args = parser(prog=prog, version_text=version_text).parse_args(argv)
|
|
335
369
|
events = []
|
|
336
370
|
try:
|
|
337
|
-
|
|
371
|
+
sources = []
|
|
372
|
+
for paths, reader, kind in (
|
|
373
|
+
(args.events, read_events, "Sidecar telemetry"),
|
|
374
|
+
(args.actions, read_actions, "Application action records"),
|
|
375
|
+
):
|
|
376
|
+
for path in paths:
|
|
377
|
+
records = reader([path])
|
|
378
|
+
events.extend(records)
|
|
379
|
+
sources.append(SourceInput(kind, str(path), "Local file", records))
|
|
338
380
|
return (
|
|
339
|
-
_render(args, events, query or _query)
|
|
381
|
+
_render(args, events, query or _query, sources)
|
|
340
382
|
if args.command == "render"
|
|
341
383
|
else _list(args, events, list_query)
|
|
342
384
|
)
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
|
-
"content_sha256": "
|
|
2
|
+
"content_sha256": "6e8bcd00bd94c415e04af65deb527be4d1581a6a5396d74c95708d6dda97fca4",
|
|
3
3
|
"files": {
|
|
4
4
|
"__init__.py": "80d80a1ba35353d271cddf02532801c60b57f5ec06fb268ae823747742ce8979",
|
|
5
|
-
"cli.py": "
|
|
5
|
+
"cli.py": "98aa2b0c528fefb5f1f90a16584a6fbda1f075ebfcf61605f52948cdb940a1af",
|
|
6
6
|
"data/action-taken-v1.json": "d863343afe7f314646a73cf063480a7e650f93857d2ed5ec3a7b1f9838671fdf",
|
|
7
7
|
"data/predicates-v1.json": "35e83a29feada10e8cfbef7009245146a9717d4f4fd58d74124e0ec91ae922c8",
|
|
8
8
|
"from_sidecar.py": "6ac023f6925a13e94e0fee762656a02ca98981f2add194b052e9014ca48e2219",
|
|
9
9
|
"inputs.py": "ebf1ca77957d29d59683348f6916bac3c21e2037f087f26ff7c7242d75edc77c",
|
|
10
|
-
"model.py": "
|
|
10
|
+
"model.py": "b631dcd7af587dcc89c18e1058bc81fb1043f04250a0d6ea585f9c595fd75cfc",
|
|
11
11
|
"narrative.py": "8ac4b12add84cc8be907cff5f25e58731ab1144888aec8d376e286de461a6179",
|
|
12
|
+
"provenance.py": "d310e13bfd45cb16edcc72e7b2bc28c916e3ff45208e2b44240e589f0ec0558d",
|
|
12
13
|
"schemas.py": "c47e0138fb8bfeaf1db1725a00d4a37ff4a136a9e8f5907356b1006f301a9ded",
|
|
13
14
|
"temporal.py": "b69081e9280d578dc1261a276da3178e979a02efaf1137f720b4520ed543a36d",
|
|
15
|
+
"theme.py": "ded47be13f5c591da95a1bb91ee1b665c1570c929d842bdaa1a41325d0b40caa",
|
|
14
16
|
"timeline_source.py": "dc875458d69c6135b3ffe0db9c35e511da603b5d52409f780f3e1b74dc464520",
|
|
15
|
-
"visualizer.py": "
|
|
17
|
+
"visualizer.py": "5eb7b863652c51a4a5751bcd8ae735b688e6e3e61089f0c158473ebb52ae6655"
|
|
16
18
|
},
|
|
17
19
|
"legal": {
|
|
18
20
|
"LICENSE": "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594",
|
|
@@ -215,6 +215,9 @@ 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
|
+
# Canonical associations observed for this leaf and exact receiver workload.
|
|
219
|
+
# A trust-domain match, another token, or a wildcard is not such evidence.
|
|
220
|
+
observed_tenant_ids: tuple[str, ...] = ()
|
|
218
221
|
|
|
219
222
|
|
|
220
223
|
@dataclass(frozen=True)
|
|
@@ -267,6 +270,9 @@ class TokenRelationGraph:
|
|
|
267
270
|
actions: dict[str, list[UnifiedEvent]] = field(default_factory=dict)
|
|
268
271
|
diagnostics: list[str] = field(default_factory=list)
|
|
269
272
|
receipts: dict[str, list[UnifiedEvent]] = field(default_factory=dict)
|
|
273
|
+
# Retain conflicting creation claims even when one token's merged fields
|
|
274
|
+
# become unknown. Receiving/reporting observations cannot supply this map.
|
|
275
|
+
creator_tenant_claims: dict[str, tuple[str, ...]] = field(default_factory=dict)
|
|
270
276
|
|
|
271
277
|
|
|
272
278
|
# ---------------------------------------------------------------------------
|
|
@@ -298,6 +304,37 @@ def extract_token_relation_graph(events: list[UnifiedEvent]) -> TokenRelationGra
|
|
|
298
304
|
terminals,
|
|
299
305
|
)
|
|
300
306
|
|
|
307
|
+
creation_events = [
|
|
308
|
+
event
|
|
309
|
+
for event in events
|
|
310
|
+
if event.event_type in {"mint_root", "attenuate", "composite_mint"}
|
|
311
|
+
and event.token_id in tokens
|
|
312
|
+
and not tokens[event.token_id].placeholder
|
|
313
|
+
]
|
|
314
|
+
token_creators: dict[str, set[str]] = {}
|
|
315
|
+
for event in creation_events:
|
|
316
|
+
actor = event.raw_event.get("presenter_spiffe_id")
|
|
317
|
+
candidates = token_creators.setdefault(event.token_id, set())
|
|
318
|
+
if isinstance(actor, str) and actor:
|
|
319
|
+
candidates.add(actor)
|
|
320
|
+
# Another observation of this exact token may supply its agreed
|
|
321
|
+
# creator workload. Receiving tenant identity is never used here.
|
|
322
|
+
if tokens[event.token_id].creator_spiffe_id:
|
|
323
|
+
candidates.add(tokens[event.token_id].creator_spiffe_id)
|
|
324
|
+
creator_claims: dict[str, set[str]] = {}
|
|
325
|
+
for event in creation_events:
|
|
326
|
+
actor = event.raw_event.get("presenter_spiffe_id")
|
|
327
|
+
tenant = event.raw_event.get("tenant_id")
|
|
328
|
+
if isinstance(tenant, str) and tenant:
|
|
329
|
+
# A sparse creation row still disputes the same token's tenant.
|
|
330
|
+
# Preserve its possible tenant for the recorded creator candidates
|
|
331
|
+
# instead of silently turning a conflict into a unique association.
|
|
332
|
+
actors = (
|
|
333
|
+
(actor,) if isinstance(actor, str) and actor else token_creators[event.token_id]
|
|
334
|
+
)
|
|
335
|
+
for candidate in actors:
|
|
336
|
+
creator_claims.setdefault(candidate, set()).add(tenant)
|
|
337
|
+
|
|
301
338
|
return TokenRelationGraph(
|
|
302
339
|
tokens=tokens,
|
|
303
340
|
originators=originators,
|
|
@@ -306,6 +343,9 @@ def extract_token_relation_graph(events: list[UnifiedEvent]) -> TokenRelationGra
|
|
|
306
343
|
leaf_token_ids=leaf_token_ids,
|
|
307
344
|
root_token_ids=root_token_ids,
|
|
308
345
|
chain_failures=chain_failures,
|
|
346
|
+
creator_tenant_claims={
|
|
347
|
+
actor: tuple(sorted(tenants)) for actor, tenants in creator_claims.items()
|
|
348
|
+
},
|
|
309
349
|
actions={
|
|
310
350
|
tid: sorted(
|
|
311
351
|
(e for e in events if e.event_type == "action_taken" and e.token_id == tid),
|
|
@@ -787,7 +827,14 @@ def _extract_terminals(
|
|
|
787
827
|
for the same token_id, its summary + payload + presenter are attached.
|
|
788
828
|
"""
|
|
789
829
|
action_by_token: dict[str, list[UnifiedEvent]] = {}
|
|
830
|
+
receiver_claims: dict[tuple[str, str], set[str]] = {}
|
|
790
831
|
for e in events:
|
|
832
|
+
if e.event_type in {"token_received", "receipt_observed", "action_taken"} or (
|
|
833
|
+
e.event_type == "chain_failure" and e.raw_event.get("wire_event_type") == "receive"
|
|
834
|
+
):
|
|
835
|
+
actor, tenant = e.raw_event.get("actor_spiffe_id"), e.raw_event.get("tenant_id")
|
|
836
|
+
if isinstance(actor, str) and isinstance(tenant, str) and actor and tenant:
|
|
837
|
+
receiver_claims.setdefault((e.token_id, actor), set()).add(tenant)
|
|
791
838
|
# TENANT_SOURCE_PREFIX (defined in aeg.schemas) is the shared
|
|
792
839
|
# producer/consumer contract — `from_sidecar.py` sets
|
|
793
840
|
# `source=f"{TENANT_SOURCE_PREFIX}{name}"` on per-tenant SIEM
|
|
@@ -823,6 +870,14 @@ def _extract_terminals(
|
|
|
823
870
|
continue
|
|
824
871
|
tenant_id = _tenant_from_spiffe_pattern(audience)
|
|
825
872
|
workload_label = _workload_label_from_spiffe(audience)
|
|
873
|
+
# Readers accept identity strings, including opaque imported values.
|
|
874
|
+
# A pattern actor must not establish an exact receiver association even
|
|
875
|
+
# if it happens to equal the audience string; the view also guards it.
|
|
876
|
+
observed_tenants = (
|
|
877
|
+
tuple(sorted(receiver_claims.get((tid, audience), ())))
|
|
878
|
+
if not any(c in audience for c in "*?[")
|
|
879
|
+
else ()
|
|
880
|
+
)
|
|
826
881
|
actions = sorted(
|
|
827
882
|
action_by_token.get(tid, []),
|
|
828
883
|
key=lambda e: (e.timestamp_iso, json.dumps(e.raw_event, sort_keys=True)),
|
|
@@ -845,6 +900,7 @@ def _extract_terminals(
|
|
|
845
900
|
leaf_token_id=tid,
|
|
846
901
|
tenant_id=tenant_id,
|
|
847
902
|
workload_label=workload_label,
|
|
903
|
+
observed_tenant_ids=observed_tenants,
|
|
848
904
|
action_summary=raw.get("action_summary"),
|
|
849
905
|
action_payload=raw.get("action_payload") or {},
|
|
850
906
|
# B44.3: read the spec-canonical field name `actor_spiffe_id`
|
|
@@ -864,6 +920,7 @@ def _extract_terminals(
|
|
|
864
920
|
leaf_token_id=tid,
|
|
865
921
|
tenant_id=tenant_id,
|
|
866
922
|
workload_label=workload_label,
|
|
923
|
+
observed_tenant_ids=observed_tenants,
|
|
867
924
|
)
|
|
868
925
|
)
|
|
869
926
|
return out
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Render-time input inventory. Never infers acquisition from event contents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from html import escape
|
|
8
|
+
|
|
9
|
+
from .schemas import UnifiedEvent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def participant_observations(events: list[UnifiedEvent]) -> tuple[tuple[str, str], ...]:
|
|
13
|
+
"""Recorded reporting identity; a received token's presenter is not its reporter."""
|
|
14
|
+
pairs = set()
|
|
15
|
+
for event in events:
|
|
16
|
+
# Use normalized identity fields. Imported _record extensions remain
|
|
17
|
+
# opaque even when they happen to be dictionaries.
|
|
18
|
+
raw = event.raw_event
|
|
19
|
+
tenant = raw.get("tenant_id") or ""
|
|
20
|
+
actor = raw.get("actor_spiffe_id") or ""
|
|
21
|
+
if not actor and event.event_type in {"mint_root", "attenuate", "composite_mint"}:
|
|
22
|
+
actor = raw.get("presenter_spiffe_id") or ""
|
|
23
|
+
if tenant or actor:
|
|
24
|
+
pairs.add((str(tenant), str(actor)))
|
|
25
|
+
return tuple(sorted(pairs))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class SourceInput:
|
|
30
|
+
kind: str
|
|
31
|
+
label: str
|
|
32
|
+
acquisition: str
|
|
33
|
+
events: list[UnifiedEvent] = field(default_factory=list)
|
|
34
|
+
status: str = "read"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class SourceSummary:
|
|
39
|
+
kind: str
|
|
40
|
+
label: str
|
|
41
|
+
acquisition: str
|
|
42
|
+
status: str
|
|
43
|
+
matching_records: int
|
|
44
|
+
participants: tuple[tuple[str, str], ...]
|
|
45
|
+
first_observed: str | None
|
|
46
|
+
last_observed: str | None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class Provenance:
|
|
51
|
+
sources: tuple[SourceSummary, ...]
|
|
52
|
+
central_status: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def summarize_sources(
|
|
56
|
+
sources: list[SourceInput], selected: list[UnifiedEvent], *, central_status: str
|
|
57
|
+
) -> Provenance:
|
|
58
|
+
# Selection retains the original event objects. Track membership by identity,
|
|
59
|
+
# not by a filename, timestamp or caller-controlled event field that could
|
|
60
|
+
# make an overlapping local record look like a successful central query.
|
|
61
|
+
selected_ids = {id(event) for event in selected}
|
|
62
|
+
summaries = []
|
|
63
|
+
for source in sources:
|
|
64
|
+
matching = [event for event in source.events if id(event) in selected_ids]
|
|
65
|
+
records = {event.raw_event.get("_source", id(event)) for event in matching}
|
|
66
|
+
times = sorted(
|
|
67
|
+
datetime.fromisoformat(event.timestamp_iso.replace("Z", "+00:00"))
|
|
68
|
+
.astimezone(timezone.utc)
|
|
69
|
+
.isoformat()
|
|
70
|
+
for event in matching
|
|
71
|
+
)
|
|
72
|
+
summaries.append(
|
|
73
|
+
SourceSummary(
|
|
74
|
+
source.kind,
|
|
75
|
+
source.label,
|
|
76
|
+
source.acquisition,
|
|
77
|
+
source.status,
|
|
78
|
+
len(records),
|
|
79
|
+
participant_observations(matching),
|
|
80
|
+
times[0] if times else None,
|
|
81
|
+
times[-1] if times else None,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
return Provenance(tuple(summaries), central_status)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def source_panel(provenance: Provenance | None, diagnostics: list[str]) -> str:
|
|
88
|
+
"""One escaped, initially collapsed panel shared by CLI and hosted renders."""
|
|
89
|
+
sources = provenance.sources if provenance else ()
|
|
90
|
+
used = {source.kind for source in sources if source.matching_records}
|
|
91
|
+
summary = (
|
|
92
|
+
" + ".join(
|
|
93
|
+
kind
|
|
94
|
+
for kind in (
|
|
95
|
+
"Sidecar telemetry",
|
|
96
|
+
"Control-plane trace data",
|
|
97
|
+
"Application action records",
|
|
98
|
+
)
|
|
99
|
+
if kind in used
|
|
100
|
+
)
|
|
101
|
+
or "Source inventory not supplied"
|
|
102
|
+
)
|
|
103
|
+
parts = [
|
|
104
|
+
'<details id="aac-sources"><summary>Data sources used for this graph'
|
|
105
|
+
f'<span class="aac-source-summary">{escape(summary)}</span></summary>'
|
|
106
|
+
'<div class="aac-source-body">'
|
|
107
|
+
]
|
|
108
|
+
if provenance is None:
|
|
109
|
+
parts.append(
|
|
110
|
+
"<p>This caller did not supply a source inventory. Files, acquisition method "
|
|
111
|
+
"and control-plane query status are unknown; they must not be inferred from the graph.</p>"
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
parts.append(
|
|
115
|
+
f"<p><strong>Control-plane trace:</strong> {escape(provenance.central_status)}</p>"
|
|
116
|
+
)
|
|
117
|
+
parts.append(
|
|
118
|
+
'<div class="aac-source-table"><table><thead><tr><th>Source and input</th>'
|
|
119
|
+
"<th>Observed tenant / workload</th><th>Matching records</th>"
|
|
120
|
+
"<th>Observed record span (UTC)</th></tr></thead><tbody>"
|
|
121
|
+
)
|
|
122
|
+
for source in sources:
|
|
123
|
+
identities = (
|
|
124
|
+
"<br>".join(
|
|
125
|
+
f"{escape(tenant or 'Tenant not recorded')} / {escape(actor or 'Workload not recorded')}"
|
|
126
|
+
for tenant, actor in source.participants
|
|
127
|
+
)
|
|
128
|
+
or "No matching identity observations"
|
|
129
|
+
)
|
|
130
|
+
span = (
|
|
131
|
+
f"{escape(source.first_observed)} → {escape(source.last_observed or '')}"
|
|
132
|
+
if source.first_observed
|
|
133
|
+
else "No matching observations"
|
|
134
|
+
)
|
|
135
|
+
contribution = "Contributed" if source.matching_records else "Did not contribute"
|
|
136
|
+
parts.append(
|
|
137
|
+
f"<tr><td><strong>{escape(source.kind)}</strong><br><code>{escape(source.label)}</code>"
|
|
138
|
+
f"<br>{escape(source.acquisition)} · {escape(source.status)} · {contribution}</td>"
|
|
139
|
+
f"<td>{identities}</td><td>{source.matching_records}</td><td>{span}</td></tr>"
|
|
140
|
+
)
|
|
141
|
+
parts.append(
|
|
142
|
+
"</tbody></table></div><p>Counts describe matching input rows for files or normalized "
|
|
143
|
+
"observations for hosted data, not unique business actions. Timestamp spans describe "
|
|
144
|
+
"available records, not execution start/end or continuous coverage.</p>"
|
|
145
|
+
)
|
|
146
|
+
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>"
|
|
151
|
+
)
|
|
152
|
+
if "Control-plane trace data" in used:
|
|
153
|
+
parts.append(
|
|
154
|
+
"<li>Central metadata omits private business details, authority caps, exact expiry "
|
|
155
|
+
"and full receipts. Best-effort forwarding may omit or delay events.</li>"
|
|
156
|
+
)
|
|
157
|
+
if "Application action records" in used:
|
|
158
|
+
parts.append(
|
|
159
|
+
"<li>Application records are reports, not independently verified business truth.</li>"
|
|
160
|
+
)
|
|
161
|
+
if "Sidecar telemetry" in used:
|
|
162
|
+
parts.append(
|
|
163
|
+
"<li>Receipt verdicts are recorded sidecar observations; AEG does not re-verify signatures.</li>"
|
|
164
|
+
)
|
|
165
|
+
parts.append(
|
|
166
|
+
"<li>Repeated records do not establish exactly-once business-action counts.</li></ul>"
|
|
167
|
+
)
|
|
168
|
+
if diagnostics:
|
|
169
|
+
parts.append(
|
|
170
|
+
"<h3>Input diagnostics</h3><ul>"
|
|
171
|
+
+ "".join(f"<li>{escape(note)}</li>" for note in dict.fromkeys(diagnostics))
|
|
172
|
+
+ "</ul>"
|
|
173
|
+
)
|
|
174
|
+
parts.append(
|
|
175
|
+
'<section class="aac-next-steps"><h2>If an action or result is missing</h2>'
|
|
176
|
+
"<p>Check the chain root/attempt, input files, observed times and query status first.</p><ul>"
|
|
177
|
+
"<li><strong>Missing tokens, participants or handoffs:</strong> add authorized participants’ "
|
|
178
|
+
"retained sidecar files with <code>--events FILE</code>. An authorized trace via "
|
|
179
|
+
"<code>--profile PROFILE</code> or a saved <code>--trace-json FILE</code> export may add "
|
|
180
|
+
"forwarded observations. For a sidecar + application graph (combo 3), adding a profile "
|
|
181
|
+
"uses combo 4; it does not guarantee a missing result.</li>"
|
|
182
|
+
"<li><strong>Missing business details/results:</strong> add matching structured "
|
|
183
|
+
"<code>action_taken</code> records with <code>--actions FILE</code>; check token IDs and "
|
|
184
|
+
"the token-to-root mapping. Arbitrary application logs are not supported. More central "
|
|
185
|
+
"data cannot supply private business payloads or full receipts.</li>"
|
|
186
|
+
"<li><strong>Failed query:</strong> follow the diagnostic; check profile/access configuration "
|
|
187
|
+
"and connectivity, then retry. Failure is not a successful empty query.</li>"
|
|
188
|
+
"<li><strong>Possibly delayed forwarding:</strong> render again later or use retained local "
|
|
189
|
+
"files. Data never recorded, never forwarded or no longer retained may remain unavailable.</li>"
|
|
190
|
+
"</ul><p>More source categories help only when matching records exist. Otherwise leave the "
|
|
191
|
+
"outcome unknown. Rendering again reads evidence; it does not rerun the business action.</p>"
|
|
192
|
+
"</section></div></details>"
|
|
193
|
+
)
|
|
194
|
+
return "".join(parts)
|