secubox-deb/HOWTO-grammar.md
CyberMind 53a630e9f8
docs: codify the SecuBox CTL grammar (closes #216) (#217)
Permanent documentation for the 8-verb / 8-layer modular tools box that
emerged across #173, #176, #180-#184, #190, #212. Three new docs + one
README section, all bearing the 1991 attribution.

  docs/grammar.md            Canonical conceptual frame: layered map,
                             8 verbs in a table, composing-the-grammar
                             worked examples, naming conventions,
                             philosophical roots.

  HOWTO-grammar.md           Concrete 6-step walkthrough for adding a
                             9th verb (~1h budget): issue framing,
                             worktree, CTL skeleton (Bash + Python),
                             FastAPI mirror, Debian packaging, nginx
                             route under secubox-routes.d/, live test
                             on the board, commit/PR conventions,
                             common pitfalls drawn from real incidents
                             (#163 secubox.d vs secubox-routes.d,
                             #173 LXC default path, #176 hook trigger,
                             #194 CAP_NET_ADMIN, #180 naming).

  wiki/CTL-Grammar.md        Public-facing wiki page mirrored to the
                             GitHub wiki repo (per the wiki-sync
                             pattern established for Acknowledgments).
                             Embeds Punk Exposure (Peek/Poke/Emancipate)
                             as the seed concept with 1991 attribution.

  README.md                  New section under Features summarising the
                             grammar with the 9-row layer table and a
                             pointer to docs/grammar.md + HOWTO-grammar.md.

Attribution recorded on all three documents: copyright spiritual
concept Gerald Kerma (GK2), 1991, Notre-Dame-du-Cruet, Savoie.

The CTL is the user's voice. The modular tools box is their organ. The
language is the way they say *this is mine, this is mine, this is mine*.

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-05-19 08:01:08 +02:00

9.2 KiB

HOWTO — forge a new CTL verb

Copyright spiritual concept · Gérald Kerma · 1991

This is the concrete walkthrough. See docs/grammar.md for the conceptual frame and the 8 canonical verbs already in place.

You want to add xctl widget add/remove/list (or any 9th verb). Follow these steps in order. Time budget: ~1 hour for a Bash-style verb, ~2 hours for a Python+FastAPI one.


1. Frame the gap

Write the issue body following this template (adapt verbs/nouns):

## Context

The X module already exposes <capability> via the web UI and FastAPI, but
operators have no CLI grammar for it. Closing the gap parallel to:

- `mitmproxyctl route add` (#173) — INTERCEPTION
- `giteactl repo mirror add` (#176) — REPLICATION

## Gap

Forge `xctl widget` with subcommands:

  xctl widget add NAME [--option ...]
  xctl widget remove NAME
  xctl widget list
  xctl widget status NAME

## Out of scope

(Anything you defer to a followup ticket.)

Submit, capture the issue number. Reference it as #<N> in everything that follows.


2. Worktree

scripts/agent-worktree.sh start --issue <N>
cd ~/CyberMindStudio/secubox-deb-worktrees/<N>-<slug>

You're now on a branch named feature/<N>-<slug> (or fix/... if the issue is labeled bug). Every commit message ends with (ref #<N>) or (closes #<N>) on the final commit.


3. Pick the language

When Use
The verb is mostly shell-orchestration (lxc-attach, systemctl, file ops) Bash
The verb needs rich subparsers, JSON I/O, or a registry pattern Python

Both styles exist in-tree. Reference implementations:

  • Bashpackages/secubox-gitea/sbin/giteactl, packages/secubox-metablogizer/sbin/metablogizerctl
  • Pythonpackages/secubox-mitmproxy/bin/mitmproxyctl, packages/secubox-health-doctor/sbin/healthctl

4. Skeleton

Bash skeleton

#!/bin/bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
#
# xctl — SecuBox X control (issue #<N>).

set -euo pipefail
VERSION="0.1.0"
CONFIG_FILE="/etc/secubox/x.toml"

GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
log()   { printf "${GREEN}[X]${NC} %s\n" "$*"; }
warn()  { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; }
error() { printf "${RED}[ERROR]${NC} %s\n" "$*" >&2; }

# Lifecycle
cmd_start()    { systemctl start   secubox-x.service && log started; }
cmd_stop()     { systemctl stop    secubox-x.service && log stopped; }
cmd_restart()  { systemctl restart secubox-x.service && log restarted; }
cmd_status()   { systemctl is-active secubox-x.service; }
cmd_logs()     { journalctl -u secubox-x.service -n "${1:-50}" --no-pager; }

# Three-fold (JSON discovery)
cmd_components() {
  cat <<EOF
{"service":"secubox-x.service","api_base":"/api/v1/x","ctl_version":"$VERSION"}
EOF
}
cmd_access() {
  cat <<EOF
{"api":{"list":"GET /widgets","add":"POST /widget","remove":"DELETE /widget/{name}"}}
EOF
}

# Widget noun
cmd_widget() {
  local act="${1:-}"; shift || true
  case "$act" in
    add)     widget_add "$@" ;;
    remove|rm|delete) widget_remove "$@" ;;
    list|ls) widget_list "$@" ;;
    status)  widget_status "$@" ;;
    *) echo "Usage: xctl widget {add|remove|list|status} [args]" ;;
  esac
}

widget_add()    { :; }   # implement: name validation, side effect, idempotent
widget_remove() { :; }
widget_list()   { :; }
widget_status() { :; }

case "${1:-}" in
  start|stop|restart|status) c="$1"; shift; cmd_$c "$@" ;;
  logs)       shift; cmd_logs "$@" ;;
  components) cmd_components ;;
  access)     cmd_access ;;
  widget)     shift; cmd_widget "$@" ;;
  *) echo "Usage: xctl <command>"; exit 1 ;;
esac

Python skeleton

#!/usr/bin/env python3
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
"""xctl — SecuBox X control (#<N>)."""
from __future__ import annotations
import argparse, json, sys
from pathlib import Path

sys.path.insert(0, "/usr/lib/secubox/x")
from api import engine  # canonical writer used by FastAPI too

def cmd_widget_add(args): engine.widget_add(args.name); return 0
def cmd_widget_remove(args): engine.widget_remove(args.name); return 0
def cmd_widget_list(args): print(json.dumps(engine.widget_list(), indent=2)); return 0

def build_parser():
    p = argparse.ArgumentParser(prog="xctl")
    sub = p.add_subparsers(dest="cmd", required=True)
    for n in ("start", "stop", "restart", "status", "components", "access", "logs"):
        sub.add_parser(n)
    w = sub.add_parser("widget"); ws = w.add_subparsers(dest="wc", required=True)
    add = ws.add_parser("add"); add.add_argument("name"); add.set_defaults(func=cmd_widget_add)
    rm  = ws.add_parser("remove"); rm.add_argument("name"); rm.set_defaults(func=cmd_widget_remove)
    ls  = ws.add_parser("list"); ls.set_defaults(func=cmd_widget_list)
    return p

def main():
    args = build_parser().parse_args()
    return args.func(args) if hasattr(args, "func") else 0

if __name__ == "__main__":
    sys.exit(main())

5. FastAPI mirror

Every verb has a route. Same engine, two callers (CTL and API).

# packages/secubox-x/api/main.py
from fastapi import FastAPI
from . import engine

app = FastAPI(root_path="/api/v1/x")

@app.get("/widgets")
def widget_list(): return engine.widget_list()

@app.post("/widget")
def widget_add(name: str): return engine.widget_add(name)

@app.delete("/widget/{name}")
def widget_remove(name: str): return engine.widget_remove(name)

The CTL and the web UI both call into engine.widget_*. Don't duplicate business logic between CTL and API — they're two surfaces on the same engine.


6. Debian packaging

packages/secubox-x/debian/:

  • control — Depends on secubox-core, python3-uvicorn, etc.
  • changelog — bump version, include Closes: #<N>.
  • rules — install the CTL to /usr/sbin/xctl, the API to /usr/lib/secubox/x/, the systemd unit to /lib/systemd/system/.
  • postinstsystemctl enable --now secubox-x.service (idempotent).
  • prermsystemctl disable --now secubox-x.service.

systemd unit boilerplate:

[Unit]
Description=SecuBox X
After=network.target secubox-core.service

[Service]
Type=simple
User=secubox
Group=secubox
WorkingDirectory=/usr/lib/secubox/x
ExecStart=/usr/bin/python3 -m uvicorn api.main:app \
    --uds /run/secubox/x.sock --log-level warning
Restart=on-failure
ReadWritePaths=/run/secubox /var/cache/secubox

[Install]
WantedBy=multi-user.target

If your CTL needs to read nft sets, add AmbientCapabilities=CAP_NET_ADMIN (see #194 — without it, subprocess.run(["nft", ...]) silently returns empty).


7. Nginx route (so the API is reachable via admin.gk2)

Add /etc/nginx/secubox-routes.d/x.conf:

location /api/v1/x/ {
    proxy_pass http://unix:/run/secubox/x.sock:/api/v1/x/;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_read_timeout 60s;
}

This goes in secubox-routes.d/, not secubox.d/ — the admin.gk2 server block includes the former (lesson learned from #163).


8. Live test on the board

The verb is only forged when it survives prod. From your worktree:

# Copy the CTL onto the board first
scp packages/secubox-x/sbin/xctl root@192.168.1.200:/usr/sbin/

ssh root@192.168.1.200 'xctl access | jq .'        # discovery
ssh root@192.168.1.200 'xctl widget add demo'      # the new verb
ssh root@192.168.1.200 'xctl widget list'          # observe side-effect

If anything is wrong, the operator's CLI tells them so. That's the whole point of the grammar.


9. Commit + PR

Commit messages stay terse. Don't include AI attribution footers — the project's global reference is enough. Example:

feat(xctl): forge widget noun verbs (closes #<N>)

Ninth verb of the SecuBox grammar, layer = <layer>. Parallel to
mitmproxyctl route (#173), giteactl repo mirror (#176).

Subcommands:
  xctl widget add NAME [--option ...]
  xctl widget remove NAME
  xctl widget list
  xctl widget status NAME

Live-tested on gk2 board: <one-line observation>.

Push the branch, open the PR with a one-paragraph summary + a test plan checklist. Don't add 🤖 Generated with Claude Code style footers.


10. Update grammar.md

After merge, add a row to the canonical table in docs/grammar.md and the wiki version. The grammar is a living document; new verbs extend it.


Common pitfalls

  • Naming: it's <module>ctl, not <module>-ctl or <module>_ctl. Renaming after the fact (e.g. metactlpublishctl in #180) is user-hostile; pick right from the start.
  • Nounlessness: don't ship a flat verb set without a noun. xctl upload invites future verb collisions; xctl widget upload does not.
  • AmbientCapabilities vs NoNewPrivileges: they coexist when caps are set in the unit (#194).
  • Filesystem fetches don't trigger Gitea hooks (#176): for verbs that pull data into Gitea-managed repos, use Gitea's API, not git fetch on the bare repo from the host.
  • Default LXC paths: board reality is /data/lxc/, not /var/lib/lxc/ (#173). Auto-detect or read from config.
  • The grammar surfaces real bugs: when healthctl check flags 4/11 failing on first run, those are real anomalies, not check bugs.