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.
Files changed (54) hide show
  1. {aac_cli-0.2.4 → aac_cli-0.2.6}/PKG-INFO +24 -6
  2. aac_cli-0.2.4/aac_cli.egg-info/PKG-INFO → aac_cli-0.2.6/README.md +22 -26
  3. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/__init__.py +1 -1
  4. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/cli.py +74 -16
  5. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/data/renderer-source.json +2 -2
  6. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/aeg_cli.py +4 -1
  7. aac_cli-0.2.6/aac_cli/aeg_list.py +173 -0
  8. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/agent_layout.py +0 -18
  9. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/agent_record.py +1 -1
  10. aac_cli-0.2.6/aac_cli/chain_list.py +100 -0
  11. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/cli.py +59 -8
  12. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/config.py +12 -85
  13. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/init_cli.py +0 -3
  14. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/registration_state.py +9 -48
  15. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/trace_query.py +20 -5
  16. aac_cli-0.2.4/README.md → aac_cli-0.2.6/aac_cli.egg-info/PKG-INFO +44 -4
  17. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli.egg-info/SOURCES.txt +2 -0
  18. {aac_cli-0.2.4 → aac_cli-0.2.6}/pyproject.toml +4 -4
  19. {aac_cli-0.2.4 → aac_cli-0.2.6}/LICENSE +0 -0
  20. {aac_cli-0.2.4 → aac_cli-0.2.6}/MANIFEST.in +0 -0
  21. {aac_cli-0.2.4 → aac_cli-0.2.6}/NOTICE +0 -0
  22. {aac_cli-0.2.4 → aac_cli-0.2.6}/THIRD_PARTY_NOTICES.md +0 -0
  23. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/__main__.py +0 -0
  24. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/__init__.py +0 -0
  25. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/data/action-taken-v1.json +0 -0
  26. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/data/predicates-v1.json +0 -0
  27. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/from_sidecar.py +0 -0
  28. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/inputs.py +0 -0
  29. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/model.py +0 -0
  30. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/narrative.py +0 -0
  31. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/schemas.py +0 -0
  32. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/temporal.py +0 -0
  33. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/timeline_source.py +0 -0
  34. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/_aeg/visualizer.py +0 -0
  35. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/admin_key_pem.py +0 -0
  36. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/agent_cli.py +0 -0
  37. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/agent_config.py +0 -0
  38. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/agent_health.py +0 -0
  39. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/api_key_rotation_state.py +0 -0
  40. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/config_render.py +0 -0
  41. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/dev_material.py +0 -0
  42. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/idp_recovery.py +0 -0
  43. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/material_cases.py +0 -0
  44. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/profiles.py +0 -0
  45. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/reference.py +0 -0
  46. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/secure_files.py +0 -0
  47. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/sso_login.py +0 -0
  48. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli/supplied_material.py +0 -0
  49. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli.egg-info/dependency_links.txt +0 -0
  50. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli.egg-info/requires.txt +0 -0
  51. {aac_cli-0.2.4 → aac_cli-0.2.6}/aac_cli.egg-info/top_level.txt +0 -0
  52. {aac_cli-0.2.4 → aac_cli-0.2.6}/launchers/aac +0 -0
  53. {aac_cli-0.2.4 → aac_cli-0.2.6}/launchers/aeg +0 -0
  54. {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.4
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.10
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.10 or newer is required.
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 rendering local evidence, selecting a profile, and migrating an older
73
- standalone AEG installation.
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. No old agent-record format is migrated.
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.10 or newer is required.
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 rendering local evidence, selecting a profile, and migrating an older
73
- standalone AEG installation.
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. No old agent-record format is migrated.
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
@@ -10,4 +10,4 @@ entrypoint (`aac_cli.cli:entrypoint`).
10
10
  # test in bin/tests/test_cli_version_parity.py. A literal keeps the
11
11
  # shipped code free of import-time metadata lookups (and their
12
12
  # not-installed failure mode).
13
- __version__ = "0.2.4"
13
+ __version__ = "0.2.6"
@@ -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 candidate roots from explicitly supplied local files."
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("--task-ref", help="Exact local task reference.")
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("--since", help="Look back from now, e.g. 24h or 7d.")
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
- "local list requires --events and/or --actions; profile and hybrid listing belong to a later release"
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 args.task_ref and args.task_ref not in task_refs:
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
- if args.output == "json":
257
- print(json.dumps({"chains": rows, "diagnostics": diagnostics}, indent=2))
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']} | local"
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": "e444d19d48410200f040c05bba84d3e2426ecb5ba350430203cad5df74f4104d",
2
+ "content_sha256": "f0477c0f8e73bffe5e111842d8aca3f5745345acb8cb58bed911deba7d826798",
3
3
  "files": {
4
4
  "__init__.py": "80d80a1ba35353d271cddf02532801c60b57f5ec06fb268ae823747742ce8979",
5
- "cli.py": "b413ca8fffa51f0874deeaad47c16a2bed4ce0ec769074dd566bba3e5480566b",
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(argv, query=query, prog="aeg", version_text=version_text())
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; no old record is migrated or removed."
156
+ "under a different name. The existing record is unchanged."
157
157
  )
158
158
  try:
159
159
  record = AgentRecord(**document)