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

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 ( 

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 

50 

51 

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 

59 

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 

67 

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 ) 

88 

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) 

118 

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) 

131 

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) 

146 

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 

151 

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) 

168 

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 

176 

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) 

180 

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) 

185 

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) 

190 

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) 

194 

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) 

198 

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) 

202 

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 

210 

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) 

214 

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) 

218 

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) 

222 

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) 

239 

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) 

269 

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) 

273 

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) 

278 

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 

286 

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) 

334 

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 

345 

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 ) 

359 

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) 

366 

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) 

382 

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) 

391 

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) 

410 

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) 

433 

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 

438 

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) 

466 

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) 

501 

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 

506 

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) 

559 

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 

567 

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 

571 

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) 

605 

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) 

671 

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 

679 

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) 

704 

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 

712 

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) 

745 

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) 

772 

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 

780 

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) 

814 

815 return parser