@lamalibre/create-lamaste 4.2.0 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/package.json +2 -2
  2. package/scripts/bundle-vendor.js +13 -8
  3. package/scripts/lamaste-priv +205 -2
  4. package/src/index.js +8 -4
  5. package/src/lib/node-version.js +114 -0
  6. package/src/lib/panel-pin.js +41 -0
  7. package/src/lib/relay-services.js +15 -1
  8. package/src/lib/service-config.js +113 -0
  9. package/src/tasks/gatekeeper.js +8 -3
  10. package/src/tasks/node.js +34 -75
  11. package/src/tasks/redeploy.js +36 -0
  12. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  13. package/vendor/docs/00-introduction/quickstart.md +5 -5
  14. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  15. package/vendor/docs/01-concepts/authentication.md +6 -3
  16. package/vendor/docs/01-concepts/authorization.md +29 -17
  17. package/vendor/docs/01-concepts/certificates.md +3 -3
  18. package/vendor/docs/01-concepts/mtls.md +2 -2
  19. package/vendor/docs/01-concepts/nginx-reverse-proxy.md +8 -3
  20. package/vendor/docs/01-concepts/security-model.md +110 -35
  21. package/vendor/docs/01-concepts/tickets.md +2 -2
  22. package/vendor/docs/01-concepts/tunneling.md +4 -4
  23. package/vendor/docs/02-guides/agent-setup.md +34 -16
  24. package/vendor/docs/02-guides/certificate-management.md +2 -2
  25. package/vendor/docs/02-guides/cloud-provisioning.md +4 -4
  26. package/vendor/docs/02-guides/desktop-app-setup.md +11 -10
  27. package/vendor/docs/02-guides/installation.md +3 -3
  28. package/vendor/docs/02-guides/onboarding.md +2 -2
  29. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  30. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  31. package/vendor/docs/03-architecture/installer.md +4 -4
  32. package/vendor/docs/03-architecture/management-flow.md +1 -1
  33. package/vendor/docs/03-architecture/nginx-configuration.md +91 -26
  34. package/vendor/docs/03-architecture/overview.md +8 -8
  35. package/vendor/docs/03-architecture/panel-client.md +6 -6
  36. package/vendor/docs/03-architecture/panel-server.md +2 -2
  37. package/vendor/docs/03-architecture/state-management.md +2 -2
  38. package/vendor/docs/03-architecture/system-overview.md +1 -1
  39. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +407 -0
  40. package/vendor/docs/04-api-reference/app-host.md +360 -0
  41. package/vendor/docs/04-api-reference/audit-and-notifications.md +57 -0
  42. package/vendor/docs/04-api-reference/certificates.md +95 -38
  43. package/vendor/docs/04-api-reference/gatekeeper.md +5 -3
  44. package/vendor/docs/04-api-reference/lifecycle-hooks.md +333 -0
  45. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  46. package/vendor/docs/04-api-reference/overview.md +137 -111
  47. package/vendor/docs/04-api-reference/registry-and-oidc.md +8 -4
  48. package/vendor/docs/04-api-reference/tunnels.md +15 -13
  49. package/vendor/docs/05-operations/backup-and-restore.md +3 -1
  50. package/vendor/docs/05-operations/uninstalling.md +1 -1
  51. package/vendor/docs/05-operations/upgrades.md +25 -20
  52. package/vendor/docs/06-reference/config-files.md +195 -97
  53. package/vendor/docs/06-reference/glossary.md +3 -3
  54. package/vendor/docs/06-reference/installer-flags.md +13 -13
  55. package/vendor/docs/06-reference/ports-and-services.md +1 -1
  56. package/vendor/docs/06-reference/troubleshooting.md +33 -8
  57. package/vendor/docs/_index.json +20 -0
  58. package/vendor/docs/gatekeeper-design.md +22 -19
  59. package/vendor/docs/identity-directory-design.md +57 -56
  60. package/vendor/docs/package.json +1 -1
  61. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  62. package/vendor/gatekeeper/dist/cli/index.js +0 -0
  63. package/vendor/gatekeeper/dist/lib/types.d.ts +12 -0
  64. package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
  65. package/vendor/gatekeeper/dist/server/index.d.ts +2 -0
  66. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  67. package/vendor/gatekeeper/dist/server/index.js +47 -8
  68. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  69. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  70. package/vendor/gatekeeper/dist/server/routes/authz.js +233 -141
  71. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  72. package/vendor/gatekeeper/package.json +4 -3
  73. package/vendor/server/bin/lamaste-server.js +25 -1
  74. package/vendor/server/package.json +2 -2
  75. package/vendor/server/src/commands/audit.js +60 -0
  76. package/vendor/server/src/commands/panel-pin.js +40 -0
  77. package/vendor/server/src/commands/plugins.js +963 -12
  78. package/vendor/server/src/commands/push-install.js +317 -0
  79. package/vendor/server/src/commands/registry-credential.js +2 -1
  80. package/vendor/server/src/commands/uninstall.js +28 -1
  81. package/vendor/server/src/ndjson.js +4 -2
  82. package/vendor/server/src/panel-api.js +277 -34
  83. package/vendor/server/src/panel-identity.js +165 -0
  84. package/vendor/server-ui/dist/assets/index-BTlBt_cD.js +84 -0
  85. package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +1 -0
  86. package/vendor/server-ui/dist/index.html +2 -2
  87. package/vendor/serverd/package.json +2 -2
  88. package/vendor/serverd/src/index.js +56 -4
  89. package/vendor/serverd/src/lib/actor.js +24 -0
  90. package/vendor/serverd/src/lib/agent-jobs.js +330 -0
  91. package/vendor/serverd/src/lib/app-error.js +10 -0
  92. package/vendor/serverd/src/lib/audit.js +145 -0
  93. package/vendor/serverd/src/lib/hook-effects.js +123 -0
  94. package/vendor/serverd/src/lib/hook-pipelines.js +902 -0
  95. package/vendor/serverd/src/lib/migrations/state/0006_plugin_agent.sql +4 -0
  96. package/vendor/serverd/src/lib/migrations/state/0007_audit_and_notifications.sql +29 -0
  97. package/vendor/serverd/src/lib/migrations/state/0008_plugin_hooks.sql +70 -0
  98. package/vendor/serverd/src/lib/migrations/state/0009_plugin_agent_half.sql +53 -0
  99. package/vendor/serverd/src/lib/migrations/state/0010_plugin_routes_and_retained_data.sql +47 -0
  100. package/vendor/serverd/src/lib/migrations/state/0011_agent_job_signature.sql +14 -0
  101. package/vendor/serverd/src/lib/mtls.js +89 -8
  102. package/vendor/serverd/src/lib/nginx.js +298 -44
  103. package/vendor/serverd/src/lib/notifications.js +107 -0
  104. package/vendor/serverd/src/lib/plugin-hooks.js +560 -0
  105. package/vendor/serverd/src/lib/plugin-hosting.js +688 -0
  106. package/vendor/serverd/src/lib/plugin-lifecycle.js +603 -0
  107. package/vendor/serverd/src/lib/plugin-operations.js +295 -0
  108. package/vendor/serverd/src/lib/plugin-packages.js +249 -0
  109. package/vendor/serverd/src/lib/plugins.js +298 -68
  110. package/vendor/serverd/src/lib/push-install.js +88 -2
  111. package/vendor/serverd/src/lib/relay-hooks.js +100 -0
  112. package/vendor/serverd/src/lib/relay-signing.js +43 -0
  113. package/vendor/serverd/src/lib/request-hooks.js +79 -0
  114. package/vendor/serverd/src/lib/retained-data.js +99 -0
  115. package/vendor/serverd/src/lib/trust-proxy.js +34 -0
  116. package/vendor/serverd/src/lib/tunnel-hooks.js +88 -0
  117. package/vendor/serverd/src/lib/vhost-reconcile.js +260 -0
  118. package/vendor/serverd/src/middleware/twofa-session.js +11 -1
  119. package/vendor/serverd/src/routes/agent-forwarder.js +37 -0
  120. package/vendor/serverd/src/routes/management/agent-panel-authz.js +97 -0
  121. package/vendor/serverd/src/routes/management/agents.js +262 -1
  122. package/vendor/serverd/src/routes/management/audit.js +81 -0
  123. package/vendor/serverd/src/routes/management/certs.js +1 -15
  124. package/vendor/serverd/src/routes/management/plugins.js +569 -54
  125. package/vendor/serverd/src/routes/management/tunnels.js +26 -0
  126. package/vendor/serverd/src/routes/management.js +4 -0
  127. package/vendor/server-ui/dist/assets/index-B5Wp8lu-.js +0 -257
  128. package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +0 -1
@@ -0,0 +1,360 @@
1
+ # App Host
2
+
3
+ When an [agent-hosted plugin](agent-hosted-plugins.md)'s manifest names an agent package (`agent.package`), Lamaste installs the plugin's **agent half** onto the hosting agent itself: a system account, a hardened systemd unit, its directories and the system packages it declares. Installing them needs root, and `lamaste-agent` runs as an unprivileged user — so it goes through one root program on the agent, **`lamaste-apphost`**, which the agent's user may run through one sudoers rule. This page is the agent machine's side: enabling the helper, what it creates, the unit, the relay-signed jobs it runs, and the contract an agent half is written to. The relay's side — hosting, approval, updates, removal — is in [Agent-Hosted Plugins](agent-hosted-plugins.md#hosting-with-an-agent-half). The design is `plugin-development/design/agent-hosted-app-plugins.md` (Slice 3).
4
+
5
+ It is a page of its own because it documents another machine and another trust boundary: a root program, its files and its unit on the agent, which the relay's API pages only refer to.
6
+
7
+ ## In Plain English
8
+
9
+ The operator of an agent machine says once: _the panel may install plugins here_ (`sudo lamaste-agent apphost enable`). From then on, hosting a plugin from the panel installs its agent half on that machine with nothing done by hand: the agent's sync timer downloads exactly the package version the relay pinned, checks it byte for byte, and hands it to the helper, which unpacks it read-only under `/opt`, gives it an account of its own and a data directory under `/var/lib`, and starts it as a locked-down service. Plugin code never runs as root.
10
+
11
+ The helper does only what the relay **signed**. When it is enabled it remembers (pins) the relay's public key; every install, version switch and removal the relay asks for carries the relay's signature over exactly that job, and the helper refuses anything else — so a compromised agent account cannot make it install, switch or delete anything the relay did not ask for.
12
+
13
+ ## Enabling the helper
14
+
15
+ Once per agent machine (Linux with systemd; macOS agents cannot host an agent half Lamaste installs):
16
+
17
+ ```bash
18
+ sudo npm install -g @lamalibre/lamaste-agent@4.3.1 # a root-owned install
19
+ sudo lamaste-agent apphost enable # the agent runs as the user who ran sudo
20
+ sudo lamaste-agent apphost enable --user root # the agent runs as root: no sudoers rule
21
+ ```
22
+
23
+ `enable` refuses to act from an install another user could have changed: the package, the helper it ships and the core library it loads — and every directory above them — must be root-owned and writable by no group or other user. A user-level install (`nvm`, a home-directory prefix) is not; install it root-owned with `sudo npm install -g @lamalibre/lamaste-agent@4.3.1` and run `enable` from that install. It also needs `/usr/bin/node` (the helper's interpreter and the Node.js every agent half runs on — from the distribution or NodeSource), systemd as the system manager (`/run/systemd/system`), and `sudo` and `visudo` unless the agent runs as root.
24
+
25
+ What `enable` does:
26
+
27
+ 1. Installs the helper root:root 0755 at `/usr/local/sbin/lamaste-apphost` (written under a hidden temporary name, renamed into place).
28
+ 2. Writes `/etc/sudoers.d/lamaste-apphost` (root:root 0440) naming the agent's user — the one who ran `sudo`, or `--user`. The file is checked with `visudo -c` before it is renamed into place; an invalid rule is never live. With `--user root` no rule is written and an existing one is removed. **App hosting serves one OS user per machine**: while the rule names another user, `enable` is refused — that user's agents could no longer install, update or remove their agent halves, which would stay on the machine with nobody to manage them. Unhost or uninstall the plugins those agents host first (`apphost status` lists the agent halves installed), then `--force` replaces the rule (or removes it, for `--user root`).
29
+ 3. Checks that the helper answers (`status`) and that the agent's user can run it with `sudo -n`.
30
+ 4. Pins the relay key of every agent of that user on the machine (see [Pinning the relay key](#pinning-the-relay-key)), printing each label's key id to compare with the relay's. A failure here leaves the helper enabled — it runs no job until `sudo lamaste-agent apphost pin` succeeds.
31
+
32
+ The agent's next sync reports the helper to the relay in its [host facts](agent-hosted-plugins.md#host-facts); from then on the panel lists the agent as able to host plugins whose agent half Lamaste installs.
33
+
34
+ ### The sudoers rule
35
+
36
+ ```
37
+ Defaults!/usr/local/sbin/lamaste-apphost env_reset
38
+ Defaults!/usr/local/sbin/lamaste-apphost !setenv
39
+ Defaults!/usr/local/sbin/lamaste-apphost env_keep -= "NODE_OPTIONS NODE_PATH NODE_EXTRA_CA_CERTS NODE_ICU_DATA NODE_REPL_EXTERNAL_MODULE NODE_REDIRECT_WARNINGS NODE_V8_COVERAGE NODE_COMPILE_CACHE NODE_PRESERVE_SYMLINKS NODE_DEBUG NODE_DEBUG_NATIVE NODE_TLS_REJECT_UNAUTHORIZED"
40
+ Defaults!/usr/local/sbin/lamaste-apphost env_delete += "NODE_OPTIONS NODE_PATH …the same list…"
41
+ <user> ALL=(root) NOPASSWD: /usr/local/sbin/lamaste-apphost
42
+ ```
43
+
44
+ Any arguments are allowed: the helper validates every argument and derives every path itself. The environment is reset, and the Node.js variables that load code, change module resolution or write files can never reach it, whatever else sudoers says.
45
+
46
+ ### `lamaste-agent apphost`
47
+
48
+ | Command | Run as | Does |
49
+ | ----------------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50
+ | `sudo lamaste-agent apphost enable [--user <name>] [--force]` | root | Install the helper and the rule, pin each agent's relay key (above). Refused while the rule names another user, unless `--force` |
51
+ | `sudo lamaste-agent apphost pin [--label <label>] [--user <name>]` | root | Pin the relay key again — after a relay rotated its key, or an agent was enrolled again. The user defaults to the one the rule names, else root. Prints each label's key id and the one pinned before (`was …`); exits 1 when a label failed |
52
+ | `lamaste-agent apphost status [--json]` | the agent's user or root | The helper (protocol, Node.js, whether it is the version this install ships), the pinned relay keys, the agent halves installed (version, previous version, unit state, owner). As root also the user the rule names |
53
+ | `lamaste-agent apphost relay-keys [--label <label>] [--json]` | the agent's user | What each agent's relay publishes now, fetched over the agent's pinned mTLS channel, and whether it is the key pinned |
54
+ | `sudo lamaste-agent apphost remove <route> [--purge] [--label <label>]` | root | Remove an agent half as the operator, with no relay involved — for an agent its relay can no longer reach. Keeps its data unless `--purge`. Acts as the agent the owner record names; `--label` names it when there is no record. Unhost or uninstall the plugin on the relay too |
55
+ | `sudo lamaste-agent apphost disable [--force]` | root | Remove the rule (first), the helper and the pinned keys. Refused while agent halves are installed; `--force` removes them first, keeping their data |
56
+
57
+ All are Linux only. `enable`, `pin`, `remove` and `disable` refuse to run from an install that is not root-owned, as `enable` does.
58
+
59
+ ## The helper
60
+
61
+ `/usr/local/sbin/lamaste-apphost` is a self-contained CommonJS script (Node.js built-ins only, `#!/usr/bin/node`) that reads nothing the agent's user can write. Every name and path derives from validated arguments; content — a tarball, a service spec, a plugin config, a signature — arrives on stdin, so neither a tarball nor a secret is in the sudo log, and nothing read from stdin is ever echoed. One mutating command runs at a time (`/run/lamaste-apphost.lock`, holding the owner's PID; a lock whose process is gone is taken over).
62
+
63
+ | Command | stdin | Signed | Does |
64
+ | ----------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
65
+ | `install <label> <route> <version> <integrity>` | the signature (one line of JSON), the service spec (one line of JSON), then the package `.tgz` | yes | Install that version of the agent half and make it current |
66
+ | `config <label> <route>` | the plugin config (JSON) | no | Write the service's copy of the plugin config; start the unit, restart it when the port changed (a rotated secret needs no restart). Exit 3 when the agent half is not installed |
67
+ | `activate <label> <route> <version>` | the signature | yes | Point `current` at a kept version, restore that version's unit, restart |
68
+ | `remove <label> <route> [--purge]` | the signature | yes | Stop and disable the unit; remove unit, code and config. `--purge` also deletes the data directory, the account and the owner record |
69
+ | `status [<route>]` | — | no | `{ protocol, node, apps: { <route>: { versions, current, previous, unit, owner } } }` — `node` is the Node.js the helper, and so every agent half, runs on |
70
+
71
+ `<label>` is the agent's label on this machine. `config` and `status` are not signed: the agent's user holds the plugin config already, and `status` changes nothing. An operator's `sudo lamaste-agent apphost remove|disable` loads the helper into its own root process and calls its removal directly — operator authority comes only from code already running as root, never from the helper's command line.
72
+
73
+ The mutating commands print `{ "ok": true, "version": "1.2.0", "unit": "active" }` (`version`: what `current` points at, `null` after `remove`; `unit`: `active`, `inactive` or `absent`). Errors are one line on stderr, `lamaste-apphost: …`.
74
+
75
+ | Exit | Meaning |
76
+ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
77
+ | 0 | Done |
78
+ | 1 | Failed |
79
+ | 2 | Refused: bad arguments or input |
80
+ | 3 | That agent half is not installed (`config`, `activate`) |
81
+ | 4 | The route belongs to another agent or another package |
82
+ | 5 | Refused: not a job the relay signed for this agent and command — no signature, no pinned key, another key, a bad, expired or reused one, or one for other arguments |
83
+
84
+ ### What `install` checks
85
+
86
+ In this order; nothing of the agent half is written before they pass:
87
+
88
+ 1. The arguments: the route (`^[a-z0-9]([a-z0-9-]{0,98}[a-z0-9])?$`), the label, a semver version without build metadata, a SHA-512 SRI integrity.
89
+ 2. The relay's signature on the first line (see [Signed app jobs](#signed-app-jobs)): an `app-install` job for this agent and route whose spec — package, version, integrity, service, system packages — is exactly the arguments and the spec line.
90
+ 3. The tarball's SHA-512 against the integrity; then the archive itself, unpacked in memory: POSIX ustar as npm writes it, valid checksums, only regular files and directories, every path inside `package/` with no `.`, `..` or empty segment, each at most once, nothing after the end marker. Links, devices, FIFOs and GNU or global PAX extensions are refused.
91
+ 4. systemd is the system manager.
92
+ 5. The job id has not run before — it is recorded now, so the job is spent whatever follows.
93
+ 6. **Ownership.** The route's owner record names this label and this package; a route with data or code on the machine but no owner record is not taken over.
94
+ 7. At most 50 agent halves per machine; enough free space in `/opt/lamalibre/apps` for the unpacked package, 4 KiB per entry and a 64 MiB margin.
95
+ 8. **The bundled rule**, on the unpacked tree: `package.json` names the spec's package at the version; no non-empty `dependencies` or `optionalDependencies`, no `bundleDependencies`; no `preinstall`, `install` or `postinstall` script; no `binding.gyp`; the service entry is a file in the package. Every file becomes root-owned 0644, every directory 0755 — no executable or set-id bit, nothing the agent half can change.
96
+
97
+ Then: the approved system packages it lacks are installed (`apt-get install -y --no-install-recommends`, Debian and Ubuntu only), the account, the owner record, the data and config directories are created, the version is moved into place, the unit is written, `current` switched, systemd reloaded and the unit (re)started — once its plugin config is there. A failure before the version is in place takes back what this run created; a failure after it restores the version that ran before, or for a first install removes everything. The current and previous versions are kept; older ones are deleted.
98
+
99
+ ### What it creates
100
+
101
+ | Path or name | Owner, mode | What |
102
+ | --------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
103
+ | `lamaste-app-<route>` (user and group) | system account | The agent half's account: `--system`, no home (`/nonexistent`), shell `/usr/sbin/nologin`, comment `lamaste-app route=<route>` |
104
+ | `/opt/lamalibre/apps/<route>/<version>/` | root, dirs 0755, files 0644 | The package (`package/`) and `apphost.json`, the spec it was installed with (restored on `activate`) |
105
+ | `/opt/lamalibre/apps/<route>/current`, `previous` | root (symlinks) | The running version and the one before it |
106
+ | `/var/lib/lamalibre/apps/<route>/` | the account, 0700 | The agent half's data — kept across versions and on removal, deleted only by a purge |
107
+ | `/etc/lamalibre/apps/<route>/` | root:\<account's group\>, 0750 | The agent half's configuration directory |
108
+ | `/etc/lamalibre/apps/<route>/plugin-config.json` | the account, 0600 | Port and installation secret (`LAMASTE_APP_CONFIG`) |
109
+ | `/etc/systemd/system/lamalibre-app-<route>.service` | root, 0644 | The unit (below) |
110
+ | `/var/lib/lamalibre/apps/.owners/<route>.json` | root 0644, directory root 0755 | The owner record: `{ route, package, label, since }` — outside the data the agent half can write. Kept on removal, deleted by a purge |
111
+ | `/var/lib/lamalibre/apps/.jobs/used.json` | root 0600, directory root 0700 | The ids of the signed jobs that ran, each kept until its signature expires |
112
+ | `/var/lib/lamalibre/apps/.trash/` | root, 0700 | A purged data directory is moved here, out of its account's reach, and deleted |
113
+ | `/etc/lamalibre/apphost/relays/<label>.json` | root 0644, directories root 0755 | The relay key pinned for agent `<label>` |
114
+ | `/opt/lamalibre/apps/.stage-<hex>-<route>/` | root, 0700 | Unpacking, while an install runs |
115
+
116
+ A route never starts with `.`, so the dot-directories cannot collide with a data directory.
117
+
118
+ **Account names.** `lamaste-app-<route>` while that is at most 30 characters (routes of up to 18 characters) — readable in `ps`, the journal and file listings. A longer route gets `lamaste-app-<first 2 characters>-<16 hex digits of SHA-256(route)>`, always exactly 31 characters (systemd's limit for `User=`): `samplepark-management` runs as `lamaste-app-sa-3e26a99e45bb5463`. The lengths keep the two forms apart, and 64 bits of hash make crafting a route whose name equals another's a 2^64 search. The account's comment names its route; an account of the same name whose comment names another route — or none — is never reused, given files or deleted (exit 4), and neither is a group of that name without the account. An account with uid or gid 0 is refused.
119
+
120
+ ## The unit
121
+
122
+ For plugin `market-indicators`, with `memoryMax: "256M"`:
123
+
124
+ ```ini
125
+ # Managed by lamaste-apphost: the agent half of Lamaste app plugin market-indicators.
126
+ [Unit]
127
+ Description=Lamaste app plugin market-indicators (agent half)
128
+ After=network-online.target
129
+ Wants=network-online.target
130
+
131
+ [Service]
132
+ Type=simple
133
+ User=lamaste-app-market-indicators
134
+ Group=lamaste-app-market-indicators
135
+ ExecStart=/usr/bin/node /opt/lamalibre/apps/market-indicators/current/package/dist/agentd.mjs
136
+ Environment=LAMASTE_APP_CONFIG=/etc/lamalibre/apps/market-indicators/plugin-config.json
137
+ Environment=LAMASTE_APP_DATA_DIR=/var/lib/lamalibre/apps/market-indicators
138
+ Environment=NODE_ENV=production
139
+ WorkingDirectory=/var/lib/lamalibre/apps/market-indicators
140
+ Restart=on-failure
141
+ RestartSec=5
142
+ NoNewPrivileges=yes
143
+ ProtectSystem=strict
144
+ ReadWritePaths=/var/lib/lamalibre/apps/market-indicators
145
+ ReadOnlyPaths=/run
146
+ ProtectHome=yes
147
+ PrivateTmp=yes
148
+ PrivateDevices=yes
149
+ TemporaryFileSystem=/dev/shm:nosuid,nodev,noexec
150
+ PrivateIPC=yes
151
+ RemoveIPC=yes
152
+ ProtectProc=invisible
153
+ ProtectKernelTunables=yes
154
+ ProtectKernelModules=yes
155
+ ProtectKernelLogs=yes
156
+ ProtectControlGroups=yes
157
+ ProtectClock=yes
158
+ ProtectHostname=yes
159
+ RestrictSUIDSGID=yes
160
+ RestrictRealtime=yes
161
+ RestrictNamespaces=yes
162
+ RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
163
+ LockPersonality=yes
164
+ SystemCallArchitectures=native
165
+ SystemCallFilter=@system-service
166
+ SystemCallErrorNumber=EPERM
167
+ CapabilityBoundingSet=
168
+ UMask=0077
169
+ MemoryMax=256M
170
+ StandardOutput=journal
171
+ StandardError=journal
172
+
173
+ [Install]
174
+ WantedBy=multi-user.target
175
+ ```
176
+
177
+ The unit is written from a fixed template; it holds paths, never a secret.
178
+
179
+ | Directive | Why |
180
+ | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
181
+ | `User=` / `Group=` the agent half's account | Plugin code never runs as root or as the agent's user |
182
+ | `NoNewPrivileges`, `CapabilityBoundingSet=` (empty), `RestrictSUIDSGID` | No privilege gained through set-id binaries or capabilities; no privileged port, no `chown` |
183
+ | `ProtectSystem=strict`, `ReadWritePaths=` the data directory, `ReadOnlyPaths=/run`, `ProtectHome`, `PrivateTmp`, `UMask=0077` | The whole file system read-only but the data directory and a private `/tmp`; `/run` too (strict leaves it writable, and `/run/lock` is world-writable); homes invisible; new files private |
184
+ | `PrivateDevices`, `TemporaryFileSystem=/dev/shm`, `PrivateIPC`, `RemoveIPC` | No device nodes, an empty `/dev/shm` it cannot write (PrivateDevices alone binds the machine's, which every user can write; a bundled Node agent half has no use for POSIX shared memory), no shared IPC namespace, its IPC objects removed when it stops |
185
+ | `ProtectProc=invisible` | Other users' processes are not visible in `/proc` |
186
+ | `ProtectKernelTunables`, `ProtectKernelModules`, `ProtectKernelLogs`, `ProtectControlGroups`, `ProtectClock`, `ProtectHostname` | The kernel's tunables and modules, the cgroup tree, the clock and the hostname cannot be changed; the kernel log cannot be read |
187
+ | `RestrictRealtime`, `RestrictNamespaces`, `LockPersonality`, `SystemCallArchitectures=native` | No realtime scheduling, no new namespaces, no personality or foreign-ABI tricks |
188
+ | `SystemCallFilter=@system-service`, `SystemCallErrorNumber=EPERM` | Only the system calls services need; a call outside the set fails with `EPERM` instead of killing the process (libuv probes `io_uring` and falls back to its thread pool) |
189
+ | `RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX` | IP and Unix sockets only. Outbound network stays the application's (an agent half fetches what it must); it binds loopback |
190
+ | `MemoryMax=` | From `agent.service.memoryMax`; left out when the manifest sets none |
191
+ | `Restart=on-failure`, `WantedBy=multi-user.target` | Restarted 5 s after a crash; started at boot |
192
+
193
+ Not used, on purpose:
194
+
195
+ - **`MemoryDenyWriteExecute`** — V8's JIT writes the machine code it runs, which the setting forbids.
196
+ - **`ProcSubset=pid`** — Node.js reads `/proc/cpuinfo`, `/proc/stat` and `/proc/meminfo` for `os.cpus()`, `os.totalmem()` and `os.freemem()`, which applications use to size worker pools; hiding them breaks them quietly.
197
+ - **`AF_NETLINK`** is not among the address families, so `os.networkInterfaces()` fails. An agent half binds `127.0.0.1` and needs no interface list.
198
+ - **`IPAddressDeny`** — the application's own outbound calls are its business (market data, a mail relay).
199
+
200
+ The journal has the agent half's output: `journalctl -u lamalibre-app-<route>`.
201
+
202
+ ## Signed app jobs
203
+
204
+ The helper runs as root on the agent's behalf, so whatever the agent's user could hand it — a service spec naming system packages, a version to activate, a purge — it would otherwise do as root. Approval of what an agent half brings happens on the relay ([approval](lifecycle-hooks.md#approval)); the relay therefore **signs** every app job when it creates it, and the helper refuses anything it did not sign. The agent carries the signature from `agent-config` to the helper and can neither make nor change one.
205
+
206
+ The relay signs with its Ed25519 relay key — the key it already signs [relay assertions](../01-concepts/security-model.md#5c-the-agent-daemon-lamaste-agentd-and-relay-assertions) with (`/etc/lamalibre/lamaste/relay-signing-key.json`). The signed bytes are the canonical JSON (keys sorted at every level, no whitespace) of the envelope:
207
+
208
+ ```json
209
+ {
210
+ "v": 1,
211
+ "typ": "lamaste.apphost.job",
212
+ "kid": "…32 hex: the relay key's id…",
213
+ "agent": "vm1",
214
+ "route": "market-indicators",
215
+ "jobId": "…uuid…",
216
+ "kind": "app-install",
217
+ "payload": {
218
+ "spec": {
219
+ "package": "@lamalibre/market-indicators-agent",
220
+ "version": "1.2.0",
221
+ "integrity": "sha512-…",
222
+ "service": { "entry": "dist/agentd.mjs", "memoryMax": "256M" },
223
+ "systemPackages": ["sqlite3"]
224
+ }
225
+ },
226
+ "issuedAt": "2026-10-16T10:00:00.000Z",
227
+ "expiresAt": "2026-10-16T10:11:30.000Z"
228
+ }
229
+ ```
230
+
231
+ | Kind | Payload | Helper command |
232
+ | -------------- | ---------------------- | ---------------------------------------------------------------------------------------- |
233
+ | `app-install` | `{ spec }` | `install` — the spec, version and integrity must equal its arguments and stdin |
234
+ | `app-activate` | `{ version, package }` | `activate` — the version must equal its argument; the route's owner must be that package |
235
+ | `app-remove` | `{ purge, package }` | `remove` — the purge flag must equal `--purge`; the route's owner must be that package |
236
+
237
+ `agent` is the label the relay knows the agent by; `expiresAt` is the job's deadline (its timeout plus 90 s of delivery). The signature travels as `{ "envelope": base64url(canonical JSON), "sig": base64url(Ed25519 signature) }`, listed with the job in `agent-config`. A relay assertion signs the base64url text of its payload while an app job signs JSON bytes, which start with `{` — never a base64url character — so neither can be taken for the other.
238
+
239
+ The helper verifies, with Node.js built-ins only (it trusts no code the agent's user can change):
240
+
241
+ - the line is exactly `{ envelope, sig }`, both canonical base64url, and the envelope is canonical JSON — what was signed is exactly what is read;
242
+ - every envelope field and no other; a validity of at most one hour;
243
+ - the key id is the one pinned for `<label>`, and the signature verifies with it;
244
+ - `agent` is the relay's label for this agent (from the pin), `route` and `kind` are the command's;
245
+ - `issuedAt` is not more than 5 s ahead of the machine's clock, `expiresAt` has not passed;
246
+ - the payload is what the command was asked;
247
+ - the job id has not run before: ids are kept in `/var/lib/lamalibre/apps/.jobs/used.json` until they expire, so a job runs once (at most 10 000 within the hour).
248
+
249
+ Any failure is exit 5, with a message that says what to do — `re-pin the relay key: sudo lamaste-agent apphost pin` for a key the relay rotated or an agent enrolled again, `check the clocks of agent and relay` for a signature from the future.
250
+
251
+ ### Pinning the relay key
252
+
253
+ `sudo lamaste-agent apphost enable` and `sudo lamaste-agent apphost pin` fetch each agent's relay key **as the agent's user** (`sudo -n -u <user> -H -- … lamaste-agent apphost relay-keys --json`), over that agent's own pinned mTLS channel (`GET /api/agents/me/relay-keys`) — root never handles the agent's certificate or its password. An agent without a pinned panel certificate is not pinned: its channel could be anyone's. Root accepts the answer only as it validates it — a label of that user's agents, an `https` panel URL, an Ed25519 key whose id is derived from the key (the first 16 bytes of the SHA-256 of its DER SubjectPublicKeyInfo, hex), the relay's label for the agent — and writes the first key, the one the relay signs with:
254
+
255
+ ```json
256
+ {
257
+ "v": 1,
258
+ "label": "vm1",
259
+ "agent": "vm1",
260
+ "kid": "…32 hex…",
261
+ "publicKey": "…base64 DER SPKI…",
262
+ "panelUrl": "https://203.0.113.42:9292",
263
+ "pinnedAt": "2026-10-16T10:00:00.000Z"
264
+ }
265
+ ```
266
+
267
+ The helper reads a pin only from a root-owned file (opened without following links) in root-owned directories nobody else may write.
268
+
269
+ **Whose labels.** Root checks each label against the agent user's own agent registry, which it reads itself: `~/.lamalibre/lamaste/agents.json` in that user's home as the user database names it (`getent passwd` — the home `sudo -H` gives the CLI that fetched the keys), only as a regular file that user owns, not through a link and at most 1 MiB. A label the registry does not list is not pinned. A pin is per label for the whole machine and the helper serves root as well as the rule's user, so for any user but root a label root's own agents use is refused too (`pin it with --user root`) — one user's answer never replaces the key another's agent verifies its jobs with. The registry is that user's file: the check keeps its pins to the agents it set up, and away from root's; it does not make its answer more trustworthy than its account (below).
270
+
271
+ **Trust on first use.** The pin is as trustworthy as the agent's account and its channel at the moment `enable` or `pin` runs. Compare it with the relay's key once:
272
+
273
+ - on the relay: the panel logs the key id at start-up (`journalctl -u lamalibre-lamaste-serverd | grep 'signing key ready'`), and `GET /api/agents/me/relay-keys` with the admin certificate answers `keys[0].kid`;
274
+ - on the agent: `enable` and `pin` print each label's pinned id in full, and for a re-pin the id pinned before (`was …`); `sudo lamaste-agent apphost status` lists them; `lamaste-agent apphost relay-keys` shows what the relay publishes now and whether it is pinned;
275
+ - in the panel: the Certificates page and the host dialog show the first 12 hex digits of the id each agent reports pinned, and an agent whose pin is not the relay's key is not eligible (`re-pin the relay key`).
276
+
277
+ **Rotation.** After the relay's signing key is replaced, every agent's helper refuses that relay's jobs until `sudo lamaste-agent apphost pin` runs on the agent; the panel shows those agents as not eligible meanwhile. An agent enrolled again under another relay label needs a re-pin too.
278
+
279
+ ## On the agent: how a job arrives
280
+
281
+ The relay lists app jobs in `GET /api/tunnels/agent-config` (`agentJobs`, see [Agent-Hosted Plugins](agent-hosted-plugins.md#what-the-agent-does-with-it)). The agent's sync timer runs them one after another after converge, outside the agent lock:
282
+
283
+ - **A job is checked first.** An app job without a signature, with a malformed one, or whose signature does not describe exactly that job (id, route, kind, deadline, payload) is never run and is reported failed.
284
+ - **`app-install`** waits for the next sync until the plugin config has arrived (the relay issues it before it lists the job). Then the agent checks the helper — enabled, answering, protocol ≥ 1, the route not another agent's — fetches a registry token from the panel (`GET /api/agents/me/registry-credential`), runs `npm pack <package>@<version> --ignore-scripts` into a fresh 0700 directory under `~/.lamalibre/lamaste/tmp/agent/` with that token in a throw-away userconfig, refuses a tarball over 128 MiB or one whose SHA-512 is not the pinned integrity, and pipes signature, spec and tarball to `lamaste-apphost install`. Under the agent lock it then records the route in `apphost-state.json` and hands the service its plugin config (`lamaste-apphost config`).
285
+ - **`app-activate`** and **`app-remove`** hand the signature to `activate` / `remove`. Removing with the helper disabled succeeds (`disable` removed every agent half first) unless it is a purge.
286
+ - **Failures are reported, never undone here.** The agent cannot sign, so it never removes or switches an agent half on its own: the relay, which recovers from every install it dispatched, sends the signed removal or activation. A result is kept in `app-job-results.json` until the relay took it — the helper runs a signed job once, so a report that failed is made again on the next sync without running the job again.
287
+ - **Plugin configs.** For a route recorded in `apphost-state.json`, converge hands a fetched config to the service (`lamaste-apphost config`) **before** acknowledging it, so the relay never signs with a secret the service does not hold; a rotation therefore needs no restart. When the record cannot be read nothing is delivered or acknowledged until the sync rebuilds it from the helper's owner records.
288
+
289
+ `sync.log` records `app install ran: <route>`, `app install refused (not signed by the relay): <route>` and `plugin config delivered to its service: <route>`. The sync unit allows one run 20 minutes (`TimeoutStartSec=20min`): an install may take its full 10 minutes.
290
+
291
+ ## Several agents on one machine
292
+
293
+ Agents of different relays may share a machine and its helper — under the one user the rule names, or root (`enable` for another user is refused unless `--force`). Each label has its own pin, and each relay's jobs name its own agent. A route belongs to the agent that first installed it (its owner record); another agent, or another package, is refused that route — and the data kept with it — until the owner purges it (exit 4). The sync also leaves a route alone that another agent's `apphost-state.json` lists, and delivers no config while two agents claim one.
294
+
295
+ ## Writing an agent half
296
+
297
+ The manifest names the agent package and how it runs (see [the manifest](agent-hosted-plugins.md#the-manifest)):
298
+
299
+ ```json
300
+ "agent": {
301
+ "requires": { "node": ">=22", "os": ["linux"], "systemPackages": ["sqlite3"] },
302
+ "package": "@lamalibre/market-indicators-agent",
303
+ "service": { "entry": "dist/agentd.mjs", "memoryMax": "256M" }
304
+ }
305
+ ```
306
+
307
+ The contract:
308
+
309
+ - **One bundled package.** An `@lamalibre/` package published at the plugin's version, whose registry entry carries a SHA-512 integrity. Bundle everything into it (esbuild): no runtime `dependencies` or `optionalDependencies`, no `bundleDependencies`, no `preinstall`/`install`/`postinstall` script, no native addon (`binding.gyp`). The tarball the relay pins is then the whole code that runs; nothing is compiled or fetched as root.
310
+ - **The entry** is a relative `.js`, `.mjs` or `.cjs` path in the package, run as `/usr/bin/node <entry>` — the Node.js of the helper, which `agent.requires.node` is checked against.
311
+ - **Files are read-only.** Every file is installed root-owned 0644 — no executable bit, so the agent half spawns no program of its own package directly; directories 0755.
312
+ - **It writes only its data directory**, `LAMASTE_APP_DATA_DIR` (also its working directory, 0700), and its private `/tmp`. Use `node:sqlite` for a database.
313
+ - **Its config** is `LAMASTE_APP_CONFIG` (0600): `{ plugin, port, secret, issuedAt, previousSecret?, previousIssuedAt?, fetchedAt, acknowledged }`. Listen on `127.0.0.1:<port>` and verify every `/admin/*` request:
314
+
315
+ ```js
316
+ import { createCallerAssertionVerifier } from '@lamalibre/lamaste@4.3.1/agent'; // bundled
317
+
318
+ const verifier = createCallerAssertionVerifier({
319
+ configPath: process.env.LAMASTE_APP_CONFIG,
320
+ plugin: 'market-indicators',
321
+ });
322
+ const port = await verifier.port();
323
+ ```
324
+
325
+ The verifier re-reads the file when a signature does not verify, so a rotated secret needs no restart; a changed port restarts the unit.
326
+
327
+ - **Lifecycle hooks** are answered at `POST /admin/hooks/<stage>` ([agent hooks](lifecycle-hooks.md#agent-hooks)); the agent's own `install:pre-tunnel` reaches the service on its loopback once it runs.
328
+ - **Limits of the sandbox:** no capabilities (no port below 1024), IP and Unix sockets only, no `os.networkInterfaces()`, only its own processes in `/proc`. Its output goes to the journal; `NODE_ENV=production`.
329
+ - **System packages** (`agent.requires.systemPackages`) are installed with `apt-get` after approval, on Debian and Ubuntu only.
330
+ - **Name** the plugin by its route: a plugin with an agent half is named exactly its route (its packages keep their names), and the route starts and ends with a letter or digit.
331
+
332
+ ## Retained data
333
+
334
+ Removing an agent half without purge — unhosting, uninstalling, a rolled-back install — keeps its data directory, its account and its owner record on the agent. The relay keeps a record of it, by the plugin's name, so it can still be purged after the plugin is uninstalled: the Plugins page shows it under **Retained data** on the plugin's card, or — once the plugin is uninstalled — under **Retained data of uninstalled plugins**; `lamaste-server plugins retained-data [<name>]` lists it ([the API](agent-hosted-plugins.md#retained-data)). Hosting the same agent package on that agent again takes the data back; another package is refused (`retained_data_conflict`) until it is purged:
335
+
336
+ - from the relay — **Purge** on the Plugins page, or `lamaste-server plugins purge-data <name> --agent <label>` — an `app-remove` job with purge, gated by push install like any removal and followed like any operation;
337
+ - on the machine — for an agent the relay can no longer reach — `sudo lamaste-agent apphost remove <route> --purge`, then **Forget** on the Plugins page or `lamaste-server plugins forget-data <name> --agent <label>` to drop the relay's record. Forgetting touches nothing on the agent.
338
+
339
+ ## Limits
340
+
341
+ | What | Limit |
342
+ | ------------------------------------ | ----------------------------------------------------------------- |
343
+ | Agent halves per machine | 50 |
344
+ | Package tarball | 128 MiB; 512 MiB unpacked; 50 000 entries; `package.json` 1 MiB |
345
+ | Free space in `/opt/lamalibre/apps` | The unpacked package + 4 KiB per entry + 64 MiB |
346
+ | System packages | 50, Debian names |
347
+ | Plugin config, service spec | 64 KiB each |
348
+ | Signed job | Valid at most one hour; envelope 32 KiB; 5 s clock skew |
349
+ | Job timeouts on the agent | install 10 min, activate 2 min, remove 5 min (plus 90 s delivery) |
350
+ | Waiting for another helper operation | install 5 min, remove 2 min, config and activate 1 min |
351
+
352
+ ## What it does not do
353
+
354
+ - **`lamaste-agent uninstall` refuses** while agent halves are installed for the agents it would remove — they would run on with no tunnel and nobody to remove them. Unhost or uninstall those plugins on the panel first (the agent removes them on its next sync), or `sudo lamaste-agent apphost disable --force` (their data is kept).
355
+ - **System packages are never removed** — not on removal, not on purge.
356
+ - **No other package managers:** system packages are installed on Debian and Ubuntu only; elsewhere a plugin that declares them cannot be hosted.
357
+ - **macOS agents** cannot host an agent half Lamaste installs; an agent half installed by hand is still hosted the 2a way.
358
+ - **Settings and secrets** of an agent half (slice 4) are not delivered yet; the plugin config carries the port and the installation secret only.
359
+
360
+ See [Config Files](../06-reference/config-files.md#agent-machine-the-app-host) for every file with its owner and mode, and [Security Model](../01-concepts/security-model.md#5d-the-app-host-on-agents) for the trust model.
@@ -0,0 +1,57 @@
1
+ # Audit Log & Notifications
2
+
3
+ The panel keeps an **audit log** of administrative decisions — approving a plugin's relay hooks, switching auto-approve, hosting and removing a hosting, installing, updating and uninstalling a plugin — and of what plugins' [lifecycle hooks](lifecycle-hooks.md) report through the `audit` effect. **Notifications** are messages for the relay's administrators: a hook's `notify-admins` effect, an update waiting for approval (with why auto-approve did not cover it), a rule pack updated with Lamaste. Both live in `state.db` and hold metadata only — never a request or response body or a secret.
4
+
5
+ All endpoints are admin only.
6
+
7
+ ## `GET /api/audit`
8
+
9
+ Newest first. Query: `plugin`, `action` (exact, or a prefix ending in `.` such as `hooks.`), `before` (an entry id, to page back), `limit` (1–200, default 50).
10
+
11
+ ```json
12
+ {
13
+ "entries": [
14
+ {
15
+ "id": 42,
16
+ "at": "2026-10-15T12:00:00.000Z",
17
+ "actor": "admin",
18
+ "action": "hooks.approved",
19
+ "plugin": "fx",
20
+ "details": { "digest": "…", "version": "1.0.0", "for": "installed", "certSerial": "0A1B" }
21
+ }
22
+ ]
23
+ }
24
+ ```
25
+
26
+ | Actor | Who |
27
+ | ---------------- | ------------------------------------------------------------ |
28
+ | `admin` | an administrator certificate (`certSerial` tells two apart) |
29
+ | `agent:<label>` | an agent certificate |
30
+ | `plugin:<route>` | a plugin's hook, through the `audit` effect (`hook.<event>`) |
31
+ | `auto-approve` | a plugin's auto-approve |
32
+ | `lamaste` | the panel itself |
33
+
34
+ Actions written by Lamaste: `plugin.installed`, `plugin.uninstalled`, `plugin.hosted`, `plugin.unhosted`, `plugin.updated`, `plugin.update.reverted`, `plugin.update.awaiting-approval`, `plugin.update.discarded`, `hooks.approved`, `hooks.auto-approve.enabled|disabled`, `hooks.auto-approve.withheld`, `hooks.rules-updated`, `hooks.effects-dropped`, and for an [agent half Lamaste installs](app-host.md):
35
+
36
+ | Action | When |
37
+ | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
38
+ | `plugin.agent-half.installed` | The agent reported the pinned version installed and its unit active — `agentLabel`, `package`, `version`, `integrity`, `phase`, `retainedDataReused` |
39
+ | `plugin.agent-half.activated` | A failed update switched the agent back to the version before — `version`, `from` |
40
+ | `plugin.agent-half.removed` | Removed from the agent — `version`, `purge`, `phase` (`uninstall` or `rollback`) |
41
+ | `plugin.agent-half.purged` | Retained data purged on the agent — `route`, `package` |
42
+ | `plugin.agent-half.retained-data.forgotten` | The relay's record of retained data dropped without touching the agent |
43
+ | `hooks.auto-approve.withheld` | Auto-approve did not cover an update because its agent half changes what runs as root — `reasons` |
44
+
45
+ Details are capped at 4 KiB (strings at 300 characters, three levels deep). Entries are kept 180 days, at most 50 000.
46
+
47
+ ## `GET /api/notifications`
48
+
49
+ Query: `unread=true`, `before`, `limit`. Answers `{ notifications: [{ id, at, source, level, message, read, readAt }], unread }`. `level` is `info`, `warning` or `critical`; `source` is `lamaste` or `plugin:<route>`. Messages are one line of plain text (500 characters) — the panel renders them as text. Kept 30 days, at most 1 000.
50
+
51
+ ## `POST /api/notifications/read`
52
+
53
+ `{ "ids": [1, 2] }` or `{ "all": true }` → `{ ok, changed }`. Read state is shared by all administrators.
54
+
55
+ ## Panel and CLI
56
+
57
+ The panel's **Audit** page shows the notifications and the log with plugin and action filters. `lamaste-server audit [--plugin <name>] [--action <action|prefix.>] [--limit <n>]` prints the log.