Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b28b1b9f24 |
@@ -68,7 +68,7 @@
|
||||
"type": "file",
|
||||
"path": "${dir:state}/api.env",
|
||||
"mode": "0600",
|
||||
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=${bound:mongodb-database:as}\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_BUCKET=${bound:s3-bucket:bucket}\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\n"
|
||||
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=${bound:mongodb-database:as}\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_BUCKET=mesh-novox-invoice\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\n"
|
||||
},
|
||||
{
|
||||
"id": "net",
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# The console (novox/hq ADR 0152, design 34): the mesh's tools for whoever is on a machine, served
|
||||
# over MCP on that machine's loopback.
|
||||
#
|
||||
# **Nothing is compiled here.** The console is the tool runtime's own client — `mesh serve` — which
|
||||
# the runtime image already carries beside the runtime it runs modules with. This recipe changes the
|
||||
# program the image starts and nothing else, so the console is exactly the client a person can run by
|
||||
# hand, started by the mesh instead, on the credential the mesh sealed to the machine.
|
||||
#
|
||||
# One base, named rather than pinned: the mesh answers with the copy it holds (novox/hq issue 044).
|
||||
ARG RUNTIME_BASE
|
||||
|
||||
FROM ${RUNTIME_BASE}
|
||||
ENTRYPOINT ["node", "dist/mesh.js"]
|
||||
@@ -1,38 +0,0 @@
|
||||
# mesh-console
|
||||
|
||||
The mesh's tools, on the machine a person sits at, served by a module the mesh assigned there
|
||||
(novox/hq [ADR 0152](https://git.novox.be/novox/hq), design 34).
|
||||
|
||||
Assign it to a machine and an agent on that machine has the mesh's tools at
|
||||
`http://127.0.0.1:<port>/mcp` — MCP over HTTP, `initialize`, `tools/list`, `tools/call`. A person at
|
||||
a terminal reaches the same endpoint with `mesh tools --console http://127.0.0.1:<port>` and
|
||||
`mesh call <module>.<tool> --console …`, with no credential of their own: the console holds it.
|
||||
|
||||
## What it is
|
||||
|
||||
The tool runtime's own client, `mesh serve`, started by the mesh on the credential it sealed to the
|
||||
machine for `<node>.mesh-console`. The manifest says three things nothing else in the catalogue says
|
||||
together:
|
||||
|
||||
- `invokes: ["*"]` — it calls every tool on the mesh, and the bus grants exactly that publish side;
|
||||
- a listener `from: machine` — loopback only, and the filter opens nothing for it;
|
||||
- no `emits`, no `consumes`, no `tools` — nothing on the bus can address it.
|
||||
|
||||
**Loopback is the authority boundary.** Whoever can connect is on the machine, and whoever is on the
|
||||
machine is the account that owns the mesh there (ADR 0034, ADR 0144). There is no token and no login,
|
||||
and `mesh serve` refuses to bind anything but a loopback address.
|
||||
|
||||
## What it lists
|
||||
|
||||
What the running modules answer: every tool runtime serves a `tools` verb for its module, and the
|
||||
console asks the catalogue which modules the mesh holds and each module what it serves. A module that
|
||||
did not answer — not assigned, not up, or built before the runtime answered `tools` — is named in the
|
||||
list's `_meta.notAnswering` and can still be called by `<module>.<tool>`.
|
||||
|
||||
The mesh's own verbs (`status`, `push`, `assign`) are the `mesh-controller` seat's tools under
|
||||
ADR 0132 and are not served on the bus yet; they appear here when they are.
|
||||
|
||||
## Port
|
||||
|
||||
The manifest declares port 4270 and the mesh assigns the machine port as it does for any listener;
|
||||
the console binds `127.0.0.1:${port:4270}`. `node show <machine>` says which port a machine was given.
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"module": "mesh-console",
|
||||
"version": "1",
|
||||
"slug": "console",
|
||||
"capabilities": [
|
||||
"container-runtime"
|
||||
],
|
||||
"invokes": [
|
||||
"*"
|
||||
],
|
||||
"own-secrets": {
|
||||
"broker": "${dir:mesh-state}/broker"
|
||||
},
|
||||
"listens": [
|
||||
{
|
||||
"name": "mcp",
|
||||
"port": 4270,
|
||||
"protocol": "tcp",
|
||||
"from": "machine",
|
||||
"why": "the mesh's tools for whoever is on this machine, over MCP on loopback; the machine's login is the authority (novox/hq ADR 0152)"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "mesh-state",
|
||||
"type": "directory",
|
||||
"mode": "0700",
|
||||
"place": "mesh"
|
||||
},
|
||||
{
|
||||
"id": "server",
|
||||
"type": "container",
|
||||
"name": "mesh-console",
|
||||
"network": "host",
|
||||
"args": [
|
||||
"serve"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_CONSOLE_LISTEN": "127.0.0.1:${port:4270}"
|
||||
},
|
||||
"volumes": [
|
||||
"${dir:mesh-state}/broker:/run/secrets/broker:ro"
|
||||
],
|
||||
"artifact": "runtime"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"on": [
|
||||
{
|
||||
"arg": "RUNTIME_BASE",
|
||||
"module": "mesh-tools",
|
||||
"artifact": "runtime"
|
||||
}
|
||||
],
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "runtime",
|
||||
"kind": "image",
|
||||
"from": "Dockerfile"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+15
-5
@@ -303,11 +303,21 @@ export class MinioClient {
|
||||
|
||||
// --- module-scoped helpers -------------------------------------------------
|
||||
|
||||
// **Neither the access key nor the bucket is derived here any more.** `accessKeyFor` minted an id
|
||||
// of its own until the mesh took that over (ADR 0048: the login is the mesh's, handed to both
|
||||
// ends), and `bucketFor` derived the bucket until the mesh took that over too (ADR 0188: the rule
|
||||
// is a line of this module's manifest, filled per consumer and delivered to both ends). Both
|
||||
// survived with no callers, which is the state a rule comes back from; they are gone.
|
||||
/** A deterministic 20-char access key id from a consumer name, so removal needs no stored state:
|
||||
* the provisioner recomputes the same id at teardown that it minted at creation. */
|
||||
export function accessKeyFor(consumer: string): string {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
const digest = createHash("sha256").update(consumer).digest();
|
||||
let out = "";
|
||||
for (let i = 0; i < 20; i++) out += chars[digest[i] % chars.length];
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A DNS-safe bucket name derived from a consumer — the removable identity of its storage. */
|
||||
export function bucketFor(consumer: string): string {
|
||||
const name = consumer.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
||||
return name.length >= 3 ? name : `mesh-${name}`;
|
||||
}
|
||||
|
||||
function bucketPolicy(bucket: string): string {
|
||||
return JSON.stringify({
|
||||
|
||||
@@ -49,8 +49,7 @@
|
||||
"s3-bucket": {
|
||||
"scheme": "http",
|
||||
"region": "eu-west",
|
||||
"port": 9000,
|
||||
"bucket": "${consumer:as:dns}"
|
||||
"port": 9000
|
||||
}
|
||||
},
|
||||
"receives": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@novox/mesh-sdk": "^0.1.2"
|
||||
"@novox/mesh-sdk": "^0.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
|
||||
@@ -10,24 +10,18 @@
|
||||
// **The access key and its secret are the mesh's, not the provisioner's (ADR 0048).** The mesh
|
||||
// derives the login (the access-key id) and hands it to both ends, and mints the secret key. minio
|
||||
// creates the service account under exactly that access key with exactly that secret — a credential
|
||||
// the provisioner invented is one the consumer could never present.
|
||||
//
|
||||
// **The bucket name is the mesh's too (ADR 0188).** It used to be computed here, from the login,
|
||||
// and every consumer transcribed the same rule into its own definition by hand — two copies of
|
||||
// one rule with nothing comparing them, and one of three was wrong for months. Now the rule is a
|
||||
// line of this module's manifest (`serves.s3-bucket.bucket: ${consumer:as:dns}`), the mesh fills
|
||||
// it per consumer, and the same filled value reaches this provisioner and the consumer's own
|
||||
// configuration. There is no second computation to disagree with.
|
||||
// the provisioner invented is one the consumer could never present. The bucket is derived from the
|
||||
// login, so teardown recomputes it with nothing to persist.
|
||||
|
||||
import { runProvisioner, type Provision } from "@novox/mesh-sdk/provisioner";
|
||||
import { emit } from "@novox/mesh-sdk/events";
|
||||
import { MinioClient } from "../client.js";
|
||||
import { MinioClient, bucketFor } from "../client.js";
|
||||
|
||||
const minio = MinioClient.fromEnv();
|
||||
|
||||
runProvisioner("s3-bucket", {
|
||||
async create(p: Provision): Promise<void> {
|
||||
const bucket = bucketNamed(p.derived);
|
||||
const bucket = bucketFor(p.as);
|
||||
const accessKeyId = p.as;
|
||||
|
||||
if (!(await minio.bucketExists(bucket))) await minio.createBucket(bucket);
|
||||
@@ -44,8 +38,8 @@ runProvisioner("s3-bucket", {
|
||||
});
|
||||
},
|
||||
|
||||
async remove(p: { as: string; derived: Readonly<Record<string, unknown>> }): Promise<void> {
|
||||
const bucket = bucketNamed(p.derived);
|
||||
async remove(p: { as: string }): Promise<void> {
|
||||
const bucket = bucketFor(p.as);
|
||||
|
||||
// Revoking the key is what cuts the consumer's access. The bucket is emptied-then-dropped only if
|
||||
// empty; a bucket that still holds objects is left for an operator rather than erroring on every
|
||||
@@ -63,28 +57,10 @@ runProvisioner("s3-bucket", {
|
||||
// Asked every minute by the harness: whether the backend still holds this consumer exactly as
|
||||
// the mesh gave it, so a login lost behind the provisioner's back is made again (novox/hq issue 120).
|
||||
async holds(p: Provision): Promise<boolean> {
|
||||
return minio.canReachAs(bucketNamed(p.derived), p.as, p.password);
|
||||
return minio.canReachAs(bucketFor(p.as), p.as, p.password);
|
||||
},
|
||||
});
|
||||
|
||||
/** The bucket the mesh derived for this consumer.
|
||||
*
|
||||
* Absent means this module is running against a control plane that does not fill `${consumer:…}`
|
||||
* yet, or a manifest whose `serves` block lost the line. Both are the same mistake from here —
|
||||
* nobody said which bucket — and both are said rather than guessed: a provisioner that fell back
|
||||
* to deriving one would restore the second rule and hide the fault behind a bucket that happens
|
||||
* to be right. */
|
||||
function bucketNamed(derived: Readonly<Record<string, unknown>>): string {
|
||||
const bucket = derived.bucket;
|
||||
if (typeof bucket !== "string" || bucket === "") {
|
||||
throw new Error(
|
||||
"the mesh did not say which bucket this consumer gets: minio's manifest must serve " +
|
||||
"`bucket` under s3-bucket (novox/hq ADR 0188)",
|
||||
);
|
||||
}
|
||||
return bucket;
|
||||
}
|
||||
|
||||
/** Emit best-effort: a broker hiccup is logged and dropped, never allowed to throw back and fail a
|
||||
* bucket that was made. */
|
||||
async function announce(type: string, body: unknown): Promise<void> {
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
"type": "file",
|
||||
"path": "${dir:state}/server.env",
|
||||
"mode": "0600",
|
||||
"content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=${bound:postgres-database:as}\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=mesh-admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=${bound:s3-bucket:bucket}\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\nOBJECTSTORE_S3_REGION=${bound:s3-bucket:region}\n"
|
||||
"content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=${bound:postgres-database:as}\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=mesh-admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=mesh-novox-ncloud\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\nOBJECTSTORE_S3_REGION=${bound:s3-bucket:region}\n"
|
||||
},
|
||||
{
|
||||
"id": "html",
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"type": "file",
|
||||
"path": "${dir:state}/server.env",
|
||||
"mode": "0600",
|
||||
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=admin\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_BUCKET=${bound:s3-bucket:bucket}\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\nMINIO_USE_SSL=false\n"
|
||||
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=admin\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_BUCKET=mesh-novox-photos\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\nMINIO_USE_SSL=false\n"
|
||||
},
|
||||
{
|
||||
"id": "net",
|
||||
|
||||
Reference in New Issue
Block a user