Clean prepaid SoT, identity mailbox, public access planes, leaf policy, fleet spawn
Some checks are pending
offline / test (push) Waiting to run

Persist account-balance books; edge caches prepaid from books. Add zappier-identity, verae-nats-accounts, verae-jobs-events, verae-access-staff, zapier-decisions. Edge binds loopback; lan-134 stays off; HTTP services prefer local spawn.
This commit is contained in:
George Lambert 2026-09-11 17:15:16 -04:00
parent 345aeeead9
commit ddf772454b
153 changed files with 2236 additions and 116 deletions

View file

@ -13,7 +13,7 @@ Operator control plane for Verae Time × Zapier **runtime** services:
cd packages/verae-fleet
npm test
node src/cli.js list
node src/cli.js serve # http://127.0.0.1:3850/
node src/cli.js serve # http://0.0.0.0:3850/
```
Against a running daemon:

View file

@ -368,9 +368,10 @@
<h1 class="title">Operator console</h1>
</header>
<h1 id="operator-console">Operator console</h1>
<p>Loopback UI at <strong>http://127.0.0.1:3850/</strong> — Fleet, Trace
(simulator), and Docs in one shell. Not a public site. NATS stays
private; SSH identity files stay on disk.</p>
<p>Operator UI at <strong>http://0.0.0.0:3850/</strong> (all
interfaces). Fleet, Trace (simulator), and Docs in one shell. NATS stays
private (<code>127.0.0.1:4222</code>); SSH identity files stay on
disk.</p>
<div class="sourceCode" id="cb1"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="bu">cd</span> packages/verae-fleet</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="ex">node</span> src/cli.js serve</span></code></pre></div>

View file

@ -1,6 +1,6 @@
# Operator console
Loopback UI at **http://127.0.0.1:3850/** — Fleet, Trace (simulator), and Docs in one shell. Not a public site. NATS stays private; SSH identity files stay on disk.
Operator UI at **http://0.0.0.0:3850/** (all interfaces). Fleet, Trace (simulator), and Docs in one shell. NATS stays private (`127.0.0.1:4222`); SSH identity files stay on disk.
```bash
cd packages/verae-fleet

View file

@ -1,7 +1,7 @@
{
"apiVersion": "verae.fleet/v1",
"control": {
"bind": "127.0.0.1",
"bind": "0.0.0.0",
"port": 3850,
"probeIntervalMs": 750,
"unhealthyAfterMs": 2500,
@ -20,6 +20,9 @@
"access-api": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"access-leaf": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"access-zapier": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"access-staff": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"identity": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"jobs-events": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"account-balance": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"customer-service": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"sales-pricing": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },

View file

@ -17,6 +17,7 @@
"PORT": "3023",
"AUTHZ_URL": "http://127.0.0.1:3020",
"LEAF_TOKEN": "leaf-dev-token",
"NATS_POLICY_PATH": "../verae-nats-accounts/policy.json",
"NATS_URL": "nats://127.0.0.1:4222"
},
"nats": {

View file

@ -0,0 +1,22 @@
{
"id": "access-staff",
"title": "Staff access plane",
"kind": "http",
"package": "verae-access-staff",
"role": "access-staff",
"managed": true,
"runtime": "HTTP :3025 plane=staff",
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
"ports": { "healthBase": 3025 },
"spawn": {
"cwd": "../verae-access-staff",
"command": "node",
"args": ["src/server.js"]
},
"env": {
"PORT": "3025",
"AUTHZ_URL": "http://127.0.0.1:3020",
"ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010"
},
"nats": { "in": [], "out": ["verae.access.authz.check"] }
}

View file

@ -0,0 +1,24 @@
{
"id": "identity",
"title": "Verae user-id bind/lookup",
"kind": "http",
"package": "zappier-identity",
"role": "identity",
"managed": true,
"runtime": "HTTP :3026 + NATS verae.identity.*",
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
"ports": { "healthBase": 3026 },
"spawn": {
"cwd": "../zappier-identity",
"command": "node",
"args": ["src/server.js"]
},
"env": {
"PORT": "3026",
"NATS_URL": "nats://127.0.0.1:4222"
},
"nats": {
"in": ["verae.identity.bind", "verae.identity.lookup"],
"out": []
}
}

View file

@ -0,0 +1,24 @@
{
"id": "jobs-events",
"title": "jobs.events mailbox",
"kind": "http",
"package": "verae-jobs-events",
"role": "jobs-events",
"managed": true,
"runtime": "HTTP :3030 + NATS verae.zapier.jobs.events",
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
"ports": { "healthBase": 3030 },
"spawn": {
"cwd": "../verae-jobs-events",
"command": "node",
"args": ["src/server.js"]
},
"env": {
"PORT": "3030",
"NATS_URL": "nats://127.0.0.1:4222"
},
"nats": {
"in": ["verae.zapier.jobs.events"],
"out": []
}
}

View file

@ -5,7 +5,7 @@
"package": "zappier",
"role": "zappier-edge",
"managed": true,
"runtime": "HTTPS :3000 (portal, admin, meter, proxy to middleware)",
"runtime": "loopback :3000 (portal, admin, Stripe; public doors are access-*)",
"health": { "type": "http", "path": "/health", "timeoutMs": 4000 },
"ports": { "healthBase": 3000 },
"spawn": {
@ -15,12 +15,15 @@
},
"env": {
"PORT": "3000",
"BIND": "127.0.0.1",
"ZAPPIER_UPSTREAM": "http://127.0.0.1:3100",
"CS_SERVICE_URL": "http://127.0.0.1:3011",
"SALES_PRICING_URL": "http://127.0.0.1:3012",
"ACCOUNTING_EXPORT_URL": "http://127.0.0.1:3013",
"NATS_URL": "nats://127.0.0.1:4222",
"AUTHZ_URL": "http://127.0.0.1:3020"
"AUTHZ_URL": "http://127.0.0.1:3020",
"ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010",
"IDENTITY_URL": "http://127.0.0.1:3026"
},
"nats": {
"in": [],
@ -32,5 +35,5 @@
"verae.billing.balance.adjust"
]
},
"notes": "Zapier x-api-key lands here. HTTPS to middleware. NATS billing to account-balance."
"notes": "Loopback only. Public Zapier traffic hits access-zapier :3024."
}

View file

@ -92,15 +92,15 @@ async function main() {
}
if (cmd === 'serve' || cmd === 'monitor') {
const sup = new Supervisor({ loaded });
const mon = new Monitor(sup);
startControlServer(sup, mon);
print(`fleet ${cmd} pid=${process.pid}\nUI http://${loaded.control.bind || '0.0.0.0'}:${loaded.control.port}/\nSIGINT stops all managed replicas`);
for (const id of Object.keys(loaded.services)) {
const s = loaded.services[id];
if (s.managed && s.enabled && s.min > 0) await sup.startService(id);
}
const mon = new Monitor(sup);
await mon.tick();
mon.start();
startControlServer(sup, mon);
print(`fleet ${cmd} pid=${process.pid}\nUI ${BASE}/\nSIGINT stops all managed replicas`);
process.on('SIGINT', async () => {
mon.stop();
await sup.stopAll();

View file

@ -41,7 +41,11 @@ export function loadMachines(root, overlayPath) {
if (extra && typeof extra === 'object') Object.assign(m, extra);
}
}
return list.map(normalizeMachine);
return list.map((m) => {
const n = normalizeMachine(m);
if (n.id === 'lan-134' && process.env.FLEET_ENABLE_LAN134 !== '1') n.enabled = false;
return n;
});
}
export function normalizeMachine(m) {
@ -86,9 +90,13 @@ export function canHost(machine, serviceId, role) {
* @param {string} serviceId
* @param {string} [role]
*/
export function pickMachine(machines, instances, serviceId, role, exclude = []) {
export function pickMachine(machines, instances, serviceId, role, exclude = [], opts = {}) {
const skip = new Set(exclude);
const eligible = machines.filter((m) => canHost(m, serviceId, role) && !skip.has(m.id));
let eligible = machines.filter((m) => canHost(m, serviceId, role) && !skip.has(m.id));
if (opts.preferLocal) {
const local = eligible.filter((m) => m.kind === 'local');
if (local.length) eligible = local;
}
if (!eligible.length) return null;
const scored = eligible.map((m) => {
const running = instances.filter((i) => i.machine === m.id && i.pid).length;

View file

@ -1,6 +1,6 @@
/**
* Operator HTTP: list, status, pause/resume/stop/start/restart.
* Binds 127.0.0.1 only.
* HTTP bind from fleet.json control.bind (default 0.0.0.0).
* @module server
*/
@ -23,7 +23,7 @@ function json(res, code, obj) {
* @param {import('./monitor.js').Monitor} [mon]
*/
export function startControlServer(sup, mon) {
const bind = sup.loaded.control?.bind || '127.0.0.1';
const bind = process.env.FLEET_BIND || sup.loaded.control?.bind || '0.0.0.0';
const port = Number(sup.loaded.control?.port || 3850);
const sim = new Simulator();

View file

@ -57,7 +57,7 @@ export function scpBaseArgs(machine) {
return args;
}
export function sshExec(machine, remoteCommand, { timeoutMs = 20000 } = {}) {
export function sshExec(machine, remoteCommand, { timeoutMs = 8000 } = {}) {
return new Promise((resolve, reject) => {
const args = [...sshBaseArgs(machine), sshTarget(machine), remoteCommand];
const child = spawn('ssh', args, { stdio: ['ignore', 'pipe', 'pipe'] });
@ -96,7 +96,7 @@ export function scpTo(machine, localFiles, remoteDir) {
const t = setTimeout(() => {
child.kill('SIGKILL');
reject(new Error('scp timeout'));
}, 30000);
}, 8000);
child.on('close', (code) => {
clearTimeout(t);
if (code !== 0) reject(new Error(Buffer.concat(err).toString('utf8') || `scp exit ${code}`));

View file

@ -153,6 +153,7 @@ export class Supervisor {
serviceId,
spec.role || serviceId,
opts.excludeMachines || [],
{ preferLocal: Boolean(spec.spawn?.command) },
);
if (!machine) {
this.log('skip', { service: serviceId, reason: 'no-machine-capacity' });

View file

@ -58,6 +58,9 @@ describe('verae-fleet', () => {
'access-api',
'access-leaf',
'access-zapier',
'access-staff',
'identity',
'jobs-events',
'account-balance',
'customer-service',
'sales-pricing',
@ -78,6 +81,8 @@ describe('verae-fleet', () => {
assert.equal(tree.keepFloor, true);
assert.equal(tree.configPath, 'services/tree-node.json');
assert.equal(loaded.fleetPath, 'fleet.json');
const lan = loaded.machines.find((m) => m.id === 'lan-134');
if (lan) assert.equal(lan.enabled, false);
});
it('starts the tree-node floor (3 available copies)', async () => {

View file

@ -53,4 +53,19 @@ describe('SSH remote machine config', () => {
const pick = pickMachine(machines, instances, 'tree-node', 'tree-node');
assert.equal(pick.id, 'ns1');
});
it('preferLocal keeps HTTP apps on the control host', () => {
const machines = [
normalizeMachine({ id: 'local', kind: 'local', host: '127.0.0.1', capacity: 32, roles: ['*'] }),
normalizeMachine({
id: 'ns1',
kind: 'ssh',
host: '70.88.205.138',
capacity: 24,
roles: ['*'],
}),
];
const pick = pickMachine(machines, [], 'account-balance', 'account-balance', [], { preferLocal: true });
assert.equal(pick.id, 'local');
});
});