Coverage for node / src / stigmem_node / cli / parser.py: 100%
227 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-18 05:34 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-18 05:34 +0000
1"""Argument parser construction for the Stigmem CLI."""
3from __future__ import annotations
5import argparse
6import os
8from ..cli_admin_handlers import (
9 _cmd_audit_discovery,
10 _cmd_auth_bootstrap_key,
11 _cmd_backfill_cids,
12 _cmd_identity_rotate_key,
13 _cmd_instruction_manifest_generate,
14 _cmd_instruction_migrate,
15)
16from ..cli_federation_handlers import (
17 _cmd_federation_cursor_export,
18 _cmd_federation_cursor_import,
19)
20from .capability import (
21 _cmd_capability_issue,
22 _cmd_capability_revoke,
23 _cmd_capability_verify,
24)
25from .federation import (
26 _cmd_federation_dnssec_confirm,
27 _cmd_federation_dnssec_pending,
28 _cmd_federation_dnssec_reject,
29 _cmd_federation_register_peer,
30)
31from .maintenance import _cmd_decay_sweep, _cmd_migrate_normalize_entities
32from .mcp import (
33 _cmd_mcp_config,
34 _cmd_mcp_detect,
35 _cmd_mcp_doctor,
36 _cmd_mcp_install,
37 _cmd_mcp_smoke,
38 _cmd_mcp_status,
39)
40from .plugins import (
41 _cmd_doctor,
42 _cmd_plugins_describe,
43 _cmd_plugins_disable,
44 _cmd_plugins_doctor,
45 _cmd_plugins_enable,
46 _cmd_plugins_list,
47 _cmd_plugins_search,
48)
49from .snapshot import _cmd_snapshot_create, _cmd_snapshot_restore
52def _build_parser() -> argparse.ArgumentParser:
53 parser = argparse.ArgumentParser(
54 prog="stigmem",
55 description="Stigmem reference node CLI",
56 )
57 sub = parser.add_subparsers(dest="command", metavar="COMMAND")
58 sub.required = True
60 # ------------------------------------------------------------------ capability
61 cap_p = sub.add_parser(
62 "capability",
63 help="capability token management (Spec-06-Capability-Tokens)",
64 )
65 cap_sub = cap_p.add_subparsers(dest="cap_command", metavar="SUBCOMMAND")
66 cap_sub.required = True
68 _cap_common = argparse.ArgumentParser(add_help=False)
69 _cap_common.add_argument(
70 "--node-url",
71 dest="node_url",
72 default="http://localhost:8765",
73 metavar="URL",
74 help="base URL of the local node (default: http://localhost:8765)",
75 )
76 _cap_common.add_argument(
77 "--api-key",
78 dest="api_key",
79 default=None,
80 metavar="KEY",
81 help="API key for authentication",
82 )
83 _cap_common.add_argument(
84 "--json",
85 action="store_true",
86 help="output raw JSON response",
87 )
89 # capability issue
90 ci_p = cap_sub.add_parser(
91 "issue",
92 parents=[_cap_common],
93 help="issue a new capability token",
94 )
95 ci_p.add_argument("--issuer", required=True, metavar="URI", help="issuer entity URI")
96 ci_p.add_argument("--subject", required=True, metavar="URI", help="subject entity URI")
97 ci_p.add_argument(
98 "--verb",
99 required=True,
100 metavar="VERB",
101 help="permission verb (e.g. read, write)",
102 )
103 ci_p.add_argument(
104 "--object",
105 required=True,
106 metavar="OBJECT",
107 help="object URI the token grants access to (e.g. stigmem://facts)",
108 )
109 ci_p.add_argument(
110 "--ttl-seconds",
111 dest="ttl_seconds",
112 type=int,
113 default=None,
114 metavar="N",
115 help="token lifetime in seconds (default: node default; max: 7776000 / 90 days)",
116 )
117 ci_p.set_defaults(func=_cmd_capability_issue)
119 # capability verify
120 cv_p = cap_sub.add_parser(
121 "verify",
122 parents=[_cap_common],
123 help="verify a capability token",
124 )
125 cv_p.add_argument(
126 "token_json",
127 metavar="TOKEN_JSON",
128 help="capability token JSON string; pass '-' to read from stdin",
129 )
130 cv_p.set_defaults(func=_cmd_capability_verify)
132 # capability revoke
133 cr_p = cap_sub.add_parser(
134 "revoke",
135 parents=[_cap_common],
136 help="revoke a capability token by token_id",
137 )
138 cr_p.add_argument("token_id", metavar="TOKEN_ID", help="ID of the token to revoke")
139 cr_p.add_argument(
140 "--reason",
141 default="",
142 metavar="REASON",
143 help="human-readable reason for revocation",
144 )
145 cr_p.set_defaults(func=_cmd_capability_revoke)
147 # ------------------------------------------------------------------ migrate
148 migrate_p = sub.add_parser("migrate", help="database migration utilities")
149 migrate_sub = migrate_p.add_subparsers(dest="migrate_command", metavar="SUBCOMMAND")
150 migrate_sub.required = True
152 ne_p = migrate_sub.add_parser(
153 "normalize-entities",
154 help="populate entity_aliases from non-canonical entity/source URIs in facts (Spec-01-Fact-Model)", # noqa: E501
155 )
156 ne_p.add_argument(
157 "--dry-run",
158 action="store_true",
159 help="print aliases without inserting",
160 )
161 ne_p.add_argument(
162 "--db",
163 metavar="PATH",
164 default=None,
165 help="path to stigmem.db (default: STIGMEM_DB_PATH env or settings default)",
166 )
167 ne_p.set_defaults(func=_cmd_migrate_normalize_entities)
169 # ------------------------------------------------------------------ plugins
170 plugins_p = sub.add_parser(
171 "plugins",
172 help="inspect installed plugins (PR 4-INF.2)",
173 )
174 plugins_sub = plugins_p.add_subparsers(dest="plugins_command", metavar="SUBCOMMAND")
175 plugins_sub.required = True
177 pl_p = plugins_sub.add_parser("list", help="list installed plugins")
178 pl_p.add_argument("--json", action="store_true", help="output as JSON")
179 pl_p.set_defaults(func=_cmd_plugins_list)
181 pd_p = plugins_sub.add_parser("describe", help="describe one installed plugin")
182 pd_p.add_argument("name", metavar="NAME", help="plugin name")
183 pd_p.add_argument("--json", action="store_true", help="output as JSON")
184 pd_p.set_defaults(func=_cmd_plugins_describe)
186 ps_p = plugins_sub.add_parser("search", help="search the built-in plugin catalog")
187 ps_p.add_argument("query", metavar="QUERY", help="catalog search term")
188 ps_p.add_argument("--json", action="store_true", help="output as JSON")
189 ps_p.set_defaults(func=_cmd_plugins_search)
191 pe_p = plugins_sub.add_parser("enable", help="print install and enable commands")
192 pe_p.add_argument("name", metavar="NAME", help="plugin slug or package name")
193 pe_p.set_defaults(func=_cmd_plugins_enable)
195 pdis_p = plugins_sub.add_parser("disable", help="print disable command")
196 pdis_p.add_argument("name", metavar="NAME", help="plugin slug or package name")
197 pdis_p.set_defaults(func=_cmd_plugins_disable)
199 pdoc_p = plugins_sub.add_parser("doctor", help="diagnose plugin install and enable state")
200 pdoc_p.add_argument("--json", action="store_true", help="output as JSON")
201 pdoc_p.set_defaults(func=_cmd_plugins_doctor)
203 # ------------------------------------------------------------------ mcp
204 mcp_p = sub.add_parser(
205 "mcp",
206 help="manage stigmem-mcp editor integrations",
207 )
208 mcp_sub = mcp_p.add_subparsers(dest="mcp_command", metavar="SUBCOMMAND")
209 mcp_sub.required = True
211 mcp_doc_p = mcp_sub.add_parser("doctor", help="diagnose stigmem-mcp setup")
212 mcp_doc_p.add_argument("--json", action="store_true", help="output as JSON")
213 mcp_doc_p.set_defaults(func=_cmd_mcp_doctor)
215 mcp_detect_p = mcp_sub.add_parser("detect", help="detect known editor configs")
216 mcp_detect_p.add_argument("--json", action="store_true", help="output as JSON")
217 mcp_detect_p.set_defaults(func=_cmd_mcp_detect)
219 mcp_status_p = mcp_sub.add_parser("status", help="show editor config status")
220 mcp_status_p.add_argument("--json", action="store_true", help="output as JSON")
221 mcp_status_p.set_defaults(func=_cmd_mcp_status)
223 mcp_config_p = mcp_sub.add_parser("config", help="print editor config snippets")
224 mcp_config_p.add_argument("editor", nargs="?", metavar="EDITOR", help="editor slug")
225 mcp_config_p.add_argument("--list", action="store_true", help="list supported editors")
226 mcp_config_p.add_argument(
227 "--stigmem-url",
228 default=os.getenv("STIGMEM_URL", "http://localhost:8765"),
229 metavar="URL",
230 help="Stigmem node URL for the emitted snippet",
231 )
232 mcp_config_p.add_argument(
233 "--stigmem-api-key",
234 default=os.getenv("STIGMEM_API_KEY", "<your-api-key>"),
235 metavar="KEY",
236 help="Stigmem API key for the emitted snippet",
237 )
238 mcp_config_p.set_defaults(func=_cmd_mcp_config)
240 mcp_install_p = mcp_sub.add_parser("install", help="write an editor MCP config")
241 mcp_install_p.add_argument("editor", metavar="EDITOR", help="editor slug")
242 mcp_install_p.add_argument("--write", action="store_true", help="apply the planned change")
243 mcp_install_p.add_argument(
244 "--dry-run",
245 dest="write",
246 action="store_false",
247 help="preview the planned change without writing (default)",
248 )
249 mcp_install_p.add_argument(
250 "--force",
251 action="store_true",
252 help="replace existing stigmem-mcp entry",
253 )
254 mcp_install_p.add_argument("--yes", action="store_true", help="skip interactive confirmation")
255 mcp_install_p.add_argument("--backup-dir", default=None, metavar="DIR", help="backup directory")
256 mcp_install_p.add_argument(
257 "--stigmem-url",
258 default=os.getenv("STIGMEM_URL", "http://localhost:8765"),
259 metavar="URL",
260 help="Stigmem node URL to write",
261 )
262 mcp_install_p.add_argument(
263 "--stigmem-api-key",
264 default=os.getenv("STIGMEM_API_KEY", "<your-api-key>"),
265 metavar="KEY",
266 help="Stigmem API key to write",
267 )
268 mcp_install_p.set_defaults(func=_cmd_mcp_install)
270 mcp_smoke_p = mcp_sub.add_parser("smoke", help="verify an editor MCP setup")
271 mcp_smoke_p.add_argument("editor", metavar="EDITOR", help="editor slug")
272 mcp_smoke_p.set_defaults(func=_cmd_mcp_smoke)
274 # ------------------------------------------------------------------ doctor
275 doctor_p = sub.add_parser("doctor", help="print node and plugin diagnostics")
276 doctor_p.add_argument("--json", action="store_true", help="output as JSON")
277 doctor_p.set_defaults(func=_cmd_doctor)
279 # ------------------------------------------------------------------ federation
280 fed_p = sub.add_parser(
281 "federation",
282 help="federation management (Spec-05-Federation-Trust)",
283 )
284 fed_sub = fed_p.add_subparsers(dest="fed_command", metavar="SUBCOMMAND")
285 fed_sub.required = True
287 rp_p = fed_sub.add_parser(
288 "register-peer",
289 help="register this node as a peer with a remote node (Spec-05-Federation-Trust)",
290 )
291 rp_p.add_argument(
292 "--remote-url",
293 required=True,
294 metavar="URL",
295 help="base URL of the remote node (e.g. http://node-b:8765)",
296 )
297 rp_p.add_argument(
298 "--local-url",
299 default=None,
300 metavar="URL",
301 help="base URL of this node as seen by the remote (default: STIGMEM_NODE_URL)",
302 )
303 rp_p.add_argument(
304 "--scopes",
305 default="company,public",
306 metavar="SCOPE[,SCOPE]",
307 help='comma-separated scopes to share (default: "company,public")',
308 )
309 rp_p.add_argument(
310 "--api-key",
311 default=None,
312 metavar="KEY",
313 help="API key for the remote node (required when remote auth_required=true)",
314 )
315 rp_p.add_argument(
316 "--tls-cert",
317 default=None,
318 metavar="FILE",
319 help="client certificate for mTLS federation registration",
320 )
321 rp_p.add_argument(
322 "--tls-key",
323 default=None,
324 metavar="FILE",
325 help="client private key for mTLS federation registration",
326 )
327 rp_p.add_argument(
328 "--ca-bundle",
329 default=None,
330 metavar="FILE",
331 help="CA bundle used to verify HTTPS federation peers",
332 )
333 rp_p.set_defaults(func=_cmd_federation_register_peer)
335 # ------------------------------------------------------------------ federation dnssec
336 # DNSSEC first-trust operator-confirm queue (Rev 6 I9). All three subcommands
337 # call the local node's admin API over HTTP (admin:federation), mirroring the
338 # register-peer node-API pattern. entity_uri travels in the request body.
339 dnssec_p = fed_sub.add_parser(
340 "dnssec",
341 help="DNSSEC first-trust operator-confirm queue (list/confirm/reject)",
342 )
343 dnssec_sub = dnssec_p.add_subparsers(dest="dnssec_command", metavar="SUBCOMMAND")
344 dnssec_sub.required = True
346 def _add_dnssec_node_args(p: argparse.ArgumentParser) -> None:
347 p.add_argument(
348 "--node-url",
349 default=None,
350 metavar="URL",
351 help="base URL of the local node (default: STIGMEM_NODE_URL)",
352 )
353 p.add_argument(
354 "--api-key",
355 default=None,
356 metavar="KEY",
357 help="admin:federation API key for the local node",
358 )
360 dp_p = dnssec_sub.add_parser(
361 "pending",
362 help="list quarantined DNSSEC first-trust candidates",
363 )
364 _add_dnssec_node_args(dp_p)
365 dp_p.set_defaults(func=_cmd_federation_dnssec_pending)
367 dc_p = dnssec_sub.add_parser(
368 "confirm",
369 help="paste-to-confirm a quarantined candidate (the key fingerprint must match)",
370 )
371 dc_p.add_argument("--entity-uri", dest="entity_uri", required=True, metavar="URI")
372 dc_p.add_argument("--node-id", dest="node_id", required=True, metavar="NODE_ID")
373 dc_p.add_argument(
374 "--key-fpr",
375 dest="key_fpr",
376 required=True,
377 metavar="FPR",
378 help="the candidate key fingerprint, pasted exactly (NF-D4-5)",
379 )
380 _add_dnssec_node_args(dc_p)
381 dc_p.set_defaults(func=_cmd_federation_dnssec_confirm)
383 dr_p = dnssec_sub.add_parser(
384 "reject",
385 help="reject a quarantined candidate without trusting it",
386 )
387 dr_p.add_argument("--entity-uri", dest="entity_uri", required=True, metavar="URI")
388 dr_p.add_argument("--node-id", dest="node_id", required=True, metavar="NODE_ID")
389 _add_dnssec_node_args(dr_p)
390 dr_p.set_defaults(func=_cmd_federation_dnssec_reject)
392 # ------------------------------------------------------------------ federation cursor-export
393 ce_p = fed_sub.add_parser(
394 "cursor-export",
395 help="export replication cursor positions to a JSON checkpoint file",
396 )
397 ce_p.add_argument(
398 "--out",
399 default="-",
400 metavar="FILE",
401 help='output file path (default: stdout, use "-" for stdout)',
402 )
403 ce_p.add_argument(
404 "--db",
405 metavar="PATH",
406 default=None,
407 help="path to stigmem.db (default: STIGMEM_DB_PATH env or settings default)",
408 )
409 ce_p.set_defaults(func=_cmd_federation_cursor_export)
411 # ------------------------------------------------------------------ federation cursor-import
412 ci_p = fed_sub.add_parser(
413 "cursor-import",
414 help="restore replication cursors from a checkpoint file after DB loss",
415 )
416 ci_p.add_argument(
417 "checkpoint_file",
418 metavar="FILE",
419 help="path to checkpoint JSON produced by cursor-export",
420 )
421 ci_p.add_argument(
422 "--force",
423 action="store_true",
424 help="overwrite cursors that are already set (default: skip existing non-null cursors)",
425 )
426 ci_p.add_argument(
427 "--db",
428 metavar="PATH",
429 default=None,
430 help="path to stigmem.db (default: STIGMEM_DB_PATH env or settings default)",
431 )
432 ci_p.set_defaults(func=_cmd_federation_cursor_import)
434 # ------------------------------------------------------------------ snapshot
435 snap_p = sub.add_parser("snapshot", help="backup/restore with signed manifests (Phase 8)")
436 snap_sub = snap_p.add_subparsers(dest="snap_command", metavar="SUBCOMMAND")
437 snap_sub.required = True
439 sc_p = snap_sub.add_parser(
440 "create",
441 help="create a signed, content-addressed snapshot tarball",
442 )
443 sc_p.add_argument(
444 "--out",
445 metavar="PATH",
446 default=None,
447 help=(
448 "output path for the .tar.gz (default: auto-named stigmem-snapshot-<ts>-<hash>.tar.gz)"
449 ),
450 )
451 sc_p.add_argument(
452 "--sign-with",
453 dest="sign_with",
454 metavar="KEY_FILE",
455 default=None,
456 help="path to a file containing a raw base64url Ed25519 private key (32 bytes); "
457 "default: use the node's built-in federation key",
458 )
459 sc_p.add_argument(
460 "--db",
461 metavar="PATH",
462 default=None,
463 help="path to stigmem.db (default: STIGMEM_DB_PATH env or settings default)",
464 )
465 sc_p.set_defaults(func=_cmd_snapshot_create)
467 sr_p = snap_sub.add_parser(
468 "restore",
469 help="verify signature + hashes and restore a snapshot tarball",
470 )
471 sr_p.add_argument(
472 "--from",
473 dest="from_path",
474 metavar="PATH",
475 required=True,
476 help="path to the .tar.gz snapshot to restore",
477 )
478 sr_p.add_argument(
479 "--trusted-keys",
480 dest="trusted_keys",
481 metavar="PATH",
482 default=None,
483 help="JSON file listing trusted base64url Ed25519 public keys; "
484 "default: only the local node's own key",
485 )
486 sr_p.add_argument(
487 "--force-unverified",
488 dest="force_unverified",
489 action="store_true",
490 help=(
491 "restore even if signature or hash verification fails (logged loudly; NOT recommended)"
492 ),
493 )
494 sr_p.add_argument(
495 "--db",
496 metavar="PATH",
497 default=None,
498 help="destination database path (default: STIGMEM_DB_PATH env or settings default)",
499 )
500 sr_p.set_defaults(func=_cmd_snapshot_restore)
502 # ------------------------------------------------------------------ decay
503 decay_p = sub.add_parser("decay", help="decay sweeper — expire stale facts (Phase 6)")
504 decay_sub = decay_p.add_subparsers(dest="decay_command", metavar="SUBCOMMAND")
505 decay_sub.required = True
507 sw_p = decay_sub.add_parser(
508 "sweep",
509 help="mark non-expiring or low-confidence facts as expired",
510 )
511 sw_p.add_argument(
512 "--ttl-seconds",
513 dest="ttl_seconds",
514 type=int,
515 default=None,
516 metavar="N",
517 help="expire non-expiring facts older than N seconds (0 = expire all)",
518 )
519 sw_p.add_argument(
520 "--min-confidence",
521 dest="min_confidence",
522 type=float,
523 default=None,
524 metavar="F",
525 help="expire active facts with confidence below F (0.0–1.0)",
526 )
527 sw_p.add_argument(
528 "--scope",
529 default="",
530 metavar="SCOPE",
531 help="restrict sweep to one scope (local/team/company/public)",
532 )
533 sw_tenant_grp = sw_p.add_mutually_exclusive_group()
534 sw_tenant_grp.add_argument(
535 "--tenant",
536 dest="tenant",
537 default="default",
538 metavar="TENANT",
539 help="restrict sweep to one tenant (default: 'default')",
540 )
541 sw_tenant_grp.add_argument(
542 "--all-tenants",
543 dest="all_tenants",
544 action="store_true",
545 help="sweep every tenant (for multi-tenant operators); mutually exclusive with --tenant",
546 )
547 sw_p.add_argument(
548 "--dry-run",
549 action="store_true",
550 help="print what would be decayed without writing",
551 )
552 sw_p.add_argument(
553 "--db",
554 metavar="PATH",
555 default=None,
556 help="path to stigmem.db (default: STIGMEM_DB_PATH env or settings default)",
557 )
558 sw_p.set_defaults(func=_cmd_decay_sweep)
560 # ------------------------------------------------------------------ instruction
561 instr_p = sub.add_parser(
562 "instruction",
563 help="instruction manifest tools (Spec-X1-Lazy-Instruction-Discovery)",
564 )
565 instr_sub = instr_p.add_subparsers(dest="instr_command", metavar="SUBCOMMAND")
566 instr_sub.required = True
568 im_p = instr_sub.add_parser("manifest", help="manage instruction manifests")
569 im_sub = im_p.add_subparsers(dest="manifest_command", metavar="SUBCOMMAND")
570 im_sub.required = True
572 img_p = im_sub.add_parser(
573 "generate",
574 help="generate a manifest JSON from a directory of markdown instruction files",
575 )
576 img_p.add_argument(
577 "path", metavar="PATH", help="directory containing markdown instruction files"
578 )
579 img_p.add_argument(
580 "--agent-id",
581 dest="agent_id",
582 required=True,
583 metavar="AGENT_ID",
584 help="agent UUID to embed in generated fact_uri values",
585 )
586 img_p.add_argument(
587 "--deployment",
588 default="default",
589 metavar="DEPLOYMENT",
590 help="deployment namespace for instruction: URIs (default: default)",
591 )
592 img_p.add_argument(
593 "--version",
594 default="v1",
595 metavar="VERSION",
596 help="manifest version string (default: v1)",
597 )
598 img_p.add_argument(
599 "--out",
600 default=None,
601 metavar="FILE",
602 help="write JSON to FILE instead of stdout",
603 )
604 img_p.set_defaults(func=_cmd_instruction_manifest_generate)
606 # instruction migrate
607 imig_p = instr_sub.add_parser(
608 "migrate",
609 help="migrate markdown instruction files to stigmem facts + publish manifest",
610 )
611 imig_p.add_argument("path", metavar="PATH", help="markdown file or directory to migrate")
612 scope_grp = imig_p.add_mutually_exclusive_group(required=True)
613 scope_grp.add_argument(
614 "--role", default=None, metavar="ROLE", help="agent role name (e.g. cto)"
615 )
616 scope_grp.add_argument(
617 "--skill", default=None, metavar="SKILL", help="skill name (e.g. paperclip)"
618 )
619 imig_p.add_argument(
620 "--agent-id",
621 dest="agent_id",
622 required=True,
623 metavar="AGENT_ID",
624 help="agent UUID owning the manifest",
625 )
626 imig_p.add_argument(
627 "--deployment",
628 default="default",
629 metavar="DEPLOYMENT",
630 help="deployment namespace (default: default)",
631 )
632 imig_p.add_argument(
633 "--version",
634 default="v1",
635 metavar="VERSION",
636 help="fact version string (default: v1)",
637 )
638 imig_p.add_argument(
639 "--node-url",
640 dest="node_url",
641 default="http://127.0.0.1:8000",
642 metavar="URL",
643 help="stigmem node base URL (default: http://127.0.0.1:8000)",
644 )
645 imig_p.add_argument(
646 "--api-key",
647 dest="api_key",
648 default=None,
649 metavar="KEY",
650 help="API key (or set STIGMEM_API_KEY env var)",
651 )
652 imig_p.add_argument(
653 "--db",
654 default=None,
655 metavar="PATH",
656 help="path to stigmem.db for local idempotency checks (skips HTTP fact queries)",
657 )
658 imig_p.add_argument(
659 "--dry-run",
660 dest="dry_run",
661 action="store_true",
662 help="show diff without writing any facts or manifest",
663 )
664 imig_p.add_argument(
665 "--yes",
666 "-y",
667 action="store_true",
668 help="skip confirmation prompt",
669 )
670 imig_p.set_defaults(func=_cmd_instruction_migrate)
672 # ------------------------------------------------------------------ audit
673 audit_p = sub.add_parser(
674 "audit",
675 help="discovery audit reports (Spec-X1-Lazy-Instruction-Discovery)",
676 )
677 audit_sub = audit_p.add_subparsers(dest="audit_command", metavar="SUBCOMMAND")
678 audit_sub.required = True
680 ad_p = audit_sub.add_parser(
681 "discovery",
682 help="print discovery audit metrics: Recall@k, Hit@k, miss rate",
683 )
684 ad_p.add_argument(
685 "--agent",
686 required=True,
687 metavar="AGENT_ID_OR_ROLE",
688 help="agent ID (UUID) or role substring to filter",
689 )
690 ad_p.add_argument(
691 "--since",
692 default=None,
693 metavar="DATE",
694 help="ISO 8601 date/datetime to start from (default: 7 days ago)",
695 )
696 ad_p.add_argument(
697 "--db",
698 default=None,
699 metavar="PATH",
700 help="path to stigmem.db (default: STIGMEM_DB_PATH env or settings default)",
701 )
702 ad_p.add_argument("--json", action="store_true", help="output as JSON")
703 ad_p.set_defaults(func=_cmd_audit_discovery)
705 # ------------------------------------------------------------------ identity
706 id_p = sub.add_parser(
707 "identity",
708 help="node identity management (Spec-10-Hardening)",
709 )
710 id_sub = id_p.add_subparsers(dest="identity_command", metavar="SUBCOMMAND")
711 id_sub.required = True
713 rk_p = id_sub.add_parser(
714 "rotate-key",
715 help="rotate the node or issuer Ed25519 key with a dual-trust window (Spec-10-Hardening)", # noqa: E501
716 )
717 rk_p.add_argument(
718 "--kind",
719 choices=["node", "issuer"],
720 required=True,
721 metavar="KIND",
722 help="key type to rotate: node (federation identity) or issuer (capability token signing)",
723 )
724 rk_p.add_argument(
725 "--dry-run",
726 dest="dry_run",
727 action="store_true",
728 help="generate artefacts and print new key without writing to TL or DB",
729 )
730 rk_p.add_argument(
731 "--dual-trust-days",
732 dest="dual_trust_days",
733 type=int,
734 default=90,
735 metavar="DAYS",
736 help="days the retiring key stays in accept_set (default: 90; must be ≥ 90)",
737 )
738 rk_p.add_argument(
739 "--db",
740 default=None,
741 metavar="PATH",
742 help="path to stigmem.db (default: STIGMEM_DB_PATH env or settings default)",
743 )
744 rk_p.set_defaults(func=_cmd_identity_rotate_key)
746 # ------------------------------------------------------------------ backfill-cids
747 bc_p = sub.add_parser(
748 "backfill-cids",
749 help="compute and persist CIDs for facts that pre-date CID backfill (Spec-21-Content-Addressed-IDs)", # noqa: E501
750 )
751 bc_p.add_argument(
752 "--db",
753 dest="db",
754 default=None,
755 metavar="PATH",
756 help="path to stigmem.db (default: STIGMEM_DB_PATH env or settings default)",
757 )
758 bc_p.add_argument(
759 "--batch-size",
760 dest="batch_size",
761 type=int,
762 default=500,
763 metavar="N",
764 help="facts to process per transaction (default: 500)",
765 )
766 bc_p.add_argument(
767 "--quiet",
768 action="store_true",
769 help="suppress progress output",
770 )
771 bc_p.set_defaults(func=_cmd_backfill_cids)
773 # ------------------------------------------------------------------ auth
774 auth_p = sub.add_parser(
775 "auth",
776 help="API key management (Spec-06-Capability-Tokens)",
777 )
778 auth_sub = auth_p.add_subparsers(dest="auth_command", metavar="SUBCOMMAND")
779 auth_sub.required = True
781 # auth bootstrap-key
782 bk_p = auth_sub.add_parser(
783 "bootstrap-key",
784 help=(
785 "register a caller-provided admin API key on a fresh install "
786 "(refuses if api_keys is non-empty; system never generates the key)"
787 ),
788 )
789 bk_p.add_argument(
790 "--key",
791 dest="key",
792 default=None,
793 metavar="VALUE",
794 help=(
795 "raw API key value to register. Generate externally; e.g., "
796 "`openssl rand -hex 32`. Alternative: STIGMEM_BOOTSTRAP_KEY env var."
797 ),
798 )
799 bk_p.add_argument(
800 "--entity-uri",
801 dest="entity_uri",
802 default="agent:admin",
803 metavar="URI",
804 help="entity URI to associate with the bootstrap key (default: agent:admin)",
805 )
806 bk_p.add_argument(
807 "--permissions",
808 dest="permissions",
809 default="admin,write,read",
810 metavar="LIST",
811 help=("comma-separated permissions for the bootstrap key (default: admin,write,read)"),
812 )
813 bk_p.set_defaults(func=_cmd_auth_bootstrap_key)
815 return parser