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

1"""Argument parser construction for the Stigmem CLI.""" 

2 

3from __future__ import annotations 

4 

5import argparse 

6import os 

7 

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 

45 

46 

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 

54 

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 

62 

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 ) 

83 

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) 

113 

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) 

126 

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) 

141 

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 

146 

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) 

163 

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 

171 

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) 

175 

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) 

180 

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) 

185 

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) 

189 

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) 

193 

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) 

197 

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 

205 

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) 

209 

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) 

213 

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) 

217 

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) 

234 

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) 

264 

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) 

268 

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) 

273 

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 

281 

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) 

329 

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) 

348 

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) 

371 

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 

376 

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) 

404 

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) 

439 

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 

444 

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) 

483 

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 

491 

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 

495 

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) 

529 

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) 

595 

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 

603 

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) 

628 

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 

636 

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) 

669 

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) 

696 

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 

704 

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) 

738 

739 return parser