cortexdb-cli 0.7.0__tar.gz → 0.7.1__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 (50) hide show
  1. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/PKG-INFO +23 -6
  2. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/README.md +22 -5
  3. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/__init__.py +1 -1
  4. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/admin_access.py +14 -1
  5. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/changes.py +121 -35
  6. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/tokens.py +4 -2
  7. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/errors.py +19 -2
  8. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/pyproject.toml +1 -1
  9. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/client.py +0 -0
  10. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/__init__.py +0 -0
  11. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/access.py +0 -0
  12. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/admin.py +0 -0
  13. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/answer.py +0 -0
  14. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/apps.py +0 -0
  15. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/audit.py +0 -0
  16. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/auth.py +0 -0
  17. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/beliefs.py +0 -0
  18. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/claims.py +0 -0
  19. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/code.py +0 -0
  20. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/compose.py +0 -0
  21. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/config_cmd.py +0 -0
  22. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/conflicts.py +0 -0
  23. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/directory.py +0 -0
  24. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/entities.py +0 -0
  25. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/episodes.py +0 -0
  26. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/erase.py +0 -0
  27. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/experience.py +0 -0
  28. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/export_cmd.py +0 -0
  29. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/facts.py +0 -0
  30. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/forget.py +0 -0
  31. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/import_cmd.py +0 -0
  32. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/init.py +0 -0
  33. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/interactive.py +0 -0
  34. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/onboard.py +0 -0
  35. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/policy.py +0 -0
  36. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/publish.py +0 -0
  37. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/recall.py +0 -0
  38. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/rollups.py +0 -0
  39. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/scopes.py +0 -0
  40. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/search.py +0 -0
  41. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/understanding.py +0 -0
  42. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/watch.py +0 -0
  43. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/commands/wire.py +0 -0
  44. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/common.py +0 -0
  45. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/config.py +0 -0
  46. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/harness_packs_data.py +0 -0
  47. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/main.py +0 -0
  48. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/onboarding.py +0 -0
  49. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/output.py +0 -0
  50. {cortexdb_cli-0.7.0 → cortexdb_cli-0.7.1}/cortexdb_cli/state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cortexdb-cli
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Command-line interface for CortexDB — the long-term memory layer for AI.
5
5
  Project-URL: Homepage, https://cortexdb.ai
6
6
  Project-URL: Documentation, https://cortexdb.ai/docs
@@ -36,8 +36,8 @@ Command-line interface for [CortexDB](https://cortexdb.ai) — the long-term mem
36
36
 
37
37
  Wraps the v1 HTTP API in a small, fast Click + Rich CLI. Anonymous signup
38
38
  in one command, no email or card; works against the public SaaS or any
39
- self-hosted CortexDB deployment. Version 0.7.0 covers CortexDB 0.14.0,
40
- shared memory included: the access API, change sets, the directory,
39
+ self-hosted CortexDB deployment. Version 0.7.1 covers CortexDB 0.14.1 and
40
+ 0.14.0, shared memory included: the access API, change sets, the directory,
41
41
  applications, rollups, publishing and the new erasures.
42
42
 
43
43
  ## Install
@@ -81,6 +81,20 @@ When a token expires you have two safe options:
81
81
  For a second brain alongside the first, isolate instead of replacing:
82
82
  `cortexdb --config-dir <dir> init`, or set `CORTEXDB_HOME`.
83
83
 
84
+ ## What's new for CortexDB 0.14.1 (cortexdb-cli 0.7.1)
85
+
86
+ - **Change sets that take long.** A staging, a preview or an activation whose
87
+ evaluation runs past the server's wait answers 202 and goes on. `changes
88
+ stage`, `parts`, `preview` and `activate` send the request again until it
89
+ answers, with a progress line, for `--wait` seconds (120 by default); past
90
+ it they exit 75 and say how to collect the answer. A 202 is never an error.
91
+ - **The tenant-binding dry run** shows how the operator plane will decide the
92
+ real run, and says when `--apply` needs `--break-glass`.
93
+ - **A write that would make a version-2 record version 1** is refused; the
94
+ hint names the fields it drops and `--convert`.
95
+ - **Token lifetimes** are the server's settings: by default 90 days for a
96
+ person, 7 for an agent, 30 for a service, 24 hours with a chain claim.
97
+
84
98
  ## What's new for CortexDB 0.14.0 (cortexdb-cli 0.7.0)
85
99
 
86
100
  CortexDB 0.14.0 is the breaking release of shared memory. What changes for a
@@ -112,9 +126,9 @@ CLI user:
112
126
  compartment gets the server's default (granular there) instead of 422
113
127
  `VIEW_NOT_ALLOWED_AT_SCOPE`.
114
128
  - **Tokens.** A token that carries `scopes` holds only the families its
115
- entries name, lifetimes are capped (7 days for a person or an agent, 30 for
116
- a service, 24 hours with chain claims), and `act` must name a delegable
117
- agent. Every mint names its tenant root (`--scope`, `--tenant` or its
129
+ entries name, lifetimes are capped (the server's settings since 0.14.1: by
130
+ default 90 days for a person, 7 for an agent, 30 for a service, 24 hours
131
+ with chain claims), and `act` must name a delegable agent. Every mint names its tenant root (`--scope`, `--tenant` or its
118
132
  `--confine` entries); only the operator mints deployment-wide, for a
119
133
  service. A minted token holds no more than its minter. `cortexdb tokens
120
134
  mint` mints with every field: `--confine`, `--exact` (the `CAP:PATH!`
@@ -241,6 +255,9 @@ cortexdb changes discard org-compile
241
255
  `--file` holds a list of records (each a put), operations
242
256
  (`{"path", "op": "put" | "delete", "record"}`), `{"records": [...]}` or
243
257
  `{"parts": [...]}`; `--delete PATH` adds deletes; `changes parts` stages more.
258
+ A staging, a preview or an activation whose evaluation runs long answers 202
259
+ (0.14.1): the command sends it again until it answers, for `--wait` seconds
260
+ (120 by default), then exits 75 saying how to collect the answer.
244
261
 
245
262
  ### watch and notices
246
263
 
@@ -4,8 +4,8 @@ Command-line interface for [CortexDB](https://cortexdb.ai) — the long-term mem
4
4
 
5
5
  Wraps the v1 HTTP API in a small, fast Click + Rich CLI. Anonymous signup
6
6
  in one command, no email or card; works against the public SaaS or any
7
- self-hosted CortexDB deployment. Version 0.7.0 covers CortexDB 0.14.0,
8
- shared memory included: the access API, change sets, the directory,
7
+ self-hosted CortexDB deployment. Version 0.7.1 covers CortexDB 0.14.1 and
8
+ 0.14.0, shared memory included: the access API, change sets, the directory,
9
9
  applications, rollups, publishing and the new erasures.
10
10
 
11
11
  ## Install
@@ -49,6 +49,20 @@ When a token expires you have two safe options:
49
49
  For a second brain alongside the first, isolate instead of replacing:
50
50
  `cortexdb --config-dir <dir> init`, or set `CORTEXDB_HOME`.
51
51
 
52
+ ## What's new for CortexDB 0.14.1 (cortexdb-cli 0.7.1)
53
+
54
+ - **Change sets that take long.** A staging, a preview or an activation whose
55
+ evaluation runs past the server's wait answers 202 and goes on. `changes
56
+ stage`, `parts`, `preview` and `activate` send the request again until it
57
+ answers, with a progress line, for `--wait` seconds (120 by default); past
58
+ it they exit 75 and say how to collect the answer. A 202 is never an error.
59
+ - **The tenant-binding dry run** shows how the operator plane will decide the
60
+ real run, and says when `--apply` needs `--break-glass`.
61
+ - **A write that would make a version-2 record version 1** is refused; the
62
+ hint names the fields it drops and `--convert`.
63
+ - **Token lifetimes** are the server's settings: by default 90 days for a
64
+ person, 7 for an agent, 30 for a service, 24 hours with a chain claim.
65
+
52
66
  ## What's new for CortexDB 0.14.0 (cortexdb-cli 0.7.0)
53
67
 
54
68
  CortexDB 0.14.0 is the breaking release of shared memory. What changes for a
@@ -80,9 +94,9 @@ CLI user:
80
94
  compartment gets the server's default (granular there) instead of 422
81
95
  `VIEW_NOT_ALLOWED_AT_SCOPE`.
82
96
  - **Tokens.** A token that carries `scopes` holds only the families its
83
- entries name, lifetimes are capped (7 days for a person or an agent, 30 for
84
- a service, 24 hours with chain claims), and `act` must name a delegable
85
- agent. Every mint names its tenant root (`--scope`, `--tenant` or its
97
+ entries name, lifetimes are capped (the server's settings since 0.14.1: by
98
+ default 90 days for a person, 7 for an agent, 30 for a service, 24 hours
99
+ with chain claims), and `act` must name a delegable agent. Every mint names its tenant root (`--scope`, `--tenant` or its
86
100
  `--confine` entries); only the operator mints deployment-wide, for a
87
101
  service. A minted token holds no more than its minter. `cortexdb tokens
88
102
  mint` mints with every field: `--confine`, `--exact` (the `CAP:PATH!`
@@ -209,6 +223,9 @@ cortexdb changes discard org-compile
209
223
  `--file` holds a list of records (each a put), operations
210
224
  (`{"path", "op": "put" | "delete", "record"}`), `{"records": [...]}` or
211
225
  `{"parts": [...]}`; `--delete PATH` adds deletes; `changes parts` stages more.
226
+ A staging, a preview or an activation whose evaluation runs long answers 202
227
+ (0.14.1): the command sends it again until it answers, for `--wait` seconds
228
+ (120 by default), then exits 75 saying how to collect the answer.
212
229
 
213
230
  ### watch and notices
214
231
 
@@ -1,3 +1,3 @@
1
1
  """CortexDB CLI - command-line interface for the v1 memory API."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.7.1"
@@ -151,7 +151,11 @@ def tenant_settings(ctx, root, mode, readiness_ticket, clear_readiness):
151
151
  def tenant_binding(ctx, root, from_tenant, apply_):
152
152
  """Bind a root's records and tombstones to the tenant it names (POST /v1/admin/access/tenant-binding).
153
153
 
154
- A DRY RUN unless --apply; running it again is safe.
154
+ A DRY RUN unless --apply; running it again is safe. The dry run says how
155
+ the operator plane decides the real run (operator_plane: whether it
156
+ registers the root, is in-band, the tenant's mode): when it says
157
+ break_glass_required, pass --break-glass REASON --break-glass-ticket
158
+ TICKET with --apply.
155
159
  """
156
160
  cfg = ctx.obj
157
161
  scope = root_or_die(cfg, root)
@@ -169,6 +173,15 @@ def tenant_binding(ctx, root, from_tenant, apply_):
169
173
  + (", root registered" if d.get("root_registered") else ""))
170
174
  if d.get("consistency_token"):
171
175
  console.print(f"[dim] consistency token: {escape(str(d['consistency_token']))}[/dim]")
176
+ plane = d.get("operator_plane")
177
+ if isinstance(plane, dict):
178
+ # 0.14.1 (p6a C): how the operator plane decides the run.
179
+ console.print(f" operator plane: mode {escape(str(plane.get('mode')))}"
180
+ + (", the run registers the root" if plane.get("registers_root") else "")
181
+ + (", an in-band path for the operator key" if plane.get("in_band") else ""))
182
+ if plane.get("break_glass_required"):
183
+ console.print("[warning] The real run needs break-glass: add --break-glass REASON "
184
+ "--break-glass-ticket TICKET to --apply (else 403 BREAK_GLASS_REQUIRED).[/warning]")
172
185
  if d.get("dry_run"):
173
186
  console.print("[dim]Nothing was changed. Run again with --apply.[/dim]")
174
187
 
@@ -33,7 +33,15 @@ import click
33
33
  from cortexdb_cli.client import get_client, path_segment
34
34
  from cortexdb_cli.common import api_options, body_of, emit, read_json_file, root_or_die, send
35
35
  from cortexdb_cli.errors import handle_errors
36
- from cortexdb_cli.output import console, escape, is_json_mode
36
+ from cortexdb_cli.output import console, err_console, escape, is_json_mode, print_json
37
+
38
+ #: The exit status of a change-set command whose --wait ran out while the
39
+ #: server still answered 202 (EX_TEMPFAIL): nothing failed, the evaluation
40
+ #: goes on, and the same command sent again collects its answer.
41
+ EXIT_STILL_EVALUATING = 75
42
+
43
+ #: How long a change-set command keeps sending while the server answers 202.
44
+ DEFAULT_WAIT_S = 120.0
37
45
 
38
46
 
39
47
  @click.group("changes")
@@ -41,6 +49,63 @@ def changes_group() -> None:
41
49
  """Access change sets - stage, preview, activate, discard."""
42
50
 
43
51
 
52
+ def _wait_option(fn):
53
+ return click.option(
54
+ "--wait", "wait_s", type=click.FloatRange(min=0), default=DEFAULT_WAIT_S, show_default=True,
55
+ help="Seconds to keep sending the request while the server answers 202 (its evaluation runs "
56
+ "on); past it the command exits 75 and the same command collects the answer later.",
57
+ )(fn)
58
+
59
+
60
+ def _evaluating(resp: Any) -> bool:
61
+ """A 202 that means the evaluation still runs (0.14.1): a preview's
62
+ `{state: "previewing"}`, a staging's or an activation's empty body. A
63
+ dated activation's 202 carries its status, and is its answer."""
64
+ if resp.status_code != 202:
65
+ return False
66
+ data = body_of(resp)
67
+ return not data or (isinstance(data, dict) and data.get("state") == "previewing")
68
+
69
+
70
+ def _pause(resp: Any) -> float:
71
+ try:
72
+ seconds = float(resp.headers.get("retry-after") or 2)
73
+ except ValueError:
74
+ seconds = 2.0
75
+ return min(max(seconds, 0.5), 10.0)
76
+
77
+
78
+ def send_until_evaluated(cfg: dict[str, Any], label: str, call, wait_s: float) -> tuple[Any, float | None]:
79
+ """Send `call` until the server answers anything but an evaluation's
80
+ 202. The same request sent again joins the evaluation, or once it ended
81
+ is answered by it. Returns the response and, when --wait ran out first,
82
+ the seconds waited (else None)."""
83
+ start = time.monotonic()
84
+ while True:
85
+ resp = send(cfg, label, call)
86
+ if not _evaluating(resp):
87
+ return resp, None
88
+ waited = time.monotonic() - start
89
+ pause = _pause(resp)
90
+ if waited + pause > wait_s:
91
+ return resp, waited
92
+ if not is_json_mode(cfg):
93
+ err_console.print(f"[dim] still evaluating after {waited:.0f} s (202); asking again in "
94
+ f"{pause:.0f} s[/dim]")
95
+ time.sleep(pause)
96
+
97
+
98
+ def still_evaluating(cfg: dict[str, Any], change_id: str, what: str, waited: float, again: str):
99
+ """--wait ran out on a 202: say so (never as an error) and exit 75."""
100
+ if is_json_mode(cfg):
101
+ print_json({"id": change_id, "state": "evaluating", "request": what, "waited_s": round(waited),
102
+ "again": again})
103
+ else:
104
+ console.print(f"[warning]The server is still evaluating the {what} of {escape(change_id)} after "
105
+ f"{waited:.0f} s; it goes on. {escape(again)}[/warning]")
106
+ raise SystemExit(EXIT_STILL_EVALUATING)
107
+
108
+
44
109
  def _scope_option(fn):
45
110
  return click.option(
46
111
  "--scope", "-S", default=None,
@@ -239,11 +304,12 @@ def _params(proposer: str | None, scope: str | None = None) -> dict[str, str] |
239
304
  @click.option("--convert", is_flag=True, help="A version-1 tree conversion: its preview must change no access.")
240
305
  @click.option("--resync", is_flag=True, help="A source's resync change set (served during a restore).")
241
306
  @click.option("--resync-last", is_flag=True, help="The last resync change set of the restore.")
307
+ @_wait_option
242
308
  @api_options
243
309
  @click.pass_context
244
310
  @handle_errors
245
311
  def changes_stage(ctx, change_id, file, part, deletes, scope, parts_total, activate_at, reason,
246
- narrowing, convert, resync, resync_last):
312
+ narrowing, convert, resync, resync_last, wait_s):
247
313
  """Stage a change set's first parts under your own id (POST /v1/access/changes).
248
314
 
249
315
  \b
@@ -251,7 +317,9 @@ def changes_stage(ctx, change_id, file, part, deletes, scope, parts_total, activ
251
317
  cortexdb changes stage drop-tmp --delete org:acme/team:tmp --narrowing
252
318
 
253
319
  Staging is idempotent by id: the same parts sent again answer the
254
- change set's status.
320
+ change set's status. A narrowing or a conversion staging whose check
321
+ runs long answers 202 and goes on; the CLI sends it again until it
322
+ answers (--wait).
255
323
  """
256
324
  cfg = ctx.obj
257
325
  doc = read_json_file(file, "--file") if file else None
@@ -277,8 +345,12 @@ def changes_stage(ctx, change_id, file, part, deletes, scope, parts_total, activ
277
345
  if resync or resync_last:
278
346
  body["resync"] = {"last": bool(resync_last)}
279
347
  with get_client(cfg) as client:
280
- data = body_of(send(cfg, "Staging...", lambda: client.post("/v1/access/changes", json=body)))
281
- emit(cfg, data, _render_state)
348
+ resp, waited = send_until_evaluated(cfg, "Staging...",
349
+ lambda: client.post("/v1/access/changes", json=body), wait_s)
350
+ if waited is not None:
351
+ still_evaluating(cfg, change_id, "staging", waited,
352
+ "Send the same `cortexdb changes stage` again for its status, or `cortexdb changes get` it.")
353
+ emit(cfg, body_of(resp), _render_state)
282
354
 
283
355
 
284
356
  @changes_group.command("parts")
@@ -286,24 +358,33 @@ def changes_stage(ctx, change_id, file, part, deletes, scope, parts_total, activ
286
358
  @_records_options
287
359
  @_scope_option
288
360
  @_proposer_option
361
+ @_wait_option
289
362
  @api_options
290
363
  @click.pass_context
291
364
  @handle_errors
292
- def changes_parts(ctx, change_id, file, part, deletes, scope, proposer):
293
- """Stage more parts of a change set (POST /v1/access/changes/{id}/parts)."""
365
+ def changes_parts(ctx, change_id, file, part, deletes, scope, proposer, wait_s):
366
+ """Stage more parts of a change set (POST /v1/access/changes/{id}/parts).
367
+
368
+ A part whose check runs long answers 202 and goes on; the CLI sends it
369
+ again until it answers (--wait).
370
+ """
294
371
  cfg = ctx.obj
295
372
  doc = read_json_file(file, "--file") if file else None
296
373
  body = {"scope": root_or_die(cfg, scope), "parts": build_parts(doc, part, deletes, default_part=None)}
297
374
  with get_client(cfg) as client:
298
- data = body_of(send(cfg, "Staging parts...",
299
- lambda: client.post(_id_path(change_id, "/parts"), json=body,
300
- params=_params(proposer))))
301
- emit(cfg, data, _render_state)
375
+ resp, waited = send_until_evaluated(
376
+ cfg, "Staging parts...",
377
+ lambda: client.post(_id_path(change_id, "/parts"), json=body, params=_params(proposer)), wait_s)
378
+ if waited is not None:
379
+ still_evaluating(cfg, change_id, "staging of its parts", waited,
380
+ "Send the same `cortexdb changes parts` again for its status, or `cortexdb changes get` it.")
381
+ emit(cfg, body_of(resp), _render_state)
302
382
 
303
383
 
304
384
  def preview_change_set(cfg, client, change_id, root, proposer=None, people=False, limit=None,
305
- cursor=None, wait_s=60.0):
306
- """POST .../preview, retrying while the server answers 202 (being computed)."""
385
+ cursor=None, wait_s=DEFAULT_WAIT_S):
386
+ """POST .../preview, sent again while the server answers 202
387
+ `{state: "previewing"}`; exits 75 when --wait runs out first."""
307
388
  body: dict[str, Any] = {"scope": root}
308
389
  if people:
309
390
  body["people"] = True
@@ -311,17 +392,13 @@ def preview_change_set(cfg, client, change_id, root, proposer=None, people=False
311
392
  body["limit"] = limit
312
393
  if cursor:
313
394
  body["cursor"] = cursor
314
- deadline = time.monotonic() + wait_s
315
- while True:
316
- resp = send(cfg, "Previewing...",
317
- lambda: client.post(_id_path(change_id, "/preview"), json=body, params=_params(proposer)))
318
- if resp.status_code != 202 or time.monotonic() >= deadline:
319
- return body_of(resp)
320
- try:
321
- pause = float(resp.headers.get("retry-after") or 1)
322
- except ValueError:
323
- pause = 1.0
324
- time.sleep(min(max(pause, 0.2), 5.0))
395
+ resp, waited = send_until_evaluated(
396
+ cfg, "Previewing...",
397
+ lambda: client.post(_id_path(change_id, "/preview"), json=body, params=_params(proposer)), wait_s)
398
+ if waited is not None:
399
+ still_evaluating(cfg, change_id, "preview", waited,
400
+ "Run the same `cortexdb changes preview` again to collect it.")
401
+ return body_of(resp)
325
402
 
326
403
 
327
404
  @changes_group.command("preview")
@@ -331,8 +408,7 @@ def preview_change_set(cfg, client, change_id, root, proposer=None, people=False
331
408
  @click.option("--people", is_flag=True, help="List each person who gains or loses access, per unit.")
332
409
  @click.option("--limit", type=click.IntRange(min=1), default=None, help="Units per page.")
333
410
  @click.option("--cursor", default=None, help="The next_cursor of a previous page.")
334
- @click.option("--wait", "wait_s", type=float, default=60.0, show_default=True,
335
- help="Seconds to keep asking while the preview is being computed (202).")
411
+ @_wait_option
336
412
  @api_options
337
413
  @click.pass_context
338
414
  @handle_errors
@@ -340,7 +416,9 @@ def changes_preview(ctx, change_id, scope, proposer, people, limit, cursor, wait
340
416
  """A change set's access diff and its sha256 structural hash (POST .../{id}/preview).
341
417
 
342
418
  A page holds at most 1 MiB: a unit that does not fit continues on the
343
- next page (--cursor), and its counts on this page are marked +.
419
+ next page (--cursor), and its counts on this page are marked +. While
420
+ the evaluation runs past the server's wait it answers 202
421
+ {state: previewing}; the CLI asks again until it answers (--wait).
344
422
  """
345
423
  cfg = ctx.obj
346
424
  with get_client(cfg) as client:
@@ -356,16 +434,20 @@ def changes_preview(ctx, change_id, scope, proposer, people, limit, cursor, wait
356
434
  @click.option("--preview-hash", metavar="HASH",
357
435
  help="The approved preview's hash (sha256:...). Without it the CLI previews first.")
358
436
  @click.option("--yes", "-y", is_flag=True, help="Skip the confirmation after the preview.")
437
+ @_wait_option
359
438
  @api_options
360
439
  @click.pass_context
361
440
  @handle_errors
362
- def changes_activate(ctx, change_id, scope, proposer, preview_hash, yes):
441
+ def changes_activate(ctx, change_id, scope, proposer, preview_hash, yes, wait_s):
363
442
  """Approve and activate a change set (POST /v1/access/changes/{id}/activate).
364
443
 
365
444
  Without --preview-hash the CLI previews it, shows the diff, asks (on a
366
445
  terminal, unless --yes), and activates with that preview's hash. A dated
367
- change set (--activate-at) answers 202: the approval is recorded for its
368
- date.
446
+ change set (--activate-at) answers 202 with its status: the approval is
447
+ recorded for its date. An activation whose checks run long answers 202
448
+ with no body and goes on in the background; the CLI sends it again until
449
+ it answers its status (--wait), and each wait (the preview's, then the
450
+ activation's) has the whole of --wait.
369
451
 
370
452
  With the operator key, a change set whose preview says in_band (it
371
453
  registers a scope, adds a service: principal, or adds or widens a
@@ -378,7 +460,7 @@ def changes_activate(ctx, change_id, scope, proposer, preview_hash, yes):
378
460
  root = root_or_die(cfg, scope)
379
461
  with get_client(cfg) as client:
380
462
  if not preview_hash:
381
- preview = preview_change_set(cfg, client, change_id, root, proposer)
463
+ preview = preview_change_set(cfg, client, change_id, root, proposer, wait_s=wait_s)
382
464
  preview_hash = preview.get("preview_hash")
383
465
  if not preview_hash:
384
466
  raise click.UsageError("the preview returned no preview_hash; pass --preview-hash")
@@ -390,10 +472,14 @@ def changes_activate(ctx, change_id, scope, proposer, preview_hash, yes):
390
472
  if not yes:
391
473
  click.confirm(f"Activate change set {change_id}?", abort=True)
392
474
  body = {"scope": root, "preview_hash": preview_hash}
393
- resp = send(cfg, "Activating...",
394
- lambda: client.post(_id_path(change_id, "/activate"), json=body, params=_params(proposer)))
395
- data = body_of(resp)
396
- emit(cfg, data, _render_state)
475
+ resp, waited = send_until_evaluated(
476
+ cfg, "Activating...",
477
+ lambda: client.post(_id_path(change_id, "/activate"), json=body, params=_params(proposer)), wait_s)
478
+ if waited is not None:
479
+ still_evaluating(cfg, change_id, "activation", waited,
480
+ "Its activation goes on in the background: `cortexdb changes get` shows its state, and "
481
+ "the same `cortexdb changes activate --preview-hash` again answers its status.")
482
+ emit(cfg, body_of(resp), _render_state)
397
483
 
398
484
 
399
485
  @changes_group.command("get")
@@ -21,8 +21,10 @@ no `--scope`) or its `--confine` / `--exact` entries, all in that one root
21
21
  the trusted operator's alone, for a `service:` or `system:` subject (else
22
22
  403 `deployment_mint_requires_operator`). A minted token holds no more than
23
23
  its minter: its caps and entries lie within the minting token's own.
24
- Lifetimes are capped by the subject's ceiling (7 days for a person or an
25
- agent, 30 for a service): above it the mint is 422 `ttl_too_long`.
24
+ Lifetimes are capped by the subject's ceiling, the server's settings since
25
+ 0.14.1 (`CORTEX_MINT_MAX_LIFETIME_*`; by default 90 days for a person, 7 for
26
+ an agent, 30 for a service, 24 hours with a chain claim): above it the mint
27
+ is 422 `ttl_too_long`, naming the ceiling (`details.ceiling_s`).
26
28
  """
27
29
 
28
30
  from __future__ import annotations
@@ -171,9 +171,13 @@ def _reason_hints(err: ApiError) -> str | None:
171
171
  if reason == "invalid":
172
172
  return f"--cap {entry} is no capability name; see `cortexdb policy effective` for the names."
173
173
  table = {
174
+ # The ceilings are the server's settings since 0.14.1
175
+ # (CORTEX_MINT_MAX_LIFETIME_*); details.ceiling_s is the one applied.
174
176
  "ttl_too_long": lambda: (
175
- f"Mint a shorter token: --ttl at most {_d(err, 'ceiling_s')} s "
176
- "(7 days for a person or an agent, 30 for a service, 24 h with chain claims)."
177
+ (f"Mint a shorter token: --ttl at most {_d(err, 'ceiling_s')} s, this subject's ceiling"
178
+ if _d(err, "ceiling_s") else "Mint a shorter token: --ttl within this subject's ceiling")
179
+ + " (by default 90 days for a person, 7 for an agent, 30 for a service, 24 h with chain claims; "
180
+ "the server's CORTEX_MINT_MAX_LIFETIME_* settings)."
177
181
  ),
178
182
  "ttl_out_of_range": lambda: "--ttl is past what a timestamp holds; pick a sane lifetime.",
179
183
  "lifetime_over_ceiling": lambda: (
@@ -475,6 +479,19 @@ def hint_for(err: ApiError) -> str | None: # noqa: C901 - one table
475
479
  "app_unknown": "The token's application has no record (`cortexdb apps put`).",
476
480
  "not_combinable": "--mounts does not combine with subtree, cohort or recency order.",
477
481
  }.get(reason, None)
482
+ if code in ("INVALID_ACCESS_RECORD", "INVALID_SCOPE_TREE") and reason == "version_downgrade":
483
+ # 0.14.1 (p6a E): a put that leaves out every version-2 field of a
484
+ # stored version-2 record would make it version 1; only `convert`
485
+ # says that is meant, and a legacy route has none.
486
+ path = _d(err, "path") or "the record"
487
+ dropped = _list(_d(err, "dropped") or [], limit=8) or "its version-2 fields"
488
+ how = (
489
+ "or, if the conversion is meant, stage the change set with --convert (it may change no access)"
490
+ if code == "INVALID_ACCESS_RECORD"
491
+ else "or make the change with a change set staged with --convert (a legacy route has no convert)"
492
+ )
493
+ return (f"{path} is a version-2 record and this write leaves out {dropped}, which would make it "
494
+ f"version 1: state them in the record, {how}.")
478
495
  if code == "INVALID_ACCESS_RECORD":
479
496
  path = _d(err, "path")
480
497
  return f"A record is out of shape{f' at {path}' if path else ''} ({reason or 'see the message'})."
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cortexdb-cli"
7
- version = "0.7.0"
7
+ version = "0.7.1"
8
8
  description = "Command-line interface for CortexDB — the long-term memory layer for AI."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"