aac-cli 0.2.4__tar.gz → 0.2.6__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.4 → aac_cli-0.2.6}/PKG-INFO +24 -6
- aac_cli-0.2.4/aac_cli.egg-info/PKG-INFO → aac_cli-0.2.6/README.md +22 -26
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/__init__.py +1 -1
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/cli.py +74 -16
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/data/renderer-source.json +2 -2
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/aeg_cli.py +4 -1
- aac_cli-0.2.6/aac_cli/aeg_list.py +173 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/agent_layout.py +0 -18
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/agent_record.py +1 -1
- aac_cli-0.2.6/aac_cli/chain_list.py +100 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/cli.py +59 -8
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/config.py +12 -85
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/init_cli.py +0 -3
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/registration_state.py +9 -48
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/trace_query.py +20 -5
- aac_cli-0.2.4/README.md → aac_cli-0.2.6/aac_cli.egg-info/PKG-INFO +44 -4
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli.egg-info/SOURCES.txt +2 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/pyproject.toml +4 -4
- {aac_cli-0.2.4 → aac_cli-0.2.6}/LICENSE +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/MANIFEST.in +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/NOTICE +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/THIRD_PARTY_NOTICES.md +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/__main__.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/__init__.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/data/action-taken-v1.json +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/data/predicates-v1.json +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/from_sidecar.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/inputs.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/model.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/narrative.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/schemas.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/temporal.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/timeline_source.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/visualizer.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/admin_key_pem.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/agent_cli.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/agent_config.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/agent_health.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/api_key_rotation_state.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/config_render.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/dev_material.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/idp_recovery.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/material_cases.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/profiles.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/reference.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/secure_files.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/sso_login.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/supplied_material.py +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli.egg-info/dependency_links.txt +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli.egg-info/requires.txt +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli.egg-info/top_level.txt +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/launchers/aac +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/launchers/aeg +0 -0
- {aac_cli-0.2.4 → aac_cli-0.2.6}/setup.cfg +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aac-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
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
7
|
Project-URL: Documentation, https://cascadeauth.github.io/aac-starter-guide/aeg.html
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
License-File: NOTICE
|
|
@@ -59,7 +59,7 @@ pip install aac-cli
|
|
|
59
59
|
aac --version
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Python 3.
|
|
62
|
+
Python 3.11 or newer is required.
|
|
63
63
|
|
|
64
64
|
> **Mind the name.** `pip install aac` installs an unrelated project that
|
|
65
65
|
> happens to share the acronym. The AAC package is **`aac-cli`**; the
|
|
@@ -69,8 +69,13 @@ Python 3.10 or newer is required.
|
|
|
69
69
|
|
|
70
70
|
Installing or upgrading `aac-cli` supplies both `aac` and `aeg`.
|
|
71
71
|
Use the [AEG guide and command reference](https://cascadeauth.github.io/aac-starter-guide/aeg.html)
|
|
72
|
-
for
|
|
73
|
-
|
|
72
|
+
for listing local and central observations, rendering evidence, and selecting a profile.
|
|
73
|
+
|
|
74
|
+
`aeg list --profile PROFILE --since 7d` discovers participant-visible central
|
|
75
|
+
roots. Add `--events FILE` and/or `--actions FILE` for a hybrid listing with local
|
|
76
|
+
task references and a `local`, `control-plane` or `both` source column. Omit the
|
|
77
|
+
profile for offline local listing. Central fetches are bounded and expose
|
|
78
|
+
continuation; forwarding remains best-effort. Local files are never uploaded.
|
|
74
79
|
|
|
75
80
|
## Guided setup using `aac init`
|
|
76
81
|
|
|
@@ -381,7 +386,7 @@ issues them for those names or checks supplied certificates against them; renewa
|
|
|
381
386
|
keeps those names. Applying a different name set requires the installed certificate
|
|
382
387
|
to cover it already: supplied issuers can issue a certificate covering both sets
|
|
383
388
|
before the change. A CLI-issued agent needing new names must be created under a
|
|
384
|
-
new name with the intended configuration.
|
|
389
|
+
new name with the intended configuration.
|
|
385
390
|
|
|
386
391
|
Central forwarding retains the local sink and sends only permitted metadata.
|
|
387
392
|
It is asynchronous and best-effort; a local event does not prove central delivery.
|
|
@@ -973,6 +978,7 @@ Deactivation is terminal, and a deactivated SPIFFE id stays reserved.
|
|
|
973
978
|
```bash
|
|
974
979
|
aac trust-anchor list --profile prod --output table # every published key, every lifecycle state
|
|
975
980
|
aac trust-anchor describe --profile prod --kid starter-root-v1
|
|
981
|
+
aac chain list --profile prod --since 7d --output table
|
|
976
982
|
aac chain show --profile prod \
|
|
977
983
|
--token-id c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9 --output table
|
|
978
984
|
aac chain show --profile prod \
|
|
@@ -984,6 +990,18 @@ aac chain show --profile prod \
|
|
|
984
990
|
chain to any tenant that took part in it. The timeline carries metadata
|
|
985
991
|
only; the business content of each step stays in your own audit stream.
|
|
986
992
|
|
|
993
|
+
`chain list` discovers roots using central observations for your tenant. The
|
|
994
|
+
default is the last 24 hours; use `--since 7d` or timezone-qualified
|
|
995
|
+
`--from 2026-09-01T00:00Z --to 2026-09-08T00:00Z` for a different interval
|
|
996
|
+
(maximum 31 days). It reads no local files or business labels. JSON includes
|
|
997
|
+
the resolved interval and `next_page_token`; pass that token with
|
|
998
|
+
`--page-token` to continue the same query. Each invocation fetches one page
|
|
999
|
+
(default 50 rows, maximum 200). Tokens expire 15 minutes after the first page.
|
|
1000
|
+
Central evidence may be incomplete and pages are live: late forwarding can
|
|
1001
|
+
change summaries or add roots, including roots behind the current page.
|
|
1002
|
+
Repeat the original interval to refresh. A returned `root_token_id` works
|
|
1003
|
+
with `chain show --token-id` and `aeg render --root-token-id`.
|
|
1004
|
+
|
|
987
1005
|
## Getting help
|
|
988
1006
|
|
|
989
1007
|
`aac --help` lists the command groups and `aac <command> --help` shows every
|
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: aac-cli
|
|
3
|
-
Version: 0.2.4
|
|
4
|
-
Summary: The aac platform CLI — headless operator interface to the AAC control plane (tenant registration, chain audit).
|
|
5
|
-
Author: Agent Authority Cloud Project
|
|
6
|
-
License-Expression: Apache-2.0
|
|
7
|
-
Project-URL: Documentation, https://cascadeauth.github.io/aac-starter-guide/aeg.html
|
|
8
|
-
Requires-Python: >=3.10
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
License-File: NOTICE
|
|
12
|
-
License-File: THIRD_PARTY_NOTICES.md
|
|
13
|
-
Requires-Dist: httpx>=0.28
|
|
14
|
-
Requires-Dist: cryptography>=42.0
|
|
15
|
-
Requires-Dist: PyYAML>=6.0
|
|
16
|
-
Requires-Dist: pyvis<0.4,>=0.3.2
|
|
17
|
-
Requires-Dist: jsonschema>=4.18
|
|
18
|
-
Provides-Extra: test
|
|
19
|
-
Requires-Dist: pytest>=8.0; extra == "test"
|
|
20
|
-
Requires-Dist: pytest-httpx>=0.30; extra == "test"
|
|
21
|
-
Dynamic: license-file
|
|
22
|
-
|
|
23
1
|
# aac-cli — the `aac` command
|
|
24
2
|
|
|
25
3
|
`aac` is the command-line tool for tenants of Agent Authority Cloud (AAC).
|
|
@@ -59,7 +37,7 @@ pip install aac-cli
|
|
|
59
37
|
aac --version
|
|
60
38
|
```
|
|
61
39
|
|
|
62
|
-
Python 3.
|
|
40
|
+
Python 3.11 or newer is required.
|
|
63
41
|
|
|
64
42
|
> **Mind the name.** `pip install aac` installs an unrelated project that
|
|
65
43
|
> happens to share the acronym. The AAC package is **`aac-cli`**; the
|
|
@@ -69,8 +47,13 @@ Python 3.10 or newer is required.
|
|
|
69
47
|
|
|
70
48
|
Installing or upgrading `aac-cli` supplies both `aac` and `aeg`.
|
|
71
49
|
Use the [AEG guide and command reference](https://cascadeauth.github.io/aac-starter-guide/aeg.html)
|
|
72
|
-
for
|
|
73
|
-
|
|
50
|
+
for listing local and central observations, rendering evidence, and selecting a profile.
|
|
51
|
+
|
|
52
|
+
`aeg list --profile PROFILE --since 7d` discovers participant-visible central
|
|
53
|
+
roots. Add `--events FILE` and/or `--actions FILE` for a hybrid listing with local
|
|
54
|
+
task references and a `local`, `control-plane` or `both` source column. Omit the
|
|
55
|
+
profile for offline local listing. Central fetches are bounded and expose
|
|
56
|
+
continuation; forwarding remains best-effort. Local files are never uploaded.
|
|
74
57
|
|
|
75
58
|
## Guided setup using `aac init`
|
|
76
59
|
|
|
@@ -381,7 +364,7 @@ issues them for those names or checks supplied certificates against them; renewa
|
|
|
381
364
|
keeps those names. Applying a different name set requires the installed certificate
|
|
382
365
|
to cover it already: supplied issuers can issue a certificate covering both sets
|
|
383
366
|
before the change. A CLI-issued agent needing new names must be created under a
|
|
384
|
-
new name with the intended configuration.
|
|
367
|
+
new name with the intended configuration.
|
|
385
368
|
|
|
386
369
|
Central forwarding retains the local sink and sends only permitted metadata.
|
|
387
370
|
It is asynchronous and best-effort; a local event does not prove central delivery.
|
|
@@ -973,6 +956,7 @@ Deactivation is terminal, and a deactivated SPIFFE id stays reserved.
|
|
|
973
956
|
```bash
|
|
974
957
|
aac trust-anchor list --profile prod --output table # every published key, every lifecycle state
|
|
975
958
|
aac trust-anchor describe --profile prod --kid starter-root-v1
|
|
959
|
+
aac chain list --profile prod --since 7d --output table
|
|
976
960
|
aac chain show --profile prod \
|
|
977
961
|
--token-id c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9 --output table
|
|
978
962
|
aac chain show --profile prod \
|
|
@@ -984,6 +968,18 @@ aac chain show --profile prod \
|
|
|
984
968
|
chain to any tenant that took part in it. The timeline carries metadata
|
|
985
969
|
only; the business content of each step stays in your own audit stream.
|
|
986
970
|
|
|
971
|
+
`chain list` discovers roots using central observations for your tenant. The
|
|
972
|
+
default is the last 24 hours; use `--since 7d` or timezone-qualified
|
|
973
|
+
`--from 2026-09-01T00:00Z --to 2026-09-08T00:00Z` for a different interval
|
|
974
|
+
(maximum 31 days). It reads no local files or business labels. JSON includes
|
|
975
|
+
the resolved interval and `next_page_token`; pass that token with
|
|
976
|
+
`--page-token` to continue the same query. Each invocation fetches one page
|
|
977
|
+
(default 50 rows, maximum 200). Tokens expire 15 minutes after the first page.
|
|
978
|
+
Central evidence may be incomplete and pages are live: late forwarding can
|
|
979
|
+
change summaries or add roots, including roots behind the current page.
|
|
980
|
+
Repeat the original interval to refresh. A returned `root_token_id` works
|
|
981
|
+
with `chain show --token-id` and `aeg render --root-token-id`.
|
|
982
|
+
|
|
987
983
|
## Getting help
|
|
988
984
|
|
|
989
985
|
`aac --help` lists the command groups and `aac <command> --help` shows every
|
|
@@ -39,7 +39,7 @@ def parser(
|
|
|
39
39
|
commands = result.add_subparsers(dest="command", required=True)
|
|
40
40
|
render = commands.add_parser("render", help="Write a self-contained interactive HTML graph.")
|
|
41
41
|
listing = commands.add_parser(
|
|
42
|
-
"list", help="List
|
|
42
|
+
"list", help="List roots from local files, a control-plane profile, or both."
|
|
43
43
|
)
|
|
44
44
|
for command in (render, listing):
|
|
45
45
|
command.add_argument(
|
|
@@ -72,9 +72,18 @@ def parser(
|
|
|
72
72
|
selector.add_argument("--root-token-id", metavar="ID")
|
|
73
73
|
selector.add_argument("--mint-response", type=Path, metavar="FILE")
|
|
74
74
|
render.add_argument("--output", type=Path, required=True, metavar="FILE")
|
|
75
|
-
listing.add_argument(
|
|
75
|
+
listing.add_argument(
|
|
76
|
+
"--profile", help="Query this AAC CLI profile; with files, join by root ID."
|
|
77
|
+
)
|
|
78
|
+
listing.add_argument("--task-ref", help="Exact local task reference; requires local files.")
|
|
79
|
+
listing.add_argument("--limit", type=int, help="Central page size, 1..200 (default 50).")
|
|
80
|
+
listing.add_argument("--max-pages", type=int, help="Central fetch bound, 1..20 (default 1).")
|
|
81
|
+
listing.add_argument("--page-token", help="Continue central enumeration; omit --since.")
|
|
76
82
|
time_start = listing.add_mutually_exclusive_group()
|
|
77
|
-
time_start.add_argument(
|
|
83
|
+
time_start.add_argument(
|
|
84
|
+
"--since",
|
|
85
|
+
help="Look back, e.g. 24h or 7d; online defaults to 24h, maximum 31d. Local also accepts m.",
|
|
86
|
+
)
|
|
78
87
|
time_start.add_argument(
|
|
79
88
|
"--from", dest="from_time", help="Inclusive ISO 8601 observation time with timezone."
|
|
80
89
|
)
|
|
@@ -185,11 +194,21 @@ def _date(value: str) -> datetime:
|
|
|
185
194
|
raise InputError(f"invalid time {value!r}; use ISO 8601 with timezone") from exc
|
|
186
195
|
|
|
187
196
|
|
|
188
|
-
def _list(args, events) -> int:
|
|
197
|
+
def _list(args, events, list_query) -> int:
|
|
198
|
+
if args.profile is not None:
|
|
199
|
+
if not args.profile.strip():
|
|
200
|
+
raise InputError("--profile must name an existing AAC CLI profile")
|
|
201
|
+
if args.task_ref is not None and not (args.events or args.actions):
|
|
202
|
+
raise InputError(
|
|
203
|
+
"--task-ref requires local files; the control plane stores no task references"
|
|
204
|
+
)
|
|
205
|
+
if list_query is None:
|
|
206
|
+
raise InputError("online listing requires the aeg command supplied by aac-cli")
|
|
207
|
+
return list_query(args, events)
|
|
189
208
|
if not (args.events or args.actions):
|
|
190
|
-
raise InputError(
|
|
191
|
-
|
|
192
|
-
)
|
|
209
|
+
raise InputError("supply --events, --actions and/or --profile to list roots")
|
|
210
|
+
if any(value is not None for value in (args.limit, args.max_pages, args.page_token)):
|
|
211
|
+
raise InputError("--limit, --max-pages and --page-token require --profile")
|
|
193
212
|
start = _date(args.from_time) if args.from_time else None
|
|
194
213
|
stop = _date(args.to_time) if args.to_time else None
|
|
195
214
|
if args.since:
|
|
@@ -204,6 +223,13 @@ def _list(args, events) -> int:
|
|
|
204
223
|
raise InputError("--since duration is outside supported range") from exc
|
|
205
224
|
if start and stop and start > stop:
|
|
206
225
|
raise InputError("--from/--since must precede --to")
|
|
226
|
+
rows, diagnostics = local_rows(events, args.task_ref, start, stop)
|
|
227
|
+
print_listing(args.output, rows, diagnostics)
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def local_rows(events, task_ref, start, stop):
|
|
232
|
+
"""Select local activity, retaining older evidence for token/root correlation."""
|
|
207
233
|
roots, mapping = roots_and_mapping(events)
|
|
208
234
|
diagnostics = [
|
|
209
235
|
f"{e.raw_event.get('_source', e.source)}: unmatched action {e.token_id}; supply sidecar root mapping"
|
|
@@ -230,7 +256,7 @@ def _list(args, events) -> int:
|
|
|
230
256
|
or (e.raw_event.get("action_payload") or {}).get("task_ref")
|
|
231
257
|
}
|
|
232
258
|
)
|
|
233
|
-
if
|
|
259
|
+
if task_ref is not None and task_ref not in task_refs:
|
|
234
260
|
continue
|
|
235
261
|
observed = [
|
|
236
262
|
e
|
|
@@ -240,36 +266,68 @@ def _list(args, events) -> int:
|
|
|
240
266
|
]
|
|
241
267
|
if not observed:
|
|
242
268
|
continue
|
|
243
|
-
times = sorted(e.timestamp_iso for e in observed)
|
|
269
|
+
times = sorted(_date(e.timestamp_iso) for e in observed)
|
|
244
270
|
rows.append(
|
|
245
271
|
{
|
|
246
272
|
"root_token_id": root,
|
|
247
273
|
"task_refs": task_refs,
|
|
248
|
-
"first_observed": times[0],
|
|
249
|
-
"last_observed": times[-1],
|
|
274
|
+
"first_observed": times[0].isoformat(),
|
|
275
|
+
"last_observed": times[-1].isoformat(),
|
|
250
276
|
"sources": "local",
|
|
251
277
|
"evidence_sources": sorted(
|
|
252
278
|
{str(e.raw_event.get("_source", e.source)) for e in observed}
|
|
253
279
|
),
|
|
254
280
|
}
|
|
255
281
|
)
|
|
256
|
-
|
|
257
|
-
|
|
282
|
+
return rows, diagnostics
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def print_listing(output, rows, diagnostics, coverage=None):
|
|
286
|
+
if output == "json":
|
|
287
|
+
body = {"chains": rows, "diagnostics": diagnostics}
|
|
288
|
+
if coverage is not None:
|
|
289
|
+
body.update(coverage)
|
|
290
|
+
print(json.dumps(body, indent=2))
|
|
258
291
|
else:
|
|
292
|
+
if coverage is not None:
|
|
293
|
+
central = coverage["central"]
|
|
294
|
+
print(
|
|
295
|
+
"Central observations may be incomplete (best-effort forwarding; live pagination)."
|
|
296
|
+
)
|
|
297
|
+
if coverage["from"] is None or coverage["to"] is None:
|
|
298
|
+
print("Activity interval: unknown (continuation failed)")
|
|
299
|
+
else:
|
|
300
|
+
print(f"Activity interval: [{coverage['from']}, {coverage['to']})")
|
|
301
|
+
print(f"Central fetch: {central['status']}; {central['pages_fetched']} page(s).")
|
|
302
|
+
print("Sources describe this query; local does not mean never received centrally.")
|
|
303
|
+
print(
|
|
304
|
+
"Times and hop information are observed, not execution or business-completion totals."
|
|
305
|
+
)
|
|
259
306
|
print("ROOT TOKEN ID | TASK REFERENCES | FIRST OBSERVED (UTC) | SOURCES")
|
|
260
307
|
for row in rows:
|
|
261
308
|
print(
|
|
262
|
-
f"{row['root_token_id']} | {', '.join(row['task_refs']) or '(unknown)'} | {row['first_observed']} |
|
|
309
|
+
f"{row['root_token_id']} | {', '.join(row['task_refs']) or '(unknown)'} | {row['first_observed']} | {row['sources']}"
|
|
263
310
|
)
|
|
311
|
+
if coverage is not None and coverage["central"]["next_page_token"]:
|
|
312
|
+
cursor = coverage["central"]["next_page_token"]
|
|
313
|
+
if coverage["central"]["status"] == "failed":
|
|
314
|
+
print(
|
|
315
|
+
f"Retry with --page-token {cursor} only if the failure was transient; "
|
|
316
|
+
"otherwise restart the original query."
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
print(
|
|
320
|
+
f"Continue with --page-token {cursor} (same profile/files/filter; omit --since)."
|
|
321
|
+
)
|
|
264
322
|
for diagnostic in diagnostics:
|
|
265
323
|
print(diagnostic, file=sys.stderr)
|
|
266
|
-
return 0
|
|
267
324
|
|
|
268
325
|
|
|
269
326
|
def main(
|
|
270
327
|
argv: list[str] | None = None,
|
|
271
328
|
*,
|
|
272
329
|
query: Query | None = None,
|
|
330
|
+
list_query: Callable[[argparse.Namespace, list], int] | None = None,
|
|
273
331
|
prog: str = "aeg",
|
|
274
332
|
version_text: str = "shared AAC renderer",
|
|
275
333
|
) -> int:
|
|
@@ -280,7 +338,7 @@ def main(
|
|
|
280
338
|
return (
|
|
281
339
|
_render(args, events, query or _query)
|
|
282
340
|
if args.command == "render"
|
|
283
|
-
else _list(args, events)
|
|
341
|
+
else _list(args, events, list_query)
|
|
284
342
|
)
|
|
285
343
|
except (InputError, ValueError, OSError) as exc:
|
|
286
344
|
for diagnostic in unjoinable_diagnostics(events):
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"content_sha256": "
|
|
2
|
+
"content_sha256": "f0477c0f8e73bffe5e111842d8aca3f5745345acb8cb58bed911deba7d826798",
|
|
3
3
|
"files": {
|
|
4
4
|
"__init__.py": "80d80a1ba35353d271cddf02532801c60b57f5ec06fb268ae823747742ce8979",
|
|
5
|
-
"cli.py": "
|
|
5
|
+
"cli.py": "8fa9605246ba0ddbd24e6034b9582049fac11df139d47ee9e2ee0f322ff53c4c",
|
|
6
6
|
"data/action-taken-v1.json": "d863343afe7f314646a73cf063480a7e650f93857d2ed5ec3a7b1f9838671fdf",
|
|
7
7
|
"data/predicates-v1.json": "35e83a29feada10e8cfbef7009245146a9717d4f4fd58d74124e0ec91ae922c8",
|
|
8
8
|
"from_sidecar.py": "6ac023f6925a13e94e0fee762656a02ca98981f2add194b052e9014ca48e2219",
|
|
@@ -21,6 +21,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
21
21
|
return 0
|
|
22
22
|
from ._aeg import cli
|
|
23
23
|
from ._aeg.inputs import InputError, read_trace
|
|
24
|
+
from .aeg_list import list_chains
|
|
24
25
|
|
|
25
26
|
def query(profile: str, root: str):
|
|
26
27
|
from .trace_query import query_trace
|
|
@@ -30,7 +31,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
30
31
|
except (ValueError, TypeError) as exc:
|
|
31
32
|
raise InputError(str(exc)) from exc
|
|
32
33
|
|
|
33
|
-
return cli.main(
|
|
34
|
+
return cli.main(
|
|
35
|
+
argv, query=query, list_query=list_chains, prog="aeg", version_text=version_text()
|
|
36
|
+
)
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
def entrypoint() -> None:
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Bounded AEG discovery over the public chain-list JSON contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
from ._aeg.cli import _date, local_rows, print_listing
|
|
8
|
+
from ._aeg.inputs import InputError, token_id
|
|
9
|
+
from .chain_list import query_params
|
|
10
|
+
from .trace_query import query_chains
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _observed_time(value) -> str:
|
|
14
|
+
if type(value) is not int:
|
|
15
|
+
raise ValueError("expected integer observation time")
|
|
16
|
+
return datetime.fromtimestamp(value, timezone.utc).isoformat()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _page(body, params, previous_root):
|
|
20
|
+
"""Validate the complete page before adding any of its rows to the result."""
|
|
21
|
+
try:
|
|
22
|
+
if (
|
|
23
|
+
type(body["schema_version"]) is not int
|
|
24
|
+
or body["schema_version"] != 1
|
|
25
|
+
or body["pagination"] != "live"
|
|
26
|
+
or body["central_evidence"] != "best_effort"
|
|
27
|
+
):
|
|
28
|
+
raise ValueError("expected chain-list JSON v1 with live/best-effort evidence")
|
|
29
|
+
if not isinstance(body["from"], str) or not isinstance(body["to"], str):
|
|
30
|
+
raise ValueError("expected resolved central interval")
|
|
31
|
+
window = query_params(
|
|
32
|
+
since=None,
|
|
33
|
+
start=body["from"],
|
|
34
|
+
end=body["to"],
|
|
35
|
+
limit=body["limit"],
|
|
36
|
+
page_token=None,
|
|
37
|
+
)
|
|
38
|
+
if type(body["limit"]) is not int:
|
|
39
|
+
raise ValueError("expected integer page limit")
|
|
40
|
+
if any(window[key] != value for key, value in params.items() if key != "cursor"):
|
|
41
|
+
raise ValueError("central response changed the requested interval or limit")
|
|
42
|
+
more, cursor = body["has_more"], body["next_page_token"]
|
|
43
|
+
if (
|
|
44
|
+
type(more) is not bool
|
|
45
|
+
or (more and (not isinstance(cursor, str) or not 1 <= len(cursor) <= 2048))
|
|
46
|
+
or (not more and cursor is not None)
|
|
47
|
+
):
|
|
48
|
+
raise ValueError("invalid central continuation")
|
|
49
|
+
if not isinstance(body["chains"], list) or len(body["chains"]) > body["limit"]:
|
|
50
|
+
raise ValueError("invalid central page rows")
|
|
51
|
+
if more and not body["chains"]:
|
|
52
|
+
raise ValueError("empty central page has a continuation")
|
|
53
|
+
rows = []
|
|
54
|
+
for item in body["chains"]:
|
|
55
|
+
root = token_id(item["root_token_id"], "central root")
|
|
56
|
+
if root <= previous_root:
|
|
57
|
+
raise ValueError("central roots must advance in ascending order")
|
|
58
|
+
previous_root = root
|
|
59
|
+
first = _observed_time(item["first_event_unix_seconds"])
|
|
60
|
+
last = _observed_time(item["last_event_unix_seconds"])
|
|
61
|
+
if first > last:
|
|
62
|
+
raise ValueError("reversed central observation times")
|
|
63
|
+
for key in ("participant_tenant_ids", "event_categories", "outcomes"):
|
|
64
|
+
if not isinstance(item[key], list) or not all(
|
|
65
|
+
isinstance(v, str) for v in item[key]
|
|
66
|
+
):
|
|
67
|
+
raise ValueError(f"invalid central {key}")
|
|
68
|
+
for key in ("observed_token_count", "max_observed_hop"):
|
|
69
|
+
if type(item[key]) is not int or item[key] < 0:
|
|
70
|
+
raise ValueError(f"invalid central {key}")
|
|
71
|
+
rows.append(
|
|
72
|
+
{
|
|
73
|
+
"root_token_id": root,
|
|
74
|
+
"task_refs": [],
|
|
75
|
+
"first_observed": first,
|
|
76
|
+
"last_observed": last,
|
|
77
|
+
"sources": "control-plane",
|
|
78
|
+
"evidence_sources": ["control-plane"],
|
|
79
|
+
"central_observations": item,
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
return window, rows, more, cursor, previous_root
|
|
83
|
+
except (KeyError, TypeError, ValueError, OverflowError, OSError) as exc:
|
|
84
|
+
raise InputError(f"incompatible chain-list JSON: {exc}") from exc
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def list_chains(args, events) -> int:
|
|
88
|
+
max_pages = args.max_pages if args.max_pages is not None else 1
|
|
89
|
+
if not 1 <= max_pages <= 20:
|
|
90
|
+
raise InputError("--max-pages must be between 1 and 20")
|
|
91
|
+
# Resolve relative time once before any I/O; reuse it for local input and
|
|
92
|
+
# every page. A bare cursor obtains its bound interval from the first page.
|
|
93
|
+
params = query_params(
|
|
94
|
+
since=args.since,
|
|
95
|
+
start=args.from_time,
|
|
96
|
+
end=args.to_time,
|
|
97
|
+
limit=args.limit,
|
|
98
|
+
page_token=args.page_token,
|
|
99
|
+
)
|
|
100
|
+
window = dict(params)
|
|
101
|
+
central_rows = []
|
|
102
|
+
diagnostics = []
|
|
103
|
+
pages = 0
|
|
104
|
+
more = None
|
|
105
|
+
cursor = args.page_token
|
|
106
|
+
status = "failed"
|
|
107
|
+
previous_root = ""
|
|
108
|
+
seen_cursors = {cursor} if cursor is not None else set()
|
|
109
|
+
for _ in range(max_pages):
|
|
110
|
+
try:
|
|
111
|
+
body = query_chains(args.profile, params)
|
|
112
|
+
next_window, rows, next_more, next_cursor, last_root = _page(
|
|
113
|
+
body, params, previous_root
|
|
114
|
+
)
|
|
115
|
+
if next_cursor is not None and next_cursor in seen_cursors:
|
|
116
|
+
raise InputError("incompatible chain-list JSON: repeated continuation")
|
|
117
|
+
except (ValueError, TypeError) as exc:
|
|
118
|
+
diagnostics.append(str(exc))
|
|
119
|
+
status = "failed"
|
|
120
|
+
break
|
|
121
|
+
window = next_window
|
|
122
|
+
central_rows.extend(rows)
|
|
123
|
+
pages += 1
|
|
124
|
+
more, cursor, previous_root = next_more, next_cursor, last_root
|
|
125
|
+
status = "more" if more else "exhausted"
|
|
126
|
+
if not more:
|
|
127
|
+
break
|
|
128
|
+
seen_cursors.add(cursor)
|
|
129
|
+
params = {**window, "cursor": cursor}
|
|
130
|
+
|
|
131
|
+
if "from" in window and "to" in window:
|
|
132
|
+
rows, local_diagnostics = local_rows(
|
|
133
|
+
events, args.task_ref, _date(window["from"]), _date(window["to"])
|
|
134
|
+
)
|
|
135
|
+
diagnostics.extend(local_diagnostics)
|
|
136
|
+
else:
|
|
137
|
+
rows = []
|
|
138
|
+
diagnostics.append(
|
|
139
|
+
"Continuation interval unavailable; local rows cannot be selected. Retry the original interval."
|
|
140
|
+
)
|
|
141
|
+
joined = {row["root_token_id"]: row for row in rows}
|
|
142
|
+
for central in central_rows:
|
|
143
|
+
root = central["root_token_id"]
|
|
144
|
+
local = joined.get(root)
|
|
145
|
+
if local is not None:
|
|
146
|
+
local.update(
|
|
147
|
+
{
|
|
148
|
+
"sources": "both",
|
|
149
|
+
"first_observed": min(local["first_observed"], central["first_observed"]),
|
|
150
|
+
"last_observed": max(local["last_observed"], central["last_observed"]),
|
|
151
|
+
"evidence_sources": sorted([*local["evidence_sources"], "control-plane"]),
|
|
152
|
+
"central_observations": central["central_observations"],
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
elif args.task_ref is None:
|
|
156
|
+
joined[root] = central
|
|
157
|
+
coverage = {
|
|
158
|
+
"schema_version": 1,
|
|
159
|
+
"mode": "hybrid" if args.events or args.actions else "control-plane",
|
|
160
|
+
"from": window.get("from"),
|
|
161
|
+
"to": window.get("to"),
|
|
162
|
+
"central": {
|
|
163
|
+
"status": status,
|
|
164
|
+
"pages_fetched": pages,
|
|
165
|
+
"limit": int(window["limit"]) if "limit" in window else None,
|
|
166
|
+
"has_more": more,
|
|
167
|
+
"next_page_token": cursor,
|
|
168
|
+
"pagination": "live",
|
|
169
|
+
"evidence": "best_effort",
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
print_listing(args.output, [joined[root] for root in sorted(joined)], diagnostics, coverage)
|
|
173
|
+
return 4 if status == "failed" else 0
|
|
@@ -29,9 +29,6 @@ from aac_cli.material_cases import MATERIAL_ISSUED_HERE, MATERIAL_SUPPLIED
|
|
|
29
29
|
|
|
30
30
|
TENANTS_DIRECTORY = "tenants"
|
|
31
31
|
AGENTS_DIRECTORY = "agents"
|
|
32
|
-
# 0.1.x kept the same material under `workspaces/`. 0.2.0 is a clean break: an
|
|
33
|
-
# old folder is recognised only so the CLI can say so in one line (B270).
|
|
34
|
-
LEGACY_AGENTS_DIRECTORY = "workspaces"
|
|
35
32
|
|
|
36
33
|
# Where the container layout expects the mounted directories (matches the
|
|
37
34
|
# public starter guide's Docker recipe so B240 can reuse its mounts).
|
|
@@ -249,21 +246,6 @@ def agent_paths(name: str, home: Path | None = None) -> AgentPaths:
|
|
|
249
246
|
return AgentPaths(name=name, root=(home or cli_home()) / AGENTS_DIRECTORY / name)
|
|
250
247
|
|
|
251
248
|
|
|
252
|
-
def legacy_agent_directory(name: str, home: Path | None = None) -> Path:
|
|
253
|
-
"""Where 0.1.x kept this name's material. Read only to refuse it (B270)."""
|
|
254
|
-
return (home or cli_home()) / LEGACY_AGENTS_DIRECTORY / name
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def refuse_legacy_directory(name: str, home: Path | None = None) -> None:
|
|
258
|
-
"""One line, no migration: development material is disposable by design."""
|
|
259
|
-
legacy = legacy_agent_directory(name, home)
|
|
260
|
-
if legacy.exists():
|
|
261
|
-
raise CliConfigError(
|
|
262
|
-
f"{legacy} is from an older CLI and is not migrated; create a new agent with "
|
|
263
|
-
f"`aac init --agent {name}` after deleting it, or choose another name"
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
|
|
267
249
|
def tenant_paths(tenant_id: str, home: Path | None = None) -> TenantPaths:
|
|
268
250
|
if not CANONICAL_TENANT_ID_PATTERN.fullmatch(tenant_id):
|
|
269
251
|
raise CliConfigError(f"tenant id {tenant_id!r} is not canonical; refusing to derive a path")
|
|
@@ -153,7 +153,7 @@ def read_record(path: Path) -> AgentRecord:
|
|
|
153
153
|
raise CliConfigError(
|
|
154
154
|
f"agent record {path} has an unsupported schema_version; "
|
|
155
155
|
f"this CLI understands version {RECORD_SCHEMA_VERSION}. Create a fresh agent "
|
|
156
|
-
"under a different name
|
|
156
|
+
"under a different name. The existing record is unchanged."
|
|
157
157
|
)
|
|
158
158
|
try:
|
|
159
159
|
record = AgentRecord(**document)
|