mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 18:36:55 +00:00
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>
327 lines
9.2 KiB
Markdown
327 lines
9.2 KiB
Markdown
# 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):
|
|
|
|
```markdown
|
|
## 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
|
|
|
|
```bash
|
|
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:
|
|
|
|
- **Bash** — `packages/secubox-gitea/sbin/giteactl`, `packages/secubox-metablogizer/sbin/metablogizerctl`
|
|
- **Python** — `packages/secubox-mitmproxy/bin/mitmproxyctl`, `packages/secubox-health-doctor/sbin/healthctl`
|
|
|
|
---
|
|
|
|
## 4. Skeleton
|
|
|
|
### Bash skeleton
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```python
|
|
#!/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).
|
|
|
|
```python
|
|
# 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/`.
|
|
- `postinst` — `systemctl enable --now secubox-x.service` (idempotent).
|
|
- `prerm` — `systemctl disable --now secubox-x.service`.
|
|
|
|
systemd unit boilerplate:
|
|
|
|
```ini
|
|
[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`:
|
|
|
|
```nginx
|
|
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:
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```text
|
|
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. `metactl` → `publishctl` 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.
|