aac-cli 0.2.5__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.5/aac_cli.egg-info → aac_cli-0.2.6}/PKG-INFO +9 -4
  2. {aac_cli-0.2.5 → aac_cli-0.2.6}/README.md +8 -3
  3. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/__init__.py +1 -1
  4. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/_aeg/cli.py +74 -16
  5. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/_aeg/data/renderer-source.json +2 -2
  6. {aac_cli-0.2.5 → 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.5 → aac_cli-0.2.6}/aac_cli/agent_layout.py +0 -18
  9. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/agent_record.py +1 -1
  10. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/cli.py +0 -7
  11. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/config.py +12 -85
  12. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/init_cli.py +0 -3
  13. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/registration_state.py +9 -48
  14. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/trace_query.py +20 -5
  15. {aac_cli-0.2.5 → aac_cli-0.2.6/aac_cli.egg-info}/PKG-INFO +9 -4
  16. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli.egg-info/SOURCES.txt +1 -0
  17. {aac_cli-0.2.5 → aac_cli-0.2.6}/pyproject.toml +1 -1
  18. {aac_cli-0.2.5 → aac_cli-0.2.6}/LICENSE +0 -0
  19. {aac_cli-0.2.5 → aac_cli-0.2.6}/MANIFEST.in +0 -0
  20. {aac_cli-0.2.5 → aac_cli-0.2.6}/NOTICE +0 -0
  21. {aac_cli-0.2.5 → aac_cli-0.2.6}/THIRD_PARTY_NOTICES.md +0 -0
  22. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/__main__.py +0 -0
  23. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/_aeg/__init__.py +0 -0
  24. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/_aeg/data/action-taken-v1.json +0 -0
  25. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/_aeg/data/predicates-v1.json +0 -0
  26. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/_aeg/from_sidecar.py +0 -0
  27. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/_aeg/inputs.py +0 -0
  28. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/_aeg/model.py +0 -0
  29. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/_aeg/narrative.py +0 -0
  30. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/_aeg/schemas.py +0 -0
  31. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/_aeg/temporal.py +0 -0
  32. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/_aeg/timeline_source.py +0 -0
  33. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/_aeg/visualizer.py +0 -0
  34. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/admin_key_pem.py +0 -0
  35. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/agent_cli.py +0 -0
  36. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/agent_config.py +0 -0
  37. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/agent_health.py +0 -0
  38. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/api_key_rotation_state.py +0 -0
  39. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/chain_list.py +0 -0
  40. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/config_render.py +0 -0
  41. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/dev_material.py +0 -0
  42. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/idp_recovery.py +0 -0
  43. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/material_cases.py +0 -0
  44. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/profiles.py +0 -0
  45. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/reference.py +0 -0
  46. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/secure_files.py +0 -0
  47. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/sso_login.py +0 -0
  48. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli/supplied_material.py +0 -0
  49. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli.egg-info/dependency_links.txt +0 -0
  50. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli.egg-info/requires.txt +0 -0
  51. {aac_cli-0.2.5 → aac_cli-0.2.6}/aac_cli.egg-info/top_level.txt +0 -0
  52. {aac_cli-0.2.5 → aac_cli-0.2.6}/launchers/aac +0 -0
  53. {aac_cli-0.2.5 → aac_cli-0.2.6}/launchers/aeg +0 -0
  54. {aac_cli-0.2.5 → aac_cli-0.2.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aac-cli
3
- Version: 0.2.5
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
@@ -69,8 +69,13 @@ Python 3.11 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.
@@ -47,8 +47,13 @@ Python 3.11 or newer is required.
47
47
 
48
48
  Installing or upgrading `aac-cli` supplies both `aac` and `aeg`.
49
49
  Use the [AEG guide and command reference](https://cascadeauth.github.io/aac-starter-guide/aeg.html)
50
- for rendering local evidence, selecting a profile, and migrating an older
51
- 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.
52
57
 
53
58
  ## Guided setup using `aac init`
54
59
 
@@ -359,7 +364,7 @@ issues them for those names or checks supplied certificates against them; renewa
359
364
  keeps those names. Applying a different name set requires the installed certificate
360
365
  to cover it already: supplied issuers can issue a certificate covering both sets
361
366
  before the change. A CLI-issued agent needing new names must be created under a
362
- new name with the intended configuration. No old agent-record format is migrated.
367
+ new name with the intended configuration.
363
368
 
364
369
  Central forwarding retains the local sink and sends only permitted metadata.
365
370
  It is asynchronous and best-effort; a local event does not prove central delivery.
@@ -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.5"
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)
@@ -146,7 +146,6 @@ from aac_cli.registration_state import (
146
146
  profile_registration_lock,
147
147
  read_registration_state,
148
148
  registration_state_path,
149
- upgrade_registration_state,
150
149
  validate_tenant_domain,
151
150
  write_registration_state,
152
151
  )
@@ -666,12 +665,6 @@ def _cmd_tenant_register_locked(args: argparse.Namespace, profile: str) -> int:
666
665
  config = _resolve(args)
667
666
  normalized_admin_url = normalize_admin_url(config.admin_url)
668
667
  state = read_registration_state(profile, expected_admin_url=normalized_admin_url)
669
- if state is not None and state.schema_version == 1:
670
- # B121: v1 had no tenant_domains field. Upgrade only after the strict
671
- # v1 reader verified its original digest and normalized empty-domain
672
- # intent; every subsequent write is schema v2.
673
- state = replace(upgrade_registration_state(state), updated_at=_utc_now())
674
- write_registration_state(state)
675
668
  bound_tenant_id = read_profile_section(profile).get("tenant_id")
676
669
 
677
670
  if state is not None and state.state == "completed_credential_reissue_required":
@@ -16,9 +16,8 @@ with ``AAC_CLI_HOME`` — tests and multi-account operators):
16
16
 
17
17
  ``main`` is the RESERVED baseline profile (§3.3): always the final
18
18
  selection fallback, virtual (built-in defaults) until
19
- ``aac profile update main`` materializes it. The prototype-era
20
- ``[default]`` baseline migrates to ``[main]`` on first config touch
21
- (§8) — fail-closed when both exist with different values.
19
+ ``aac profile update main`` materializes it. Reading configuration
20
+ never changes the file or creates the CLI home (§8).
22
21
 
23
22
  TWO urls, not one base: the admin and data-plane surfaces genuinely
24
23
  live on different ports (Eng Spec §XVI) and 1C may split them into
@@ -59,7 +58,6 @@ import fcntl
59
58
  import json
60
59
  import os
61
60
  import re
62
- import sys
63
61
  import tempfile
64
62
  from collections.abc import Generator
65
63
  from contextlib import contextmanager
@@ -110,9 +108,6 @@ _DEFAULT_DATA_PLANE_URL = "http://127.0.0.1:9000"
110
108
  # selection fallback; virtual until materialized; create/delete refuse.
111
109
  RESERVED_BASELINE_PROFILE = "main"
112
110
 
113
- # The prototype-era baseline section name that §8 migrates to [main].
114
- _LEGACY_BASELINE_SECTION = "default"
115
-
116
111
  # Profile-name grammar, spec-exact (§5.3): 3–15 chars, lowercase
117
112
  # alphanumeric + single interior hyphens, at least one letter.
118
113
  PROFILE_NAME_PATTERN = re.compile(r"^(?=.{3,15}$)(?=.*[a-z])[a-z0-9]+(?:-[a-z0-9]+)*$")
@@ -260,8 +255,8 @@ def select_profile(profile_flag: str | None) -> ProfileSelection:
260
255
 
261
256
 
262
257
  # ---------------------------------------------------------------------------
263
- # Config file loading: parse → reject [DEFAULT] → migrate [default] →
264
- # validate names → guardrails. EVERY reader and writer goes through
258
+ # Config file loading: parse → reject [DEFAULT] → validate names →
259
+ # guardrails. EVERY reader and writer goes through
265
260
  # load_validated_config so no path can act on config the contract
266
261
  # rejects (Spec §5, §8).
267
262
  # ---------------------------------------------------------------------------
@@ -269,7 +264,7 @@ def select_profile(profile_flag: str | None) -> ProfileSelection:
269
264
 
270
265
  @contextmanager
271
266
  def _config_mutation_lock() -> Generator[None, None, None]:
272
- """Serialize each config parse/migrate or read-modify-replace window."""
267
+ """Serialize each config writer's read-modify-replace window."""
273
268
  home = cli_home()
274
269
  home.mkdir(parents=True, exist_ok=True)
275
270
  home.chmod(0o700)
@@ -329,61 +324,6 @@ def _parse_config_text(config_path: Path) -> configparser.ConfigParser:
329
324
  return parser
330
325
 
331
326
 
332
- def _migrate_legacy_default_section(
333
- parser: configparser.ConfigParser, config_path: Path
334
- ) -> configparser.ConfigParser:
335
- """§8: [default] → [main], atomically, on first config touch.
336
-
337
- Three cases: rename when only [default] exists; drop [default]
338
- when both exist with IDENTICAL values (report once); fail closed
339
- when both exist with different values — the CLI must not guess,
340
- merge, or discard either profile. The migration is a WRITE even on
341
- read-only commands; that is deliberate spec behavior (§8) so every
342
- PR 2 command observes the post-migration file.
343
- """
344
- # Guardrails already ran on the parsed file (review M1): §8 step 1
345
- # validates WITHOUT modification, so an invalid file is rejected
346
- # with exit 3 before this function ever rewrites it.
347
- has_legacy = parser.has_section(_LEGACY_BASELINE_SECTION)
348
- if not has_legacy:
349
- return parser
350
- has_main = parser.has_section(RESERVED_BASELINE_PROFILE)
351
- if has_main:
352
- legacy_items = dict(parser.items(_LEGACY_BASELINE_SECTION))
353
- main_items = dict(parser.items(RESERVED_BASELINE_PROFILE))
354
- if legacy_items != main_items:
355
- raise CliConfigError(
356
- f"config migration conflict in {config_path}: both "
357
- f"[default] and [main] exist with DIFFERENT values. AAC "
358
- f"will not guess which is authoritative. Fix by hand: "
359
- f"edit the file, keep the values you want under [main], "
360
- f"delete the [default] section, and re-run."
361
- )
362
- migrated = configparser.ConfigParser(interpolation=None)
363
- for section in parser.sections():
364
- if section == _LEGACY_BASELINE_SECTION:
365
- if has_main:
366
- continue # identical duplicate — [main] already carries it
367
- target = RESERVED_BASELINE_PROFILE
368
- else:
369
- target = section
370
- migrated.add_section(target)
371
- for key, value in parser.items(section):
372
- migrated[target][key] = value
373
- _write_parser_atomically(migrated, config_path)
374
- if has_main:
375
- print(
376
- f"config migration: removed [default] (identical to [main]) in {config_path}",
377
- file=sys.stderr,
378
- )
379
- else:
380
- print(
381
- f"config migration: renamed [default] to [main] in {config_path}",
382
- file=sys.stderr,
383
- )
384
- return migrated
385
-
386
-
387
327
  def _enforce_guardrails(parser: configparser.ConfigParser, config_path: Path) -> None:
388
328
  """§5.2 bounded-complexity limits + §5.3 name grammar, fail closed.
389
329
 
@@ -391,14 +331,10 @@ def _enforce_guardrails(parser: configparser.ConfigParser, config_path: Path) ->
391
331
  echoing the value (it may be sensitive). Limits are measured on the
392
332
  decoded Python strings configparser produced.
393
333
 
394
- Runs BEFORE the §8 migration (review M1), so the parser may still
395
- hold the legacy [default] section — it is the pre-migration
396
- baseline and counts/validates like [main], never as a named
397
- profile.
334
+ Only [main] is the baseline; every other section is a named profile.
398
335
  """
399
336
  sections = parser.sections()
400
- baseline_sections = {RESERVED_BASELINE_PROFILE, _LEGACY_BASELINE_SECTION}
401
- non_main = [s for s in sections if s not in baseline_sections]
337
+ non_main = [s for s in sections if s != RESERVED_BASELINE_PROFILE]
402
338
  if len(non_main) > MAX_PROFILE_COUNT - 1:
403
339
  raise CliConfigError(
404
340
  f"invalid config {config_path}: {len(non_main)} named profiles "
@@ -406,7 +342,7 @@ def _enforce_guardrails(parser: configparser.ConfigParser, config_path: Path) ->
406
342
  f"besides the reserved [main])"
407
343
  )
408
344
  for section in sections:
409
- if section not in baseline_sections and not PROFILE_NAME_PATTERN.fullmatch(section):
345
+ if section != RESERVED_BASELINE_PROFILE and not PROFILE_NAME_PATTERN.fullmatch(section):
410
346
  raise CliConfigError(
411
347
  f"invalid config {config_path}: section [{section}] is not "
412
348
  f"a valid profile name — 3–15 characters, lowercase a–z, "
@@ -434,7 +370,7 @@ def _enforce_guardrails(parser: configparser.ConfigParser, config_path: Path) ->
434
370
  f"character limit (value not shown)"
435
371
  )
436
372
  if key == "tenant_id" and CANONICAL_TENANT_ID_PATTERN.fullmatch(value) is None:
437
- # B154 PR 5 (Spec §5.4/§8.6): canonical validation is
373
+ # B154 PR 5 (Spec §5.4/§8): canonical validation is
438
374
  # MANDATORY for every bound profile — the PR-2
439
375
  # prototype-era compatibility (deprecation warning) is
440
376
  # removed. Hand-edited values are untrusted input.
@@ -471,25 +407,16 @@ def validate_hosted_trust_domain(tenant_id: str | None, value: str) -> str:
471
407
 
472
408
 
473
409
  def _load_validated_config_unlocked() -> configparser.ConfigParser:
474
- """THE config entry point: parse, validate, migrate, return.
475
-
476
- Every reader and writer calls this, so migration runs on the first
477
- PR 2 command that touches configuration (§8) and no code path ever
478
- acts on config that fails the §5 contract. Validation precedes
479
- migration (§8 step 1 / review M1): an over-limit or malformed file
480
- is rejected without being rewritten.
481
- """
410
+ """Parse and validate one config snapshot without modifying the filesystem."""
482
411
  config_path = cli_home() / "config"
483
412
  parser = _parse_config_text(config_path)
484
413
  _enforce_guardrails(parser, config_path)
485
- parser = _migrate_legacy_default_section(parser, config_path)
486
414
  return parser
487
415
 
488
416
 
489
417
  def load_validated_config() -> configparser.ConfigParser:
490
- """Validated atomic config snapshot; serializes the legacy migration."""
491
- with _config_mutation_lock():
492
- return _load_validated_config_unlocked()
418
+ """Read one snapshot; atomic replacement keeps concurrent writes untorn."""
419
+ return _load_validated_config_unlocked()
493
420
 
494
421
 
495
422
  def _write_parser_atomically(parser: configparser.ConfigParser, config_path: Path) -> None:
@@ -50,7 +50,6 @@ from aac_cli.agent_layout import (
50
50
  TenantPaths,
51
51
  agent_paths,
52
52
  pending_admin_key_paths,
53
- refuse_legacy_directory,
54
53
  tenant_paths,
55
54
  )
56
55
  from aac_cli.agent_record import (
@@ -254,7 +253,6 @@ def _load_or_create_record(
254
253
  args: argparse.Namespace, agent: AgentPaths, profile: str, config: ResolvedConfig
255
254
  ) -> tuple[AgentRecord, str]:
256
255
  """Return (record, outcome) where outcome is 'created' or 'resumed'."""
257
- refuse_legacy_directory(agent.name)
258
256
  root_problem = managed_path_problem(agent.root, root=cli_home(), kind="directory")
259
257
  if root_problem:
260
258
  raise CliConfigError(f"refusing to use agent {agent.name!r}: {root_problem}")
@@ -1790,7 +1788,6 @@ def _refresh_settings(
1790
1788
 
1791
1789
  def _configuration_preflight(args: argparse.Namespace, agent: AgentPaths) -> None:
1792
1790
  """Resolve immutable identity and validate input before any profile/remote write."""
1793
- refuse_legacy_directory(agent.name)
1794
1791
  recorded = read_record(agent.record) if agent.record.exists() else None
1795
1792
  args.accepted_config, args.accepted_https_cas = None, []
1796
1793
  if args.agent_config:
@@ -30,7 +30,7 @@ import sys
30
30
  import tempfile
31
31
  from collections.abc import Generator
32
32
  from contextlib import contextmanager
33
- from dataclasses import asdict, dataclass, replace
33
+ from dataclasses import asdict, dataclass
34
34
  from pathlib import Path
35
35
 
36
36
  import httpx
@@ -58,13 +58,12 @@ __all__ = [
58
58
  "read_registration_state",
59
59
  "registration_state_exists",
60
60
  "registration_state_path",
61
- "upgrade_registration_state",
62
61
  "validate_tenant_domain",
63
62
  "write_registration_state",
64
63
  ]
65
64
 
66
65
  SCHEMA_VERSION = 2
67
- SUPPORTED_SCHEMA_VERSIONS = frozenset({1, 2})
66
+ SUPPORTED_SCHEMA_VERSIONS = frozenset({SCHEMA_VERSION})
68
67
 
69
68
  # The §9.2 visible warning field — the file is AAC-managed.
70
69
  STATE_FILE_WARNING = (
@@ -339,16 +338,8 @@ def _normalize_existing_registration_intent(
339
338
  )
340
339
 
341
340
 
342
- def _canonical_local_intent(
343
- admin_url: str, intent: RegistrationIntent, *, schema_version: int = SCHEMA_VERSION
344
- ) -> str:
341
+ def _canonical_local_intent(admin_url: str, intent: RegistrationIntent) -> str:
345
342
  intent_fields = asdict(intent)
346
- if schema_version == 1:
347
- # Schema v1 predates B121. Its digest did not contain a business-domain
348
- # collection; accepting it as empty preserves unresolved registrations.
349
- intent_fields.pop("tenant_domains")
350
- elif schema_version != 2:
351
- raise CliConfigError(f"unsupported registration-state schema_version {schema_version}")
352
343
  public_pem = intent_fields.pop("tenant_admin_pubkey_pem")
353
344
  intent_fields["tenant_admin_key_id"] = _spki_key_id_from_normalized_pem(public_pem)
354
345
  return json.dumps(
@@ -362,29 +353,8 @@ def _canonical_local_intent(
362
353
  )
363
354
 
364
355
 
365
- def local_intent_sha256(
366
- admin_url: str, intent: RegistrationIntent, *, schema_version: int = SCHEMA_VERSION
367
- ) -> str:
368
- return hashlib.sha256(
369
- _canonical_local_intent(admin_url, intent, schema_version=schema_version).encode("utf-8")
370
- ).hexdigest()
371
-
372
-
373
- def upgrade_registration_state(state: RegistrationState) -> RegistrationState:
374
- """Upgrade a validated v1 state to v2 without changing its intent."""
375
- if state.schema_version == SCHEMA_VERSION:
376
- return state
377
- if state.schema_version != 1:
378
- raise CliConfigError(f"unsupported schema_version {state.schema_version!r}")
379
- upgraded = replace(state, schema_version=SCHEMA_VERSION)
380
- return replace(
381
- upgraded,
382
- local_intent_sha256=local_intent_sha256(
383
- upgraded.control_plane_admin_url,
384
- upgraded.registration_intent,
385
- schema_version=SCHEMA_VERSION,
386
- ),
387
- )
356
+ def local_intent_sha256(admin_url: str, intent: RegistrationIntent) -> str:
357
+ return hashlib.sha256(_canonical_local_intent(admin_url, intent).encode("utf-8")).hexdigest()
388
358
 
389
359
 
390
360
  def registration_state_dir() -> Path:
@@ -454,14 +424,13 @@ def _field_violation(state: RegistrationState) -> str | None:
454
424
  expected_digest = local_intent_sha256(
455
425
  state.control_plane_admin_url,
456
426
  state.registration_intent,
457
- schema_version=state.schema_version,
458
427
  )
459
428
  if not hmac.compare_digest(state.local_intent_sha256, expected_digest):
460
429
  return "local_intent_sha256 does not match the frozen normalized intent"
461
430
  return None
462
431
 
463
432
 
464
- def _registration_intent_from_raw(raw: object, *, schema_version: int) -> RegistrationIntent:
433
+ def _registration_intent_from_raw(raw: object) -> RegistrationIntent:
465
434
  if not isinstance(raw, dict):
466
435
  raise TypeError("registration_intent is not an object")
467
436
  expected = {
@@ -473,9 +442,8 @@ def _registration_intent_from_raw(raw: object, *, schema_version: int) -> Regist
473
442
  "agents",
474
443
  "tenant_admin_pubkey_pem",
475
444
  "admission",
445
+ "tenant_domains",
476
446
  }
477
- if schema_version == 2:
478
- expected.add("tenant_domains")
479
447
  if set(raw) != expected:
480
448
  raise TypeError(f"registration_intent fields differ (expected {sorted(expected)})")
481
449
  admission_raw = raw["admission"]
@@ -487,7 +455,7 @@ def _registration_intent_from_raw(raw: object, *, schema_version: int) -> Regist
487
455
  raise TypeError("registration_intent.admission has invalid fields")
488
456
  workloads_raw = raw["workloads"]
489
457
  domains_raw = raw["spiffe_trust_domains"]
490
- tenant_domains_raw = raw.get("tenant_domains", [])
458
+ tenant_domains_raw = raw["tenant_domains"]
491
459
  agents_raw = raw["agents"]
492
460
  if (
493
461
  not isinstance(workloads_raw, list)
@@ -632,9 +600,7 @@ def read_registration_state(
632
600
  attempt_count=raw["attempt_count"],
633
601
  control_plane_admin_url=raw["control_plane_admin_url"],
634
602
  local_intent_sha256=raw["local_intent_sha256"],
635
- registration_intent=_registration_intent_from_raw(
636
- raw["registration_intent"], schema_version=raw_schema_version
637
- ),
603
+ registration_intent=_registration_intent_from_raw(raw["registration_intent"]),
638
604
  )
639
605
  except (KeyError, TypeError, ValueError, CliConfigError) as exc:
640
606
  raise _reject(path, f"missing or mistyped field ({exc})") from exc
@@ -675,11 +641,6 @@ def write_registration_state(state: RegistrationState) -> Path:
675
641
  f"unsupported schema_version {state.schema_version!r}; "
676
642
  f"this CLI supports {sorted(SUPPORTED_SCHEMA_VERSIONS)}"
677
643
  )
678
- if state.schema_version != SCHEMA_VERSION:
679
- raise CliConfigError(
680
- f"refusing to write legacy registration-state schema_version "
681
- f"{state.schema_version}; upgrade to {SCHEMA_VERSION} first"
682
- )
683
644
  violation = _field_violation(state)
684
645
  if violation is not None:
685
646
  raise CliConfigError(
@@ -10,20 +10,35 @@ import sys
10
10
 
11
11
  def query_trace(profile: str, root: str) -> dict:
12
12
  """Run the installed CLI's trace command in-process, without discovery."""
13
+ return _query(
14
+ ["chain", "show", f"--profile={profile}", f"--token-id={root}", "--output=json"],
15
+ "chain-show",
16
+ )
17
+
18
+
19
+ def query_chains(profile: str, params: dict[str, str]) -> dict:
20
+ """Consume one public chain-list JSON page using the owning CLI."""
21
+ flags = {"from": "from", "to": "to", "limit": "limit", "cursor": "page-token"}
22
+ return _query(
23
+ ["chain", "list", f"--profile={profile}", "--output=json"]
24
+ + [f"--{flags[key]}={value}" for key, value in params.items()],
25
+ "chain-list",
26
+ )
27
+
28
+
29
+ def _query(arguments: list[str], contract: str) -> dict:
13
30
  from .cli import main
14
31
 
15
32
  output, errors = io.StringIO(), io.StringIO()
16
33
  with contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors):
17
- code = main(
18
- ["chain", "show", f"--profile={profile}", f"--token-id={root}", "--output=json"]
19
- )
34
+ code = main(arguments)
20
35
  if code:
21
36
  raise ValueError(f"central query failed (aac exit {code}): {errors.getvalue().strip()}")
22
37
  sys.stderr.write(errors.getvalue())
23
38
  try:
24
39
  body = json.loads(output.getvalue())
25
40
  if not isinstance(body, dict):
26
- raise ValueError("expected trace object")
41
+ raise ValueError("expected JSON object")
27
42
  return body
28
43
  except (ValueError, TypeError) as exc:
29
- raise ValueError(f"incompatible chain-show JSON: {exc}") from exc
44
+ raise ValueError(f"incompatible {contract} JSON: {exc}") from exc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aac-cli
3
- Version: 0.2.5
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
@@ -69,8 +69,13 @@ Python 3.11 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.
@@ -8,6 +8,7 @@ aac_cli/__init__.py
8
8
  aac_cli/__main__.py
9
9
  aac_cli/admin_key_pem.py
10
10
  aac_cli/aeg_cli.py
11
+ aac_cli/aeg_list.py
11
12
  aac_cli/agent_cli.py
12
13
  aac_cli/agent_config.py
13
14
  aac_cli/agent_health.py
@@ -32,7 +32,7 @@ build-backend = "setuptools.build_meta"
32
32
 
33
33
  [project]
34
34
  name = "aac-cli"
35
- version = "0.2.5"
35
+ version = "0.2.6"
36
36
  description = "The aac platform CLI — headless operator interface to the AAC control plane (tenant registration, chain audit)."
37
37
  readme = "README.md"
38
38
  requires-python = ">=3.11"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes