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