@lamalibre/create-lamaste 4.2.0 → 4.3.0
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.
- package/package.json +2 -2
- package/scripts/lamaste-priv +205 -2
- package/src/index.js +4 -4
- package/src/lib/node-version.js +114 -0
- package/src/lib/relay-services.js +15 -1
- package/src/lib/service-config.js +113 -0
- package/src/tasks/gatekeeper.js +8 -3
- package/src/tasks/node.js +34 -75
- package/src/tasks/redeploy.js +36 -0
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +6 -3
- package/vendor/docs/01-concepts/authorization.md +29 -17
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +8 -3
- package/vendor/docs/01-concepts/security-model.md +98 -34
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +34 -16
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +91 -26
- package/vendor/docs/03-architecture/overview.md +8 -8
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +407 -0
- package/vendor/docs/04-api-reference/app-host.md +360 -0
- package/vendor/docs/04-api-reference/audit-and-notifications.md +57 -0
- package/vendor/docs/04-api-reference/certificates.md +95 -38
- package/vendor/docs/04-api-reference/gatekeeper.md +5 -3
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +333 -0
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +137 -111
- package/vendor/docs/04-api-reference/registry-and-oidc.md +7 -3
- package/vendor/docs/04-api-reference/tunnels.md +15 -13
- package/vendor/docs/05-operations/backup-and-restore.md +1 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +25 -20
- package/vendor/docs/06-reference/config-files.md +190 -97
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +33 -8
- package/vendor/docs/_index.json +20 -0
- package/vendor/docs/gatekeeper-design.md +22 -19
- package/vendor/docs/identity-directory-design.md +57 -56
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/lib/types.d.ts +12 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +2 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +47 -8
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +233 -141
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/package.json +4 -3
- package/vendor/server/bin/lamaste-server.js +18 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/audit.js +60 -0
- package/vendor/server/src/commands/plugins.js +963 -12
- package/vendor/server/src/commands/push-install.js +317 -0
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/uninstall.js +28 -1
- package/vendor/server/src/ndjson.js +4 -2
- package/vendor/server/src/panel-api.js +32 -4
- package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +1 -0
- package/vendor/server-ui/dist/assets/index-DBp3cmaU.js +84 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +56 -4
- package/vendor/serverd/src/lib/actor.js +24 -0
- package/vendor/serverd/src/lib/agent-jobs.js +330 -0
- package/vendor/serverd/src/lib/app-error.js +10 -0
- package/vendor/serverd/src/lib/audit.js +145 -0
- package/vendor/serverd/src/lib/hook-effects.js +123 -0
- package/vendor/serverd/src/lib/hook-pipelines.js +902 -0
- package/vendor/serverd/src/lib/migrations/state/0006_plugin_agent.sql +4 -0
- package/vendor/serverd/src/lib/migrations/state/0007_audit_and_notifications.sql +29 -0
- package/vendor/serverd/src/lib/migrations/state/0008_plugin_hooks.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0009_plugin_agent_half.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0010_plugin_routes_and_retained_data.sql +47 -0
- package/vendor/serverd/src/lib/migrations/state/0011_agent_job_signature.sql +14 -0
- package/vendor/serverd/src/lib/mtls.js +89 -8
- package/vendor/serverd/src/lib/nginx.js +298 -44
- package/vendor/serverd/src/lib/notifications.js +107 -0
- package/vendor/serverd/src/lib/plugin-hooks.js +560 -0
- package/vendor/serverd/src/lib/plugin-hosting.js +688 -0
- package/vendor/serverd/src/lib/plugin-lifecycle.js +603 -0
- package/vendor/serverd/src/lib/plugin-operations.js +295 -0
- package/vendor/serverd/src/lib/plugin-packages.js +249 -0
- package/vendor/serverd/src/lib/plugins.js +298 -68
- package/vendor/serverd/src/lib/push-install.js +88 -2
- package/vendor/serverd/src/lib/relay-hooks.js +100 -0
- package/vendor/serverd/src/lib/relay-signing.js +43 -0
- package/vendor/serverd/src/lib/request-hooks.js +79 -0
- package/vendor/serverd/src/lib/retained-data.js +99 -0
- package/vendor/serverd/src/lib/trust-proxy.js +34 -0
- package/vendor/serverd/src/lib/tunnel-hooks.js +88 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +260 -0
- package/vendor/serverd/src/middleware/twofa-session.js +11 -1
- package/vendor/serverd/src/routes/agent-forwarder.js +37 -0
- package/vendor/serverd/src/routes/management/agent-panel-authz.js +97 -0
- package/vendor/serverd/src/routes/management/agents.js +262 -1
- package/vendor/serverd/src/routes/management/audit.js +81 -0
- package/vendor/serverd/src/routes/management/certs.js +1 -15
- package/vendor/serverd/src/routes/management/plugins.js +569 -54
- package/vendor/serverd/src/routes/management/tunnels.js +26 -0
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/server-ui/dist/assets/index-B5Wp8lu-.js +0 -257
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +0 -1
|
@@ -358,7 +358,19 @@ curl -s --cert client.p12:password \
|
|
|
358
358
|
"createdAt": "2026-03-16T10:00:00.000Z",
|
|
359
359
|
"expiresAt": "2028-03-16T10:00:00.000Z",
|
|
360
360
|
"revoked": false,
|
|
361
|
-
"expiringSoon": false
|
|
361
|
+
"expiringSoon": false,
|
|
362
|
+
"status": "active",
|
|
363
|
+
"hostFacts": {
|
|
364
|
+
"os": "darwin",
|
|
365
|
+
"arch": "arm64",
|
|
366
|
+
"distro": null,
|
|
367
|
+
"node": "22.13.1",
|
|
368
|
+
"systemd": false,
|
|
369
|
+
"apphost": null
|
|
370
|
+
},
|
|
371
|
+
"hostFactsAt": "2026-10-16T09:00:00.000Z",
|
|
372
|
+
"pushInstallEnabledUntil": "2026-10-16T10:30:00.000Z",
|
|
373
|
+
"pushInstallPolicy": "default"
|
|
362
374
|
},
|
|
363
375
|
{
|
|
364
376
|
"label": "office-imac",
|
|
@@ -369,25 +381,33 @@ curl -s --cert client.p12:password \
|
|
|
369
381
|
"createdAt": "2026-03-10T08:00:00.000Z",
|
|
370
382
|
"expiresAt": "2028-03-10T08:00:00.000Z",
|
|
371
383
|
"revoked": true,
|
|
372
|
-
"expiringSoon": false
|
|
384
|
+
"expiringSoon": false,
|
|
385
|
+
"status": "revoked",
|
|
386
|
+
"hostFacts": null,
|
|
387
|
+
"hostFactsAt": null
|
|
373
388
|
}
|
|
374
389
|
]
|
|
375
390
|
}
|
|
376
391
|
```
|
|
377
392
|
|
|
378
|
-
| Field
|
|
379
|
-
|
|
|
380
|
-
| `label`
|
|
381
|
-
| `serial`
|
|
382
|
-
| `capabilities`
|
|
383
|
-
| `allowedSites`
|
|
384
|
-
| `enrollmentMethod`
|
|
385
|
-
| `delegatedBy`
|
|
386
|
-
| `certType`
|
|
387
|
-
| `createdAt`
|
|
388
|
-
| `expiresAt`
|
|
389
|
-
| `revoked`
|
|
390
|
-
| `expiringSoon`
|
|
393
|
+
| Field | Type | Description |
|
|
394
|
+
| ------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
395
|
+
| `label` | `string` | Agent identifier (matches certificate CN `agent:<label>`) |
|
|
396
|
+
| `serial` | `string` | Certificate serial number |
|
|
397
|
+
| `capabilities` | `string[]` | Granted capabilities |
|
|
398
|
+
| `allowedSites` | `string[]` | Site names this agent can access for file operations |
|
|
399
|
+
| `enrollmentMethod` | `string` | `"p12"`, `"hardware-bound"`, or `"delegated"` — how the agent certificate was issued |
|
|
400
|
+
| `delegatedBy` | `string \| undefined` | For delegated certs: the label of the delegating agent |
|
|
401
|
+
| `certType` | `string \| undefined` | `"plugin-agent"` for delegated certs, absent for regular agents |
|
|
402
|
+
| `createdAt` | `string` | ISO 8601 creation timestamp |
|
|
403
|
+
| `expiresAt` | `string` | ISO 8601 expiry timestamp |
|
|
404
|
+
| `revoked` | `boolean` | Whether the certificate has been revoked |
|
|
405
|
+
| `expiringSoon` | `boolean` | `true` if the certificate is not revoked and fewer than 30 days remain until expiry |
|
|
406
|
+
| `status` | `string` | `"revoked"`, `"expired"`, `"expiring-soon"` (within 30 days) or `"active"`, in that order |
|
|
407
|
+
| `hostFacts` | `object \| null` | What the agent last reported about its host (OS, distribution, architecture, Node.js, systemd, its app host helper) — see [Host facts](agent-hosted-plugins.md#host-facts); `null` until it has |
|
|
408
|
+
| `hostFactsAt` | `string \| null` | When it reported them |
|
|
409
|
+
| `pushInstallEnabledUntil` | `string \| undefined` | End of the agent's last push-install window (open while in the future); absent once it is closed |
|
|
410
|
+
| `pushInstallPolicy` | `string \| undefined` | The push-install policy of that window |
|
|
391
411
|
|
|
392
412
|
---
|
|
393
413
|
|
|
@@ -592,6 +612,41 @@ Mints a new credential for the named agent, rewrites the authfile and restarts C
|
|
|
592
612
|
|
|
593
613
|
---
|
|
594
614
|
|
|
615
|
+
## Agent Daemon Authorization Endpoints
|
|
616
|
+
|
|
617
|
+
An agent's lamaste-agentd accepts a request from the relay only with a relay assertion (`X-Lamalibre-Relay-Assertion`, Ed25519) — see [Security Model](../01-concepts/security-model.md#5c-the-agent-daemon-lamaste-agentd-and-relay-assertions).
|
|
618
|
+
|
|
619
|
+
### `GET /api/agents/me/relay-keys` (admin and agent)
|
|
620
|
+
|
|
621
|
+
The public keys relay assertions are signed with, and the label this relay knows the calling agent by — the audience of the assertions made for it, which its local registry name need not equal. lamaste-agentd fetches this over its pinned mTLS channel on the first request from the relay and again when an assertion names a key it does not know (at most every 30 seconds). Public material; rate limited to 60 per minute.
|
|
622
|
+
|
|
623
|
+
**Response (200):**
|
|
624
|
+
|
|
625
|
+
```json
|
|
626
|
+
{
|
|
627
|
+
"keys": [{ "kid": "<32 hex>", "publicKey": "<base64 DER SubjectPublicKeyInfo, Ed25519>" }],
|
|
628
|
+
"agentLabel": "macbook-pro"
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
`kid` is the first 16 bytes of the SHA-256 of the DER key, hex; the daemon derives it from the key and refuses a response where they disagree. `agentLabel` is `null` for an admin or plugin-agent certificate.
|
|
633
|
+
|
|
634
|
+
| Status | When |
|
|
635
|
+
| ------ | --------------------------------------------------------------- |
|
|
636
|
+
| 503 | The relay's signing key file is unusable (permissions, content) |
|
|
637
|
+
|
|
638
|
+
### `GET /api/agent-panel/authz/:label` (nginx only)
|
|
639
|
+
|
|
640
|
+
The `auth_request` target of the agent-panel vhost `agent-<label>.<domain>`, called by nginx for every request on it — not by clients. nginx sends the client certificate's verification result (`X-SSL-Client-*`, so the mTLS middleware refuses a revoked or unknown certificate), and the request's `X-Original-Method` and `X-Original-URI` (`$request_uri`). Admitted: the admin certificate and the certificate of the agent the panel belongs to, when that agent has an enabled panel tunnel. The answer is `200` with the assertion for exactly that request in `X-Lamalibre-Relay-Assertion` (and `Cache-Control: no-store`), which nginx forwards to the daemon. Exempt from panel 2FA — the browser is on the agent's hostname and has no 2FA cookie; agent panels are guarded by the client certificate alone. Reachable through the panel's own vhosts as well, where it gains nothing: it only ever asserts the caller's own, verified certificate, for a panel that certificate may use.
|
|
641
|
+
|
|
642
|
+
| Status | When |
|
|
643
|
+
| ------ | ----------------------------------------------------------------------- |
|
|
644
|
+
| 400 | Missing or malformed `X-Original-Method` / `X-Original-URI` |
|
|
645
|
+
| 403 | Another agent's certificate, a plugin-agent, or no enabled panel tunnel |
|
|
646
|
+
| 503 | The relay's signing key file is unusable |
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
595
650
|
## Hardware-Bound Certificate Endpoints
|
|
596
651
|
|
|
597
652
|
Hardware-bound certificates use a CSR-based enrollment flow where the private key never leaves the agent machine. Instead of generating a P12 bundle on the server, the agent generates a key pair locally and submits a Certificate Signing Request (CSR) to the panel. The panel signs the CSR and returns the certificate.
|
|
@@ -1010,29 +1065,31 @@ curl -s --cert agent.p12:password \
|
|
|
1010
1065
|
|
|
1011
1066
|
## Quick Reference
|
|
1012
1067
|
|
|
1013
|
-
| Method | Path | Description
|
|
1014
|
-
| ------ | --------------------------------------------- |
|
|
1015
|
-
| GET | `/api/certs` | List all certs (sorted by expiry)
|
|
1016
|
-
| GET | `/api/certs/auto-renew-status` | Check certbot timer status
|
|
1017
|
-
| POST | `/api/certs/:domain/renew` | Force-renew a Let's Encrypt cert
|
|
1018
|
-
| POST | `/api/certs/mtls/rotate` | Rotate mTLS client certificate
|
|
1019
|
-
| GET | `/api/certs/mtls/download` | Download client.p12 file
|
|
1020
|
-
| POST | `/api/certs/agent` | Generate agent certificate (P12)
|
|
1021
|
-
| GET | `/api/certs/agent` | List agent certificates
|
|
1022
|
-
| GET | `/api/certs/agent/:label/download` | Download agent .p12 file
|
|
1023
|
-
| PATCH | `/api/certs/agent/:label/capabilities` | Update agent capabilities
|
|
1024
|
-
| PATCH | `/api/certs/agent/:label/allowed-sites` | Update agent site access
|
|
1025
|
-
| DELETE | `/api/certs/agent/:label` | Revoke agent certificate (releases its tunnels)
|
|
1026
|
-
| GET | `/api/agents/me/chisel-credential` | Own Chisel credential (agent-only)
|
|
1027
|
-
| POST | `/api/agents/me/chisel-credential/rotate` | Rotate own Chisel credential (agent-only, 10-min floor)
|
|
1028
|
-
| POST | `/api/agents/:label/chisel-credential/rotate` | Rotate an agent's Chisel credential (admin-only)
|
|
1029
|
-
|
|
|
1030
|
-
|
|
|
1031
|
-
|
|
|
1032
|
-
| POST | `/api/certs/agent/
|
|
1033
|
-
|
|
|
1034
|
-
| POST | `/api/certs/
|
|
1035
|
-
|
|
|
1068
|
+
| Method | Path | Description |
|
|
1069
|
+
| ------ | --------------------------------------------- | -------------------------------------------------------- |
|
|
1070
|
+
| GET | `/api/certs` | List all certs (sorted by expiry) |
|
|
1071
|
+
| GET | `/api/certs/auto-renew-status` | Check certbot timer status |
|
|
1072
|
+
| POST | `/api/certs/:domain/renew` | Force-renew a Let's Encrypt cert |
|
|
1073
|
+
| POST | `/api/certs/mtls/rotate` | Rotate mTLS client certificate |
|
|
1074
|
+
| GET | `/api/certs/mtls/download` | Download client.p12 file |
|
|
1075
|
+
| POST | `/api/certs/agent` | Generate agent certificate (P12) |
|
|
1076
|
+
| GET | `/api/certs/agent` | List agent certificates |
|
|
1077
|
+
| GET | `/api/certs/agent/:label/download` | Download agent .p12 file |
|
|
1078
|
+
| PATCH | `/api/certs/agent/:label/capabilities` | Update agent capabilities |
|
|
1079
|
+
| PATCH | `/api/certs/agent/:label/allowed-sites` | Update agent site access |
|
|
1080
|
+
| DELETE | `/api/certs/agent/:label` | Revoke agent certificate (releases its tunnels) |
|
|
1081
|
+
| GET | `/api/agents/me/chisel-credential` | Own Chisel credential (agent-only) |
|
|
1082
|
+
| POST | `/api/agents/me/chisel-credential/rotate` | Rotate own Chisel credential (agent-only, 10-min floor) |
|
|
1083
|
+
| POST | `/api/agents/:label/chisel-credential/rotate` | Rotate an agent's Chisel credential (admin-only) |
|
|
1084
|
+
| GET | `/api/agents/me/relay-keys` | Relay assertion public keys and the caller's relay label |
|
|
1085
|
+
| GET | `/api/agent-panel/authz/:label` | Agent-panel vhost authorization (nginx `auth_request`) |
|
|
1086
|
+
| POST | `/api/certs/agent/enroll` | Generate enrollment token (admin-only) |
|
|
1087
|
+
| POST | `/api/certs/agent/enroll-delegated` | Delegated enrollment token (agent-only) |
|
|
1088
|
+
| DELETE | `/api/certs/agent/enroll/:label` | Revoke unused enrollment token |
|
|
1089
|
+
| POST | `/api/certs/agent/upgrade-cert` | Upgrade agent cert to hardware-bound |
|
|
1090
|
+
| POST | `/api/enroll` | Enroll agent with token (public) |
|
|
1091
|
+
| POST | `/api/certs/admin/upgrade-to-hardware-bound` | Upgrade admin to hardware-bound |
|
|
1092
|
+
| GET | `/api/certs/admin/auth-mode` | Get admin auth mode |
|
|
1036
1093
|
|
|
1037
1094
|
### Certificate Object Shape
|
|
1038
1095
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## Authentication
|
|
6
6
|
|
|
7
|
-
All Gatekeeper endpoints proxied through the panel require admin mTLS authentication. The internal `authz/
|
|
7
|
+
All Gatekeeper endpoints proxied through the panel require admin mTLS authentication. The internal `authz/tunnel/<fqdn>` endpoint on `127.0.0.1:9294` is called by nginx only and is not exposed externally.
|
|
8
8
|
|
|
9
9
|
If the Gatekeeper service is unavailable, proxied endpoints return `503 Service Unavailable`.
|
|
10
10
|
|
|
@@ -574,12 +574,14 @@ These endpoints are not proxied through the lamalibre-lamaste-serverd and are no
|
|
|
574
574
|
### Authorization Check (nginx auth_request)
|
|
575
575
|
|
|
576
576
|
```
|
|
577
|
-
GET /authz/
|
|
577
|
+
GET /authz/tunnel/:fqdn
|
|
578
578
|
```
|
|
579
579
|
|
|
580
580
|
**Host:** `127.0.0.1:9294`
|
|
581
581
|
|
|
582
|
-
This is the nginx `auth_request` target. nginx calls this endpoint on every request to a Gatekeeper-protected resource.
|
|
582
|
+
This is the nginx `auth_request` target. nginx calls this endpoint on every request to a Gatekeeper-protected resource. The tunnel vhost names its tunnel in the path (`proxy_pass http://127.0.0.1:9294/authz/tunnel/<fqdn>;`) and sends the same hostname in `X-Original-URL` (with the request's path and query) and `Host`; a request whose `X-Original-URL` or `Host` names another hostname, or has no `Host`, is refused with 401. Gatekeeper validates the user's Authelia session cookie for that URL and checks the corresponding grant.
|
|
583
|
+
|
|
584
|
+
`GET /authz/check` — what tunnel vhosts written before 4.3 call, with `X-Original-URL` and `Host` taken from the client's `Host` header — answers `403` (logged as `vhost_predates_host_binding`): such a vhost closes its tunnel until the panel rewrites it, which it does at startup. See [nginx Configuration](../03-architecture/nginx-configuration.md#authorization-subrequests-name-their-own-hostname).
|
|
583
585
|
|
|
584
586
|
**Response codes:**
|
|
585
587
|
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Lifecycle Hooks
|
|
2
|
+
|
|
3
|
+
An [agent-hosted plugin](agent-hosted-plugins.md) can plug code into the steps Lamaste takes for it — hosting it, removing it, forwarding its admin API, noticing its tunnel go down. Lamaste owns every step; a plugin's hooks run around them. **Relay hooks** run on the relay, in a sandbox, only after an administrator approves them; **agent hooks** run in the plugin's agent half on its own agent. The design is `plugin-development/design/agent-hosted-app-plugins.md` (Slice 2).
|
|
4
|
+
|
|
5
|
+
## In Plain English
|
|
6
|
+
|
|
7
|
+
Each of Lamaste's steps is wrapped like an onion:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
relay pre → agent pre → [ Lamaste's step ] → agent post → relay post
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- **Install** (hosting the plugin on an agent): the relay can check the target and reserve what it needs, the agent half can prepare its data, then Lamaste creates the internal tunnel; afterwards the agent half can seed, and the relay can check end to end. A failing _pre_ stage stops everything; a failing _post_ stage rolls everything back.
|
|
14
|
+
- **Uninstall** (removing the hosting): final backups and deregistration before the tunnel goes, cleanup after.
|
|
15
|
+
- **Request** (the forwarded admin API, relay only): validate a request before anything reaches the agent half (`pre-forward`), scan a response before it leaves the relay — redact or block a leaked token (`post-forward`). Lamaste ships a shared **secret scanner** for that.
|
|
16
|
+
- **Tunnel state**: when the tunnel (or the application behind it) goes down or comes back, both sides are told; while it is down, the relay can answer with the plugin's own maintenance response instead of a bare 502.
|
|
17
|
+
|
|
18
|
+
A hook never sees credentials, the installation secret or the network; anything it wants done beyond its answer — an audit entry, a notification, a log line — it asks Lamaste for as an **effect**.
|
|
19
|
+
|
|
20
|
+
## The manifest
|
|
21
|
+
|
|
22
|
+
```jsonc
|
|
23
|
+
"hooks": {
|
|
24
|
+
"relay": {
|
|
25
|
+
"entry": "hooks/relay.mjs", // one bundled ES module inside the relay package
|
|
26
|
+
"effects": ["audit", "notify-admins"], // effect types the plugin's own hooks may return
|
|
27
|
+
"install": { "pre-tunnel": { "timeoutMs": 60000 }, "post-tunnel": {} },
|
|
28
|
+
"uninstall": { "pre-teardown": {}, "post-teardown": {} },
|
|
29
|
+
"request": [
|
|
30
|
+
{ "stage": "pre-forward", "routes": ["POST /admin/import/*"],
|
|
31
|
+
"body": { "maxBytes": 1048576 }, "timeoutMs": 200, "onError": "reject" },
|
|
32
|
+
{ "stage": "post-forward", "use": "lamaste:secret-scan", "routes": ["GET /admin/export/*"],
|
|
33
|
+
"body": { "maxBytes": 5242880 }, "with": { "action": "redact", "packs": ["credentials"] } }
|
|
34
|
+
],
|
|
35
|
+
"tunnel": { "down": { "timeoutMs": 2000 }, "up": {} }
|
|
36
|
+
},
|
|
37
|
+
"agent": {
|
|
38
|
+
"install": { "pre-tunnel": {}, "post-tunnel": {} },
|
|
39
|
+
"uninstall": { "pre-teardown": {}, "post-teardown": {} },
|
|
40
|
+
"tunnel": { "down": {}, "up": {} }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Only a plugin with `"hosting": "agent"` and an `agent` block may declare hooks. Validation:
|
|
46
|
+
|
|
47
|
+
| Field | Rule |
|
|
48
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
49
|
+
| `relay.entry` | A relative `.mjs`/`.js` path inside the package; required exactly when the plugin has relay hooks of its own. One self-contained ES module (Node built-ins only). |
|
|
50
|
+
| `relay.effects` | `audit`, `notify-admins`, `log`. An effect of another type is dropped. |
|
|
51
|
+
| install / uninstall `timeoutMs` | default 30 000, at most 120 000 |
|
|
52
|
+
| `tunnel` `timeoutMs` | default 2 000, at most 10 000 |
|
|
53
|
+
| request `timeoutMs` | default 500, at most 2 000 |
|
|
54
|
+
| agent stage `timeoutMs` | default 60 000, at most 300 000 |
|
|
55
|
+
| request `body.maxBytes` | at most 10 MiB (and the tunnel's body limit). Without `body` the hook sees headers only. |
|
|
56
|
+
| request `onError` | `pre-forward`: `reject` (default, 503) or `continue`; `post-forward`: `block` (default, 502) or `pass` |
|
|
57
|
+
| request `onOversize` | the same values: what happens when the body is larger than `maxBytes`. Defaults fail closed (413 / 502). |
|
|
58
|
+
| request `routes` | 1–20 patterns per hook, at most 20 hooks |
|
|
59
|
+
| `use` | `lamaste:secret-scan` (post-forward, with `body`) |
|
|
60
|
+
|
|
61
|
+
### Route patterns
|
|
62
|
+
|
|
63
|
+
`"<METHOD> <path>"` or `"<path>"` (any method). The method is `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` or `*`; `GET` also matches `HEAD`. The path starts with `/admin` (the forwarded admin API) or `/app` (the user surface — accepted, not active until the plugin gateway exists). A segment is a literal or `*`: one segment, except as the last, where it matches one or more.
|
|
64
|
+
|
|
65
|
+
Matching errs towards running the hook: literal segments match case-insensitively, segments are compared percent-decoded (`/admin/%69mport` is `/admin/import`), and empty segments are ignored. The forwarder refuses a path with an encoded `/` or `\`.
|
|
66
|
+
|
|
67
|
+
## Approval
|
|
68
|
+
|
|
69
|
+
Relay hooks are code Lamaste runs on the relay, and an [agent half Lamaste installs](app-host.md) is code it has an agent's root helper install, so an administrator approves both first. What is approved is one **digest**: SHA-256 over the canonical JSON of the normalised relay declarations (defaults applied, shared-hook settings included) with the SHA-256 of the entry module, and of the agent half pinned for the version — its package, version, tarball integrity, service (`entry`, `memoryMax`) and system packages (`{ v: 2, relay, agentHalf }`, either part `null` when the version brings none). A version whose declarations, module or agent half differ by one byte has another digest. The runner records the relay part's own digest, so a new agent half with an unchanged relay module leaves the runner current.
|
|
70
|
+
|
|
71
|
+
- Installing a plugin with relay hooks or an agent half Lamaste installs leaves them **pending**: the plugin can be enabled, but not hosted, and its forwarded API answers `503 { code: "hooks_not_approved" }` until the approval.
|
|
72
|
+
- **Approving** names the digest the administrator saw; if what the version brings changed meanwhile the panel answers `409` with the current digest.
|
|
73
|
+
- **Updating** a plugin whose new version brings another hook set or agent half waits for approval; the installed version stays live. **Auto-approve**, off by default and per plugin, approves such changes automatically — each use is in the audit log — except an agent half change that runs more as root: an agent half where there was none, another agent package, added system packages, another service entry or memory limit ([withheld](agent-hosted-plugins.md#approval)).
|
|
74
|
+
- Agent hooks are the plugin's own code on its own agent and need no approval. Shared hooks are part of the approved set by name and settings; their rules ride Lamaste releases (see below).
|
|
75
|
+
|
|
76
|
+
Every approval, auto-approve change and auto-approval is recorded in the [audit log](audit-and-notifications.md) with the administrator certificate's serial.
|
|
77
|
+
|
|
78
|
+
### `GET /api/plugins/:name/hooks`
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"hooks": {
|
|
83
|
+
"relay": {
|
|
84
|
+
"digest": "…64 hex…",
|
|
85
|
+
"entry": "hooks/relay.mjs",
|
|
86
|
+
"entrySha256": "…",
|
|
87
|
+
"stages": [{ "stage": "install:pre-tunnel", "timeoutMs": 60000 }],
|
|
88
|
+
"request": [
|
|
89
|
+
{
|
|
90
|
+
"index": 0,
|
|
91
|
+
"stage": "pre-forward",
|
|
92
|
+
"routes": ["POST /admin/import/*"],
|
|
93
|
+
"bodyMaxBytes": 1048576,
|
|
94
|
+
"timeoutMs": 200,
|
|
95
|
+
"onError": "reject",
|
|
96
|
+
"onOversize": "reject",
|
|
97
|
+
"use": null,
|
|
98
|
+
"settings": null,
|
|
99
|
+
"failOpen": [],
|
|
100
|
+
"inactiveRoutes": []
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"effects": ["audit", "notify-admins"],
|
|
104
|
+
"packs": [],
|
|
105
|
+
"failOpenCount": 0
|
|
106
|
+
},
|
|
107
|
+
"agent": { "stages": [{ "stage": "install:pre-tunnel", "timeoutMs": 60000 }] },
|
|
108
|
+
"agentHalf": {
|
|
109
|
+
"package": "@lamalibre/fx-agent",
|
|
110
|
+
"version": "1.0.0",
|
|
111
|
+
"integrity": "sha512-…",
|
|
112
|
+
"entry": "dist/agentd.mjs",
|
|
113
|
+
"memoryMax": null,
|
|
114
|
+
"systemPackages": [],
|
|
115
|
+
"serviceUser": "lamaste-app-fx",
|
|
116
|
+
"unitName": "lamalibre-app-fx.service",
|
|
117
|
+
"dataDir": "/var/lib/lamalibre/apps/fx"
|
|
118
|
+
},
|
|
119
|
+
"approval": {
|
|
120
|
+
"required": true,
|
|
121
|
+
"state": "pending",
|
|
122
|
+
"digest": "…64 hex: what to approve…",
|
|
123
|
+
"approvedDigest": null,
|
|
124
|
+
"approvedVersion": null,
|
|
125
|
+
"approvedAt": null,
|
|
126
|
+
"approvedBy": null
|
|
127
|
+
},
|
|
128
|
+
"autoApprove": false,
|
|
129
|
+
"runner": { "installedDigest": null, "current": false },
|
|
130
|
+
"pendingUpdate": null
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`failOpen` lists a hook's fail-open settings (`onError`, `onOversize`, `onUnscannable`); the panel highlights them. `relay.digest` is the relay hook set's own digest (what the runner holds); `approval.digest` is the one to approve, covering the relay hooks and `agentHalf` — the agent half Lamaste installs, `null` when there is none or it is installed by hand ([App Host](app-host.md)). `pendingUpdate` is `{ version, digest, stagedAt, relay, agentHalf, autoApproveWithheld }` while an update waits — `autoApproveWithheld` lists why auto-approve did not cover it (`null` otherwise).
|
|
136
|
+
|
|
137
|
+
### `POST /api/plugins/:name/hooks/approve`
|
|
138
|
+
|
|
139
|
+
`{ "digest": "…" }` — the installed set's digest (`200 { ok, hooks }`) or a pending update's (the update is applied: `202 { ok, operation }`). `409` when the set changed since it was shown.
|
|
140
|
+
|
|
141
|
+
### `PUT /api/plugins/:name/hooks/auto-approve`
|
|
142
|
+
|
|
143
|
+
`{ "enabled": true | false }`.
|
|
144
|
+
|
|
145
|
+
## Updating a plugin
|
|
146
|
+
|
|
147
|
+
### `POST /api/plugins/:name/update`
|
|
148
|
+
|
|
149
|
+
`{ "version"?: "1.2.3" | "latest" }` (default `latest`). For agent-hosted plugins. The panel fetches the version with `npm pack` and reads its manifest and relay module **in memory** — nothing of it is installed yet. Then:
|
|
150
|
+
|
|
151
|
+
- `200 { upToDate: true }` — nothing to do;
|
|
152
|
+
- `202 { pendingUpdate }` — what it brings (relay hook set, agent half) differs from what was approved and auto-approve is off or withheld; approve it with the digest shown;
|
|
153
|
+
- `202 { operation }` — the update runs: the new hook module goes into the runner, the install pipeline runs with `phase: "upgrade"` around installing the package (agent stages through the tunnel), and a failure restores the previous package, row and module.
|
|
154
|
+
|
|
155
|
+
The agent half of a hosted plugin whose agent half Lamaste installs is pinned when the version is staged and installed on the agent **first**, after the agent's pre stage; a failure after that activates the version that ran before ([Hosting with an agent half](agent-hosted-plugins.md#the-pipelines)). That is a push: the hosting agent must be eligible for the new version and push install must allow `update` — checked when the update starts, not while it waits for approval.
|
|
156
|
+
|
|
157
|
+
A version that names another plugin, turns the plugin into a server plugin, or drops the agent half of a hosted plugin is refused (`400`), and so is one that switches a hosted plugin between an agent half installed by hand and one Lamaste installs. `DELETE /api/plugins/:name/update` discards a pending update.
|
|
158
|
+
|
|
159
|
+
## Operations
|
|
160
|
+
|
|
161
|
+
Hosting, removing a hosting, an update and the uninstall of a hosted plugin wait on agents and hooks, so they run in the background: the API answers `202 { ok, operation }`. One operation runs per plugin at a time (`409` otherwise).
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"id": "…uuid…",
|
|
166
|
+
"plugin": "fx",
|
|
167
|
+
"kind": "host",
|
|
168
|
+
"actor": "admin",
|
|
169
|
+
"status": "running | succeeded | rolled-back | failed | interrupted",
|
|
170
|
+
"steps": [
|
|
171
|
+
{ "name": "relay install:pre-tunnel", "status": "done", "startedAt": "…", "finishedAt": "…" },
|
|
172
|
+
{
|
|
173
|
+
"name": "agent install:pre-tunnel",
|
|
174
|
+
"status": "failed",
|
|
175
|
+
"error": "agent install:pre-tunnel refused: no room"
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
"warnings": ["Rolling back: …"],
|
|
179
|
+
"error": null,
|
|
180
|
+
"params": { "agentLabel": "vm1" },
|
|
181
|
+
"startedAt": "…",
|
|
182
|
+
"finishedAt": "…"
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`GET /api/plugins/:name/operations` (newest first) and `GET /api/plugins/:name/operations/:id`. An operation the panel was restarted under is `interrupted`.
|
|
187
|
+
|
|
188
|
+
An update that added a confidential OIDC client created its secret ([Secrets of confidential clients](registry-and-oidc.md#secrets-of-confidential-clients)): once the update succeeded, the first `GET /api/plugins/:name/operations/:id` carries `oidcClients: [{ id, secret }]` and later reads do not. The panel holds the secret in memory only, for up to an hour; it is never written to `state.db`. Until that read, the operation is listed with `revealPending: true` (`false` otherwise) — the list itself never carries a secret.
|
|
189
|
+
|
|
190
|
+
## The pipelines
|
|
191
|
+
|
|
192
|
+
### Install (hosting) and uninstall
|
|
193
|
+
|
|
194
|
+
| Step | Install | Uninstall (rollback: `phase: "rollback"`) |
|
|
195
|
+
| ---------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
196
|
+
| relay pre | `install:pre-tunnel` — a failure leaves nothing | `uninstall:pre-teardown` — a failure stops it unless forced |
|
|
197
|
+
| Lamaste | reserve: internal tunnel **without** its chisel grant (the port), record and secret (`installing`), capability | — |
|
|
198
|
+
| Lamaste | the agent half Lamaste installs (`agent.package`): an `app-install` job ([App Host](app-host.md)) | — |
|
|
199
|
+
| agent pre | `install:pre-tunnel` by the agent's sync on its loopback (agent job); a failure releases what was reserved | `uninstall:pre-teardown` through the tunnel |
|
|
200
|
+
| Lamaste | grant the tunnel, wait until the agent half answers through it (150 s) | remove the tunnel (`removing`; the secret stays) |
|
|
201
|
+
| agent post | `install:post-tunnel` through the tunnel | `uninstall:post-teardown` by the agent's sync — a failure is a warning |
|
|
202
|
+
| Lamaste | — | remove the agent half Lamaste installed (`app-remove`; `purge` too) |
|
|
203
|
+
| Lamaste | — | revoke the secret and the capability |
|
|
204
|
+
| relay post | `install:post-tunnel`, then the hosting turns `active` | `uninstall:post-teardown` — a failure is a warning |
|
|
205
|
+
|
|
206
|
+
A failure after the agent's pre stage — or once the agent half's install job was dispatched — rolls back: the uninstall pipeline with `phase: "rollback"` for the sides that took part, then Lamaste's own steps in reverse (an agent half is removed without purge); the operation ends `rolled-back`. Post stages and every uninstall stage must be **idempotent**. `DELETE /api/plugins/:name/hosting?force=true` (and `DELETE /api/plugins/:name?force=true`) passes failing pre-teardown stages — for an agent that is gone — and is recorded; `?purge=true` also deletes the data of the agent half Lamaste installed.
|
|
207
|
+
|
|
208
|
+
The hosting's **phase** (`installing`, `active`, `removing`) is on the hosting record and in `agent-config`: the forwarder and the health monitor use only `active` hostings.
|
|
209
|
+
|
|
210
|
+
### Request
|
|
211
|
+
|
|
212
|
+
The [forwarder](agent-hosted-plugins.md#the-forwarded-admin-api) runs a plugin's approved request hooks on matching routes; other routes stream through untouched.
|
|
213
|
+
|
|
214
|
+
- **pre-forward**: the request is buffered up to the largest `maxBytes` of the matching hooks; each hook, in declaration order, answers `continue` (with header edits) or `reject` (4xx, never forwarded). A larger body follows `onOversize`.
|
|
215
|
+
- **post-forward**: the agent half is asked for an uncompressed response (`Accept-Encoding: identity`); the response is buffered up to `maxBytes`; each hook answers `pass`, `replace` (a new body; `Content-Length` recomputed, `Content-Encoding` and `ETag` dropped) or `block` (5xx with its `reasonId`). A server-sent event stream cannot be checked: a body-reading hook treats it as too large.
|
|
216
|
+
- A hook that fails, times out or answers something invalid follows `onError`. A runner that let a hook overrun is restarted.
|
|
217
|
+
|
|
218
|
+
What a request hook sees: method, path (on the agent half), query, the caller (`admin` or `agent:<label>`), the headers without `Cookie`, `Authorization`, Lamaste's and nginx's own, and the body when declared. Response hooks see status, headers without `Set-Cookie`, and the body.
|
|
219
|
+
|
|
220
|
+
### Tunnel state
|
|
221
|
+
|
|
222
|
+
When the [health monitor](agent-hosted-plugins.md#health) sees a hosting go `down` (reason `tunnel` or `app`), Lamaste runs the relay's `tunnel:down`; its `fallback` (`{ status, retryAfter?, contentType?, body? }`) answers forwarded requests that fail while the plugin is down. `tunnel:up` runs when it is back after a `down` (not on the first `up`), and clears the fallback.
|
|
223
|
+
|
|
224
|
+
The agent cannot see its own chisel session; `agent-config` carries the relay's view (`appPlugins[].health`), and the agent's sync calls the agent half's `tunnel:down` / `tunnel:up` once per change. The panel unreachable for 90 s counts as `tunnel` down on the agent.
|
|
225
|
+
|
|
226
|
+
## The hook runner
|
|
227
|
+
|
|
228
|
+
Relay hooks run in `lamalibre-lamaste-hooks@<route>.service`, started by its socket (`/run/lamalibre-lamaste-hooks/<route>.sock`, `root:lamaste` 0660) on the first call and exiting after ten idle minutes. The sandbox:
|
|
229
|
+
|
|
230
|
+
- a dynamic user, `ProtectSystem=strict`, `ProtectHome`, private `/tmp`, devices, IPC and network namespace, `RestrictAddressFamilies=AF_UNIX`, `IPAddressDeny=any`, no capabilities, `NoNewPrivileges`, `SystemCallFilter=@system-service` minus `@privileged`, `/etc/lamalibre`, `/etc/authelia`, `/etc/letsencrypt`, `/etc/nginx`, `/etc/ssh`, sudoers and shadow inaccessible, empty `/run` and `/var`, 256 MiB, half a CPU, 32 tasks;
|
|
231
|
+
- Node's permission model (`--permission`): reads of the runner and the approved module only — no writes, child processes, workers or addons. Node ≥ 22.13 is required; `create-lamaste` upgrades an older Node;
|
|
232
|
+
- the environment emptied before the module loads; `console` silenced and the unit's output discarded, so nothing a hook sees reaches the journal.
|
|
233
|
+
|
|
234
|
+
`lamaste-priv hooks install <route> <sha256|none>` (module on stdin, checked against the approved digest) writes the module root-owned to `/opt/lamalibre/lamaste/hooks/<route>/<sha256>.mjs` and enables the socket; `hooks activate` points back at the previous module (an update's revert); `hooks restart` stops a stuck runner; `hooks remove` disables it and deletes its modules. The panel reinstalls an approved runner at start-up if it is missing.
|
|
235
|
+
|
|
236
|
+
The protocol between the panel and the runner is one length-prefixed JSON frame each way per connection (`@lamalibre/lamaste@4.3.0/hooks`, `protocol.ts`).
|
|
237
|
+
|
|
238
|
+
## Writing hooks
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
// hooks/relay.mjs — bundled into one file (esbuild), Node built-ins only
|
|
242
|
+
import { defineRelayHooks } from '@lamalibre/lamaste@4.3.0/hooks';
|
|
243
|
+
|
|
244
|
+
export default defineRelayHooks({
|
|
245
|
+
'install:pre-tunnel': async (input) =>
|
|
246
|
+
input.agent.label.startsWith('vm-') ? { ok: true } : { ok: false, reason: 'not a VM' },
|
|
247
|
+
'request:pre-forward': (req) =>
|
|
248
|
+
req.body?.json().rows?.length > 0
|
|
249
|
+
? { action: 'continue' }
|
|
250
|
+
: { action: 'reject', status: 422, body: { error: 'empty import' } },
|
|
251
|
+
'tunnel:down': (state) => ({
|
|
252
|
+
fallback: { status: 503, retryAfter: 60, body: { error: 'maintenance', reason: state.reason } },
|
|
253
|
+
effects: [{ type: 'notify-admins', level: 'warning', message: `down (${state.reason})` }],
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
| Stage | Input | Decision |
|
|
259
|
+
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
260
|
+
| install / uninstall | `plugin`, `version`, `previousVersion` (upgrade), `phase`, `agent.label`, `settings`, `outputs` of earlier stages, `port` once assigned | `{ ok: true, output? }` / `{ ok: false, reason }` |
|
|
261
|
+
| `request:pre-forward` | `method`, `path`, `query`, `caller`, `headers`, `route`, `body?` | `{ action: 'continue', setHeaders?, removeHeaders? }` / `{ action: 'reject', status: 400–499, body? }` |
|
|
262
|
+
| `request:post-forward` | `status`, `headers`, `request`, `route`, `body?` | `{ action: 'pass' }` / `{ action: 'replace', body, contentType?, setHeaders? }` / `{ action: 'block', status?, reasonId }` |
|
|
263
|
+
| `tunnel:down` / `tunnel:up` | `state`, `reason`, `since`, `lastError?` | `{ fallback? }` (down) / `{}` |
|
|
264
|
+
|
|
265
|
+
Every decision may carry `effects` (at most 10): `{ type: 'audit', event, data? }`, `{ type: 'notify-admins', message, level? }`, `{ type: 'log', message, level?, data? }` — rate-limited per plugin (60, 6 and 120 a minute). Header edits may not touch `Host`, framing, `Cookie`/`Set-Cookie`, `Authorization`, `X-Forwarded-*`, `X-Lamalibre-*`, `X-SSL-Client-*`, `Remote-*` or `X-Accel-*`.
|
|
266
|
+
|
|
267
|
+
### Agent hooks
|
|
268
|
+
|
|
269
|
+
The agent half answers `POST /admin/hooks/<stage>` (body `{ stage, input }`, answer `{ ok: true, output? }` or `{ ok: false, reason }`). `createAgentHookHandler` from `@lamalibre/lamaste@4.3.0/agent` does the checking — the caller assertion must name Lamaste itself (`{ type: 'lamaste' }`), which no forwarded caller can, and the forwarder never reaches `/admin/hooks`:
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
import { createAgentHookHandler, createCallerAssertionVerifier } from '@lamalibre/lamaste@4.3.0/agent';
|
|
273
|
+
|
|
274
|
+
const hooks = createAgentHookHandler({
|
|
275
|
+
verifier: createCallerAssertionVerifier({ configPath, plugin: 'fx' }),
|
|
276
|
+
hooks: {
|
|
277
|
+
'install:pre-tunnel': async (input) =>
|
|
278
|
+
(await migrate()) ? { ok: true } : { ok: false, reason: 'migration failed' },
|
|
279
|
+
'tunnel:down': () => queueLocally(),
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
// in the agent half's router, for POST /admin/hooks/*:
|
|
283
|
+
const { status, body } = await hooks.handle({ method, url, headers, body: rawBody });
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The relay calls agent hooks through the tunnel while it exists. Before the tunnel is granted and after it is removed, the relay lists an **agent job** in `GET /api/tunnels/agent-config` (`agentJobs: [{ id, route, kind: "hook", stage, input, timeoutMs, deadlineAt }]`); the agent's sync calls the agent half on its own loopback with the installation secret it holds and reports `POST /api/agents/me/jobs/:id/result` (`{ ok: true, decision }` or `{ ok: false, error }`; hosting agent only, once). A job whose agent half does not answer yet waits for the next sync, until its deadline (timeout plus 90 s).
|
|
287
|
+
|
|
288
|
+
## The secret scanner (`lamaste:secret-scan`)
|
|
289
|
+
|
|
290
|
+
A shared `post-forward` hook shipped with Lamaste, configured with Lamaste rule packs only — app-specific rules belong in the plugin's own relay hook.
|
|
291
|
+
|
|
292
|
+
```jsonc
|
|
293
|
+
{
|
|
294
|
+
"stage": "post-forward",
|
|
295
|
+
"use": "lamaste:secret-scan",
|
|
296
|
+
"routes": ["GET /admin/export/*"],
|
|
297
|
+
"body": { "maxBytes": 5242880 },
|
|
298
|
+
"with": { "action": "redact", "packs": ["credentials"], "onUnscannable": "block" },
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
| Setting | Values |
|
|
303
|
+
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
304
|
+
| `action` | `redact` (default): each secret becomes `[REDACTED:<ruleId>]`, safe inside a JSON string; `block`: `502` with `reasonId: "secret-scan"` |
|
|
305
|
+
| `packs` | `credentials` (default: private keys, AWS, GitHub, GitLab, Slack, Stripe, Google, npm, DigitalOcean, Anthropic and OpenAI keys, Feria tokens), `jwt`, `assignments` (`password=…`, `"secret": "…"`) |
|
|
306
|
+
| `onUnscannable` | `block` (default) or `pass` (fail-open, shown in the approval): a compressed, binary, non-UTF-8 or non-text body |
|
|
307
|
+
|
|
308
|
+
Matching runs on the raw text: a JSON-escaped PEM key is found, and nothing but the matched characters changes (a body is never parsed and serialised again). The audit entry (`hook.secret-scan.redacted|blocked|unscannable`) names rule ids, counts and pack versions — never a value. **Rules ride Lamaste releases**: each pack has a version; within a Lamaste major a pack only gains rules; a Lamaste upgrade that changes a pack is audited for every plugin using it (`hooks.rules-updated`) and needs no re-approval.
|
|
309
|
+
|
|
310
|
+
## CLI
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
lamaste-server plugins hooks <name> # the relay hook set, its digest and approval
|
|
314
|
+
lamaste-server plugins approve-hooks <name> --digest <sha> # approve (or approve a pending update)
|
|
315
|
+
lamaste-server plugins auto-approve <name> on|off
|
|
316
|
+
lamaste-server plugins update <name> [--version <v>]
|
|
317
|
+
lamaste-server plugins discard-update <name>
|
|
318
|
+
lamaste-server plugins host <name> --agent <label> # follows the operation
|
|
319
|
+
lamaste-server plugins unhost <name> [--force] [--purge]
|
|
320
|
+
lamaste-server plugins uninstall <name> [--force] [--purge]
|
|
321
|
+
lamaste-server plugins operations <name>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## State
|
|
325
|
+
|
|
326
|
+
| Path | Contents |
|
|
327
|
+
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
328
|
+
| `state.db` `plugins.hooks`, `plugins.hooks_entry_sha256` | The declared hooks and the relay module's digest, read at install |
|
|
329
|
+
| `state.db` `plugin_hook_sets` | Approval, auto-approve, the runner's module, pack versions, a pending update, the tunnel fallback |
|
|
330
|
+
| `state.db` `plugin_operations`, `agent_jobs` | Operations and their steps; agent jobs and results (a day) |
|
|
331
|
+
| `/opt/lamalibre/lamaste/hooks/<route>/` | The approved module (`<sha256>.mjs`, root, 0644) and `runner.env` |
|
|
332
|
+
| `/run/lamalibre-lamaste-hooks/<route>.sock` | The runner's socket (systemd) |
|
|
333
|
+
| `<agent>/agents/<label>/app-plugin-tunnel-state.json` | What tunnel state the agent last told each agent half (0600) |
|
|
@@ -255,7 +255,7 @@ The wildcard check probes `test-lamaste-check.<domain>`. Wildcard DNS is optiona
|
|
|
255
255
|
|
|
256
256
|
### `POST /api/onboarding/provision`
|
|
257
257
|
|
|
258
|
-
Starts the full stack provisioning process in the background. This configures and starts Chisel and Authelia, issues certbot certificates, and writes nginx vhosts. The Chisel and Authelia binaries and units are installed by `create-lamaste`, not here: if one is missing, the task fails with the error "Chisel is not installed. Run `npx @lamalibre/create-lamaste@4.
|
|
258
|
+
Starts the full stack provisioning process in the background. This configures and starts Chisel and Authelia, issues certbot certificates, and writes nginx vhosts. The Chisel and Authelia binaries and units are installed by `create-lamaste`, not here: if one is missing, the task fails with the error "Chisel is not installed. Run `npx @lamalibre/create-lamaste@4.3.0` on the server to repair the installation." (likewise for Authelia). Task IDs are unchanged from earlier versions; only the titles changed.
|
|
259
259
|
|
|
260
260
|
Provisioning runs asynchronously. This endpoint returns immediately with a 202 status. Use the WebSocket stream endpoint to follow progress in real time.
|
|
261
261
|
|