feat(secrets-broker): add 'secret rm' + a new-namespace heads-up on put

Both from jackdaw-dev feedback after a mis-namespaced item (missing host prefix)
hid under a prefix nobody searches:
- 'secret rm <name>' — delete an item by exact name (bw soft-delete to trash,
  recoverable); closes the 'no delete path, append-only in practice' gap.
- 'secret put' now warns (stderr, non-blocking) when a name opens a brand-new
  top-level namespace, listing existing ones + suggesting the host prefix —
  catches a typo'd/missing prefix at store time.

Installed copy at ~/.local/bin/secret synced.
This commit is contained in:
vh
2026-08-11 22:58:33 -07:00
parent 850a1976d5
commit a249073a08
+23
View File
@@ -16,6 +16,7 @@ Commands:
secret put <name> (--file P | --stdin) [--folder C] [--field k=v]...
secret get <name> [--field F] [--file OUT]
secret list [--prefix P]
secret rm <name> # delete an item (soft-delete to trash)
secret backfill [--dry-run] # scan THIS box's local secrets, upsert each
"""
import argparse
@@ -186,6 +187,16 @@ def cmd_put(a):
for kv in (a.field or []):
k, v = kv.split("=", 1)
extra.append({"name": k, "value": v, "type": 0})
# namespace heads-up: warn (don't block) when this opens a brand-new top-level
# namespace — catches a typo'd/missing host prefix at store time instead of
# letting the item hide under a prefix nobody searches.
if not find(env, s, a.name):
top = a.name.split("/")[0]
tops = {i["name"].split("/")[0] for i in json.loads(bw(["list", "items"], env, s).stdout.decode() or "[]")}
if top not in tops:
print(f"secret: note — '{a.name}' opens a NEW top-level namespace '{top}/' "
f"(existing: {', '.join(sorted(tops)) or 'none'}). If you meant to host-prefix it, "
f"'secret rm {a.name}' and re-put as {socket.gethostname()}/...", file=sys.stderr)
_, sha, mode = store_secret(env, s, a.name, data, a.folder, extra)
print(f"stored: {a.name} (sha256 {sha[:12]}, {len(data)} bytes, {mode})")
@@ -209,6 +220,16 @@ def cmd_get(a):
sys.stdout.buffer.write(data if data.endswith(b"\n") else data + b"\n")
def cmd_rm(a):
env = load_env()
s = session(env)
it = find(env, s, a.name) or die(f"not found: {a.name}")
full = json.loads(bw(["get", "item", it["id"]], env, s).stdout.decode())
sha = next((f["value"][:12] for f in (full.get("fields") or []) if f["name"] == "sha256"), "?")
bw(["delete", "item", it["id"]], env, s) # soft-delete to trash (recoverable in the vault)
print(f"deleted: {a.name} (sha256 {sha}, id {it['id']}) -> trash")
def cmd_list(a):
env = load_env()
s = session(env)
@@ -294,6 +315,8 @@ def main():
pg = sub.add_parser("get"); pg.add_argument("name")
pg.add_argument("--field"); pg.add_argument("--file"); pg.set_defaults(fn=cmd_get)
pl = sub.add_parser("list"); pl.add_argument("--prefix"); pl.set_defaults(fn=cmd_list)
pr = sub.add_parser("rm", help="delete an item by exact name (soft-delete to trash)")
pr.add_argument("name"); pr.set_defaults(fn=cmd_rm)
pb = sub.add_parser("backfill", help="scan THIS box's local secret files and upsert each")
pb.add_argument("--dry-run", action="store_true"); pb.set_defaults(fn=cmd_backfill)
a = p.parse_args()