@rubytech/create-realagent 1.0.855 → 1.0.858
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/dist/__tests__/account-id-env.test.js +47 -0
- package/dist/__tests__/port-canonicalisation.test.js +1 -0
- package/dist/index.js +47 -19
- package/dist/port-resolution.js +9 -0
- package/package.json +2 -2
- package/payload/platform/lib/account-enumeration/dist/__tests__/validate-env.test.d.ts +2 -0
- package/payload/platform/lib/account-enumeration/dist/__tests__/validate-env.test.d.ts.map +1 -0
- package/payload/platform/lib/account-enumeration/dist/__tests__/validate-env.test.js +55 -0
- package/payload/platform/lib/account-enumeration/dist/__tests__/validate-env.test.js.map +1 -0
- package/payload/platform/lib/account-enumeration/dist/index.d.ts +26 -0
- package/payload/platform/lib/account-enumeration/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/account-enumeration/dist/index.js +13 -0
- package/payload/platform/lib/account-enumeration/dist/index.js.map +1 -1
- package/payload/platform/lib/account-enumeration/src/__tests__/validate-env.test.ts +57 -0
- package/payload/platform/lib/account-enumeration/src/index.ts +44 -0
- package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +2 -2
- package/payload/platform/plugins/admin/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/admin/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/cloudflare/scripts/_stream-log.sh +1 -1
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +36 -9
- package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/docs/references/internals.md +2 -0
- package/payload/platform/plugins/email/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/scripts/email-auto-respond.js +1 -1
- package/payload/platform/plugins/email/mcp/dist/scripts/email-auto-respond.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/waitlist/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/waitlist/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/scripts/check-no-task-id-leaks.mjs +110 -0
- package/payload/platform/scripts/vnc.sh +6 -6
- package/payload/server/chunk-7ADUQXTU.js +2143 -0
- package/payload/server/chunk-BY4LZDL4.js +667 -0
- package/payload/server/chunk-CNNPNADU.js +10891 -0
- package/payload/server/chunk-FL3H3AQD.js +1603 -0
- package/payload/server/client-pool-WA5WGN7W.js +34 -0
- package/payload/server/cloudflare-task-tracker-OOQCL5ZB.js +20 -0
- package/payload/server/maxy-edge.js +3 -3
- package/payload/server/public/assets/{Checkbox-U-H3_oQu.js → Checkbox-BySsatDO.js} +1 -1
- package/payload/server/public/assets/{admin-Cpk5cT4I.js → admin-CZpefPcA.js} +1 -1
- package/payload/server/public/assets/data-BuuqlV4L.js +1 -0
- package/payload/server/public/assets/graph-CtVITeok.js +1 -0
- package/payload/server/public/assets/jsx-runtime-O5ef8xK8.css +1 -0
- package/payload/server/public/assets/{page-D_6h4ZZy.js → page-Ddc_nKh8.js} +1 -1
- package/payload/server/public/assets/{page-CNKytKTe.js → page-IQBQoOdT.js} +1 -1
- package/payload/server/public/assets/{public-DApUXgoq.js → public-BhyNH7eq.js} +1 -1
- package/payload/server/public/assets/{useAdminFetch-Cex4bYm7.js → useAdminFetch-B3MO55eB.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-CI8GpxfU.js → useVoiceRecorder-B_zVS4Oe.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +32 -8
- package/payload/server/public/assets/data-BbczthXl.js +0 -1
- package/payload/server/public/assets/graph-DzK_bDyH.js +0 -1
- package/payload/server/public/assets/jsx-runtime-OD2WKrlG.css +0 -1
- /package/payload/server/public/assets/{jsx-runtime-BjkIZEse.js → jsx-runtime-DnY0498s.js} +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Task 955 — acceptance gate for ACCOUNT_ID stamping in the brand systemd unit.
|
|
2
|
+
//
|
|
3
|
+
// Two invariants this test protects:
|
|
4
|
+
// (a) buildMaxyUnitFile emits a literal `Environment=ACCOUNT_ID=<uuid>` line
|
|
5
|
+
// in the [Service] block when accountId is provided. Pi recovery via
|
|
6
|
+
// `npx -y @rubytech/create-maxy@latest` depends on this line landing —
|
|
7
|
+
// without it, process.env.ACCOUNT_ID is undefined and the
|
|
8
|
+
// writeNodeWithEdges gate rejects every CF-setup graph write at
|
|
9
|
+
// cloudflare-task-tracker.ts:187/326/347/559.
|
|
10
|
+
// (b) buildMaxyUnitFile throws on an empty accountId. Falling through with
|
|
11
|
+
// "" would emit a unit with no ACCOUNT_ID line; the boot validator at
|
|
12
|
+
// platform/ui/server/index.ts would then FATAL reason=missing on every
|
|
13
|
+
// restart, surfacing the same regression class behind a different log
|
|
14
|
+
// prefix. Throwing at build time aborts the install loud, before the
|
|
15
|
+
// broken unit is written.
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { buildMaxyUnitFile } from "../port-resolution.js";
|
|
19
|
+
const VALID_UUID = "12345678-9abc-def0-1234-56789abcdef0";
|
|
20
|
+
function callBuildUnit(accountId) {
|
|
21
|
+
return buildMaxyUnitFile({
|
|
22
|
+
productName: "Maxy",
|
|
23
|
+
brandHostname: "maxy",
|
|
24
|
+
neo4jDedicated: false,
|
|
25
|
+
installDir: "/home/me/maxy",
|
|
26
|
+
persistDir: "/home/me/.maxy",
|
|
27
|
+
port: 19200,
|
|
28
|
+
maxyUiInternalPort: 19201,
|
|
29
|
+
vncDisplay: 99,
|
|
30
|
+
rfbPort: 5900,
|
|
31
|
+
websockifyPort: 6080,
|
|
32
|
+
cdpPort: 9222,
|
|
33
|
+
chromiumBin: "/usr/bin/chromium",
|
|
34
|
+
accountId,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
test("buildMaxyUnitFile emits Environment=ACCOUNT_ID=<uuid> for a valid accountId", () => {
|
|
38
|
+
const unit = callBuildUnit(VALID_UUID);
|
|
39
|
+
assert.match(unit, /^Environment=ACCOUNT_ID=12345678-9abc-def0-1234-56789abcdef0$/m, "ACCOUNT_ID line missing or malformed");
|
|
40
|
+
// Placement matters: identity (NODE_ENV → ACCOUNT_ID → PORT) so the most
|
|
41
|
+
// fundamental brand identity sits next to the env that selects the brand
|
|
42
|
+
// runtime. Anchored regex against the three adjacent lines.
|
|
43
|
+
assert.match(unit, /Environment=NODE_ENV=production\nEnvironment=ACCOUNT_ID=12345678-9abc-def0-1234-56789abcdef0\nEnvironment=PORT=19200/, "ACCOUNT_ID line is not placed between NODE_ENV and PORT");
|
|
44
|
+
});
|
|
45
|
+
test("buildMaxyUnitFile throws when accountId is empty", () => {
|
|
46
|
+
assert.throws(() => callBuildUnit(""), /accountId is required/, "expected throw on empty accountId, did not throw");
|
|
47
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -473,7 +473,7 @@ function ensureNonSnapChromium() {
|
|
|
473
473
|
throw new Error(`ensureNonSnapChromium: ${decision.reason}. apt install of \`chromium\` ran in installAptGroup(VNC stack) above; if its post-check passed but no chromium binary is on PATH, the system PATH is misconfigured.`);
|
|
474
474
|
}
|
|
475
475
|
if (decision.action === "install-google-chrome") {
|
|
476
|
-
console.log(" Detected snap-confined Chromium
|
|
476
|
+
console.log(" Detected snap-confined Chromium — installing Google Chrome stable...");
|
|
477
477
|
logFile(` [snap-chromium] installing google-chrome-stable from Google's signed apt repo`);
|
|
478
478
|
// Fetch + dearmor the signing key, write to /etc/apt/trusted.gpg.d/. Pipe
|
|
479
479
|
// composition runs through bash -c so the curl|gpg pipeline is one
|
|
@@ -495,7 +495,7 @@ function ensureNonSnapChromium() {
|
|
|
495
495
|
], { sudo: true });
|
|
496
496
|
console.log(" [privileged] apt-get update");
|
|
497
497
|
shell("apt-get", ["update"], { sudo: true });
|
|
498
|
-
installAptGroup("Google Chrome stable
|
|
498
|
+
installAptGroup("Google Chrome stable", ["google-chrome-stable"]);
|
|
499
499
|
// Re-resolve after install to capture the now-installed absolute path.
|
|
500
500
|
const postInstallWhich = which("google-chrome-stable");
|
|
501
501
|
if (!postInstallWhich) {
|
|
@@ -518,7 +518,7 @@ function ensureNonSnapChromium() {
|
|
|
518
518
|
// surfaces the contract breach with the install context still in scope.
|
|
519
519
|
const finalRealpath = realpath(RESOLVED_CHROMIUM_BIN);
|
|
520
520
|
if (isSnapConfinedPath(finalRealpath)) {
|
|
521
|
-
throw new Error(`ensureNonSnapChromium: resolved Chromium binary ${RESOLVED_CHROMIUM_BIN} realpaths to ${finalRealpath} which is under /snap/ — refusing to persist
|
|
521
|
+
throw new Error(`ensureNonSnapChromium: resolved Chromium binary ${RESOLVED_CHROMIUM_BIN} realpaths to ${finalRealpath} which is under /snap/ — refusing to persist.`);
|
|
522
522
|
}
|
|
523
523
|
console.log(` Chromium binary: ${RESOLVED_CHROMIUM_BIN} (realpath=${finalRealpath ?? "?"})`);
|
|
524
524
|
logFile(` [snap-chromium] resolved bin=${RESOLVED_CHROMIUM_BIN} realpath=${finalRealpath ?? "null"}`);
|
|
@@ -1171,7 +1171,7 @@ function peerBrandUsingSystemUnit() {
|
|
|
1171
1171
|
peerEnvContents.push([hostname, readFileSync(envPath, "utf-8")]);
|
|
1172
1172
|
}
|
|
1173
1173
|
catch (err) {
|
|
1174
|
-
console.error(` WARNING: unable to read peer brand .env at ${envPath} — treating as potential dependency to avoid data loss
|
|
1174
|
+
console.error(` WARNING: unable to read peer brand .env at ${envPath} — treating as potential dependency to avoid data loss: ${err instanceof Error ? err.message : String(err)}`);
|
|
1175
1175
|
return hostname;
|
|
1176
1176
|
}
|
|
1177
1177
|
}
|
|
@@ -1302,7 +1302,7 @@ WantedBy=multi-user.target
|
|
|
1302
1302
|
// mutually exclusive with the disable path: exactly one log line per install.
|
|
1303
1303
|
const peerOnSystemUnit = peerBrandUsingSystemUnit();
|
|
1304
1304
|
if (peerOnSystemUnit !== null) {
|
|
1305
|
-
const keptActiveMsg = ` [neo4j] system unit kept active — peer brand ${peerOnSystemUnit} depends on port ${DEFAULT_NEO4J_PORT}
|
|
1305
|
+
const keptActiveMsg = ` [neo4j] system unit kept active — peer brand ${peerOnSystemUnit} depends on port ${DEFAULT_NEO4J_PORT}`;
|
|
1306
1306
|
console.log(keptActiveMsg);
|
|
1307
1307
|
logFile(keptActiveMsg);
|
|
1308
1308
|
}
|
|
@@ -2130,6 +2130,33 @@ function installTunnelScripts() {
|
|
|
2130
2130
|
createTunnelSymlink(listLink, listSrc);
|
|
2131
2131
|
}
|
|
2132
2132
|
// ---------------------------------------------------------------------------
|
|
2133
|
+
// Account discovery (shared between installService + installCrons)
|
|
2134
|
+
//
|
|
2135
|
+
// Task 955 — `installService` stamps `Environment=ACCOUNT_ID=` into the brand
|
|
2136
|
+
// systemd unit so the writeNodeWithEdges gate has a non-undefined identity to
|
|
2137
|
+
// compare against; `installCrons` needs the same value to scope cron stdout
|
|
2138
|
+
// to the per-account log dir + stamp ACCOUNT_ID into the cron entry env.
|
|
2139
|
+
// Both pull from `INSTALL_DIR/data/accounts/<uuid>/account.json` written by
|
|
2140
|
+
// seed-neo4j.sh during setupAccount(). One reader, one shape, one source of
|
|
2141
|
+
// truth — without sharing, the two callers would drift on classification of
|
|
2142
|
+
// `corrupt account.json` (one might count it, the other might not) and the
|
|
2143
|
+
// gate would reject writes the cron's "scoped" log was already happily
|
|
2144
|
+
// publishing under that uuid.
|
|
2145
|
+
// ---------------------------------------------------------------------------
|
|
2146
|
+
function resolveInstallAccountId() {
|
|
2147
|
+
const accountsDir = join(INSTALL_DIR, "data/accounts");
|
|
2148
|
+
if (!existsSync(accountsDir))
|
|
2149
|
+
return "";
|
|
2150
|
+
try {
|
|
2151
|
+
for (const d of readdirSync(accountsDir)) {
|
|
2152
|
+
if (existsSync(join(accountsDir, d, "account.json")))
|
|
2153
|
+
return d;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
catch { /* directory unreadable */ }
|
|
2157
|
+
return "";
|
|
2158
|
+
}
|
|
2159
|
+
// ---------------------------------------------------------------------------
|
|
2133
2160
|
// Cron Registration
|
|
2134
2161
|
//
|
|
2135
2162
|
// Registers platform cron jobs (heartbeat, email-fetch, email-auto-respond).
|
|
@@ -2144,21 +2171,9 @@ function installCrons() {
|
|
|
2144
2171
|
return;
|
|
2145
2172
|
const nodeBin = spawnSync("which", ["node"], { encoding: "utf-8" }).stdout.trim() || "/usr/bin/node";
|
|
2146
2173
|
const platformRoot = join(INSTALL_DIR, "platform");
|
|
2147
|
-
//
|
|
2174
|
+
// Account discovery shared with installService — see resolveInstallAccountId.
|
|
2175
|
+
const accountId = resolveInstallAccountId();
|
|
2148
2176
|
const accountsDir = join(INSTALL_DIR, "data/accounts");
|
|
2149
|
-
let accountId = "";
|
|
2150
|
-
if (existsSync(accountsDir)) {
|
|
2151
|
-
try {
|
|
2152
|
-
const dirs = readdirSync(accountsDir);
|
|
2153
|
-
for (const d of dirs) {
|
|
2154
|
-
if (existsSync(join(accountsDir, d, "account.json"))) {
|
|
2155
|
-
accountId = d;
|
|
2156
|
-
break;
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
catch { /* directory unreadable */ }
|
|
2161
|
-
}
|
|
2162
2177
|
if (!accountId) {
|
|
2163
2178
|
console.error(" Cron jobs: skipped — no account found. Crons will register on the next install after account creation.");
|
|
2164
2179
|
logFile(" cron registration skipped: no account directory with account.json found");
|
|
@@ -2646,6 +2661,18 @@ function installService() {
|
|
|
2646
2661
|
checkInstallPortFree("rfbPort", RFB_PORT);
|
|
2647
2662
|
checkInstallPortFree("websockifyPort", WEBSOCKIFY_PORT_BRAND);
|
|
2648
2663
|
checkInstallPortFree("cdpPort", CDP_PORT_BRAND);
|
|
2664
|
+
// Task 955 — ACCOUNT_ID stamped into the brand unit so the writeNodeWithEdges
|
|
2665
|
+
// gate at platform/lib/graph-write/src/index.ts:170 has a real identity to
|
|
2666
|
+
// compare against (instead of process.env.ACCOUNT_ID === undefined). Resolved
|
|
2667
|
+
// here AFTER setupAccount() ran upstream — seed-neo4j.sh wrote account.json,
|
|
2668
|
+
// so an empty resolution at this point is a corrupted install (e.g. the seed
|
|
2669
|
+
// failed silently, or accounts/ was wiped between setup and unit-write).
|
|
2670
|
+
const installAccountId = resolveInstallAccountId();
|
|
2671
|
+
if (!installAccountId) {
|
|
2672
|
+
throw new Error(`installService: no account discovered at ${INSTALL_DIR}/data/accounts/<uuid>/account.json — ` +
|
|
2673
|
+
`setupAccount() (seed-neo4j.sh) should have created one. Refusing to write a systemd unit ` +
|
|
2674
|
+
`without ACCOUNT_ID; the boot validator would FATAL on every restart.`);
|
|
2675
|
+
}
|
|
2649
2676
|
const serviceFile = buildMaxyUnitFile({
|
|
2650
2677
|
productName: BRAND.productName,
|
|
2651
2678
|
brandHostname: BRAND.hostname,
|
|
@@ -2659,6 +2686,7 @@ function installService() {
|
|
|
2659
2686
|
websockifyPort: WEBSOCKIFY_PORT_BRAND,
|
|
2660
2687
|
cdpPort: CDP_PORT_BRAND,
|
|
2661
2688
|
chromiumBin: RESOLVED_CHROMIUM_BIN, // Task 929
|
|
2689
|
+
accountId: installAccountId, // Task 955
|
|
2662
2690
|
});
|
|
2663
2691
|
writeFileSync(join(serviceDir, BRAND.serviceName), serviceFile);
|
|
2664
2692
|
// Task 647 — the edge service: always-on front door that owns the public
|
package/dist/port-resolution.js
CHANGED
|
@@ -70,6 +70,14 @@ export function resolveInstallPortFromFs(opts) {
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
export function buildMaxyUnitFile(o) {
|
|
73
|
+
if (!o.accountId) {
|
|
74
|
+
// Caller (installService) must resolve the on-disk accountId before
|
|
75
|
+
// calling. Falling through with an empty value would emit a unit file
|
|
76
|
+
// with no Environment=ACCOUNT_ID line — bootValidator would FATAL on
|
|
77
|
+
// every restart with reason=missing. Throw at build time so the install
|
|
78
|
+
// aborts loudly instead of bricking the boot loop.
|
|
79
|
+
throw new Error("buildMaxyUnitFile: accountId is required — caller must resolve the on-disk account UUID before stamping the systemd unit.");
|
|
80
|
+
}
|
|
73
81
|
const neo4jServiceDep = o.neo4jDedicated
|
|
74
82
|
? `neo4j-${o.brandHostname}.service`
|
|
75
83
|
: "neo4j.service";
|
|
@@ -91,6 +99,7 @@ WatchdogSec=30
|
|
|
91
99
|
TimeoutStartSec=60
|
|
92
100
|
TimeoutStopSec=10
|
|
93
101
|
Environment=NODE_ENV=production
|
|
102
|
+
Environment=ACCOUNT_ID=${o.accountId}
|
|
94
103
|
Environment=PORT=${o.port}
|
|
95
104
|
Environment=MAXY_UI_INTERNAL_PORT=${o.maxyUiInternalPort}
|
|
96
105
|
Environment=HOSTNAME=127.0.0.1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rubytech/create-realagent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.858",
|
|
4
4
|
"description": "Install Real Agent — Built for agents. By agents.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-realagent": "./dist/index.js"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"bundle": "node scripts/bundle.js",
|
|
12
12
|
"test": "npm run build && node --test 'dist/__tests__/*.test.js'",
|
|
13
|
-
"prepublishOnly": "bash ../../platform/scripts/verify-skill-tool-surface.sh && node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
|
|
13
|
+
"prepublishOnly": "bash ../../platform/scripts/verify-skill-tool-surface.sh && node ../../platform/scripts/check-no-task-id-leaks.mjs && node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-env.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/validate-env.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Task 955 — acceptance gate for the env-vs-disk validator. Pure-function
|
|
3
|
+
// shape (no I/O, no env read, no exit) keeps the test fast and lets the
|
|
4
|
+
// caller (platform/ui/server/index.ts) own the side-effects (console.error +
|
|
5
|
+
// process.exit). The four cases cover every observable boot state — missing,
|
|
6
|
+
// no-on-disk-account, mismatch, ok — with no fallback path.
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
12
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
13
|
+
const index_js_1 = require("../index.js");
|
|
14
|
+
const UUID_A = "11111111-2222-3333-4444-555555555555";
|
|
15
|
+
const UUID_B = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
|
16
|
+
(0, node_test_1.default)("validateAccountIdEnv ok when env matches a disk account", () => {
|
|
17
|
+
const result = (0, index_js_1.validateAccountIdEnv)(UUID_A, [UUID_A]);
|
|
18
|
+
strict_1.default.deepEqual(result, { ok: true, envId: UUID_A, diskIds: [UUID_A] });
|
|
19
|
+
});
|
|
20
|
+
(0, node_test_1.default)("validateAccountIdEnv FATAL reason=missing when env is undefined", () => {
|
|
21
|
+
const result = (0, index_js_1.validateAccountIdEnv)(undefined, [UUID_A]);
|
|
22
|
+
strict_1.default.equal(result.ok, false);
|
|
23
|
+
strict_1.default.equal(result.ok ? null : result.reason, "missing");
|
|
24
|
+
strict_1.default.equal(result.envId, null);
|
|
25
|
+
});
|
|
26
|
+
(0, node_test_1.default)("validateAccountIdEnv FATAL reason=missing when env is the empty string", () => {
|
|
27
|
+
// Empty string is the systemd shape when `Environment=ACCOUNT_ID=` lands
|
|
28
|
+
// without a value (e.g. an installer regression that interpolates an empty
|
|
29
|
+
// template variable). Same FATAL classification as undefined — we never
|
|
30
|
+
// accept a falsy env value and silently degrade.
|
|
31
|
+
const result = (0, index_js_1.validateAccountIdEnv)("", [UUID_A]);
|
|
32
|
+
strict_1.default.equal(result.ok, false);
|
|
33
|
+
strict_1.default.equal(result.ok ? null : result.reason, "missing");
|
|
34
|
+
});
|
|
35
|
+
(0, node_test_1.default)("validateAccountIdEnv FATAL reason=no-on-disk-account when disk is empty", () => {
|
|
36
|
+
const result = (0, index_js_1.validateAccountIdEnv)(UUID_A, []);
|
|
37
|
+
strict_1.default.equal(result.ok, false);
|
|
38
|
+
strict_1.default.equal(result.ok ? null : result.reason, "no-on-disk-account");
|
|
39
|
+
strict_1.default.equal(result.envId, UUID_A);
|
|
40
|
+
strict_1.default.deepEqual(result.diskIds, []);
|
|
41
|
+
});
|
|
42
|
+
(0, node_test_1.default)("validateAccountIdEnv FATAL reason=mismatch when env not in disk list", () => {
|
|
43
|
+
const result = (0, index_js_1.validateAccountIdEnv)(UUID_A, [UUID_B]);
|
|
44
|
+
strict_1.default.equal(result.ok, false);
|
|
45
|
+
strict_1.default.equal(result.ok ? null : result.reason, "mismatch");
|
|
46
|
+
strict_1.default.equal(result.envId, UUID_A);
|
|
47
|
+
strict_1.default.deepEqual(result.diskIds, [UUID_B]);
|
|
48
|
+
});
|
|
49
|
+
(0, node_test_1.default)("validateAccountIdEnv ok when env matches one of multiple disk accounts", () => {
|
|
50
|
+
// Phase 0 invariant is single-account, but the validator is shape-agnostic;
|
|
51
|
+
// future multi-account expansion should not require a code change here.
|
|
52
|
+
const result = (0, index_js_1.validateAccountIdEnv)(UUID_B, [UUID_A, UUID_B]);
|
|
53
|
+
strict_1.default.equal(result.ok, true);
|
|
54
|
+
});
|
|
55
|
+
//# sourceMappingURL=validate-env.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-env.test.js","sourceRoot":"","sources":["../../src/__tests__/validate-env.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,4 +20,30 @@ export declare function getAccountsDirFromEnv(): string;
|
|
|
20
20
|
* is a deliberate boot-time invariant (see module doc).
|
|
21
21
|
*/
|
|
22
22
|
export declare function _resetEnumerationCache(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Task 955 — boot-time env-vs-disk validator. Compares `process.env.ACCOUNT_ID`
|
|
25
|
+
* (stamped by the brand systemd unit's `Environment=ACCOUNT_ID=` line) against
|
|
26
|
+
* the on-disk account set returned by `enumerateValidAccountIds`. The Hono
|
|
27
|
+
* server calls this once at boot before binding the listener; on FATAL it
|
|
28
|
+
* emits a structured `[graph-health] account-id-env FATAL` line and exits 1
|
|
29
|
+
* so systemd's restart loop surfaces the misconfiguration in journalctl.
|
|
30
|
+
*
|
|
31
|
+
* Pure function — no I/O, no env reads, no exits — caller passes both inputs.
|
|
32
|
+
* Reasons enumerate the four observable boot states (success + three failures)
|
|
33
|
+
* with no fallback path; the writeNodeWithEdges gate cannot trust an env that
|
|
34
|
+
* does not match disk, and silently degrading would re-create the silent-leak
|
|
35
|
+
* class the gate exists to close (`.docs/neo4j.md` "Account isolation invariant").
|
|
36
|
+
*/
|
|
37
|
+
export type AccountIdEnvValidationFailureReason = "missing" | "no-on-disk-account" | "mismatch";
|
|
38
|
+
export type AccountIdEnvValidation = {
|
|
39
|
+
ok: true;
|
|
40
|
+
envId: string;
|
|
41
|
+
diskIds: string[];
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
reason: AccountIdEnvValidationFailureReason;
|
|
45
|
+
envId: string | null;
|
|
46
|
+
diskIds: string[];
|
|
47
|
+
};
|
|
48
|
+
export declare function validateAccountIdEnv(envValue: string | undefined, diskIds: string[]): AccountIdEnvValidation;
|
|
23
49
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAgCtE;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAS9C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAgCtE;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAS9C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,mCAAmC,GAC3C,SAAS,GACT,oBAAoB,GACpB,UAAU,CAAC;AAEf,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GAC9C;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,mCAAmC,CAAC;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEN,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,EAAE,MAAM,EAAE,GAChB,sBAAsB,CAWxB"}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enumerateValidAccountIds = enumerateValidAccountIds;
|
|
4
4
|
exports.getAccountsDirFromEnv = getAccountsDirFromEnv;
|
|
5
5
|
exports._resetEnumerationCache = _resetEnumerationCache;
|
|
6
|
+
exports.validateAccountIdEnv = validateAccountIdEnv;
|
|
6
7
|
/**
|
|
7
8
|
* account-enumeration — single source of truth for "which accountIds are
|
|
8
9
|
* provisioned on disk for this install?".
|
|
@@ -93,4 +94,16 @@ function getAccountsDirFromEnv() {
|
|
|
93
94
|
function _resetEnumerationCache() {
|
|
94
95
|
cache.clear();
|
|
95
96
|
}
|
|
97
|
+
function validateAccountIdEnv(envValue, diskIds) {
|
|
98
|
+
if (!envValue) {
|
|
99
|
+
return { ok: false, reason: "missing", envId: null, diskIds };
|
|
100
|
+
}
|
|
101
|
+
if (diskIds.length === 0) {
|
|
102
|
+
return { ok: false, reason: "no-on-disk-account", envId: envValue, diskIds };
|
|
103
|
+
}
|
|
104
|
+
if (!diskIds.includes(envValue)) {
|
|
105
|
+
return { ok: false, reason: "mismatch", envId: envValue, diskIds };
|
|
106
|
+
}
|
|
107
|
+
return { ok: true, envId: envValue, diskIds };
|
|
108
|
+
}
|
|
96
109
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAoCA,4DAgCC;AAUD,sDASC;AAMD,wDAEC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Task 955 — acceptance gate for the env-vs-disk validator. Pure-function
|
|
2
|
+
// shape (no I/O, no env read, no exit) keeps the test fast and lets the
|
|
3
|
+
// caller (platform/ui/server/index.ts) own the side-effects (console.error +
|
|
4
|
+
// process.exit). The four cases cover every observable boot state — missing,
|
|
5
|
+
// no-on-disk-account, mismatch, ok — with no fallback path.
|
|
6
|
+
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { validateAccountIdEnv } from "../index.js";
|
|
10
|
+
|
|
11
|
+
const UUID_A = "11111111-2222-3333-4444-555555555555";
|
|
12
|
+
const UUID_B = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
|
13
|
+
|
|
14
|
+
test("validateAccountIdEnv ok when env matches a disk account", () => {
|
|
15
|
+
const result = validateAccountIdEnv(UUID_A, [UUID_A]);
|
|
16
|
+
assert.deepEqual(result, { ok: true, envId: UUID_A, diskIds: [UUID_A] });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("validateAccountIdEnv FATAL reason=missing when env is undefined", () => {
|
|
20
|
+
const result = validateAccountIdEnv(undefined, [UUID_A]);
|
|
21
|
+
assert.equal(result.ok, false);
|
|
22
|
+
assert.equal(result.ok ? null : result.reason, "missing");
|
|
23
|
+
assert.equal(result.envId, null);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("validateAccountIdEnv FATAL reason=missing when env is the empty string", () => {
|
|
27
|
+
// Empty string is the systemd shape when `Environment=ACCOUNT_ID=` lands
|
|
28
|
+
// without a value (e.g. an installer regression that interpolates an empty
|
|
29
|
+
// template variable). Same FATAL classification as undefined — we never
|
|
30
|
+
// accept a falsy env value and silently degrade.
|
|
31
|
+
const result = validateAccountIdEnv("", [UUID_A]);
|
|
32
|
+
assert.equal(result.ok, false);
|
|
33
|
+
assert.equal(result.ok ? null : result.reason, "missing");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("validateAccountIdEnv FATAL reason=no-on-disk-account when disk is empty", () => {
|
|
37
|
+
const result = validateAccountIdEnv(UUID_A, []);
|
|
38
|
+
assert.equal(result.ok, false);
|
|
39
|
+
assert.equal(result.ok ? null : result.reason, "no-on-disk-account");
|
|
40
|
+
assert.equal(result.envId, UUID_A);
|
|
41
|
+
assert.deepEqual(result.diskIds, []);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("validateAccountIdEnv FATAL reason=mismatch when env not in disk list", () => {
|
|
45
|
+
const result = validateAccountIdEnv(UUID_A, [UUID_B]);
|
|
46
|
+
assert.equal(result.ok, false);
|
|
47
|
+
assert.equal(result.ok ? null : result.reason, "mismatch");
|
|
48
|
+
assert.equal(result.envId, UUID_A);
|
|
49
|
+
assert.deepEqual(result.diskIds, [UUID_B]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("validateAccountIdEnv ok when env matches one of multiple disk accounts", () => {
|
|
53
|
+
// Phase 0 invariant is single-account, but the validator is shape-agnostic;
|
|
54
|
+
// future multi-account expansion should not require a code change here.
|
|
55
|
+
const result = validateAccountIdEnv(UUID_B, [UUID_A, UUID_B]);
|
|
56
|
+
assert.equal(result.ok, true);
|
|
57
|
+
});
|
|
@@ -94,3 +94,47 @@ export function getAccountsDirFromEnv(): string {
|
|
|
94
94
|
export function _resetEnumerationCache(): void {
|
|
95
95
|
cache.clear();
|
|
96
96
|
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Task 955 — boot-time env-vs-disk validator. Compares `process.env.ACCOUNT_ID`
|
|
100
|
+
* (stamped by the brand systemd unit's `Environment=ACCOUNT_ID=` line) against
|
|
101
|
+
* the on-disk account set returned by `enumerateValidAccountIds`. The Hono
|
|
102
|
+
* server calls this once at boot before binding the listener; on FATAL it
|
|
103
|
+
* emits a structured `[graph-health] account-id-env FATAL` line and exits 1
|
|
104
|
+
* so systemd's restart loop surfaces the misconfiguration in journalctl.
|
|
105
|
+
*
|
|
106
|
+
* Pure function — no I/O, no env reads, no exits — caller passes both inputs.
|
|
107
|
+
* Reasons enumerate the four observable boot states (success + three failures)
|
|
108
|
+
* with no fallback path; the writeNodeWithEdges gate cannot trust an env that
|
|
109
|
+
* does not match disk, and silently degrading would re-create the silent-leak
|
|
110
|
+
* class the gate exists to close (`.docs/neo4j.md` "Account isolation invariant").
|
|
111
|
+
*/
|
|
112
|
+
export type AccountIdEnvValidationFailureReason =
|
|
113
|
+
| "missing"
|
|
114
|
+
| "no-on-disk-account"
|
|
115
|
+
| "mismatch";
|
|
116
|
+
|
|
117
|
+
export type AccountIdEnvValidation =
|
|
118
|
+
| { ok: true; envId: string; diskIds: string[] }
|
|
119
|
+
| {
|
|
120
|
+
ok: false;
|
|
121
|
+
reason: AccountIdEnvValidationFailureReason;
|
|
122
|
+
envId: string | null;
|
|
123
|
+
diskIds: string[];
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export function validateAccountIdEnv(
|
|
127
|
+
envValue: string | undefined,
|
|
128
|
+
diskIds: string[],
|
|
129
|
+
): AccountIdEnvValidation {
|
|
130
|
+
if (!envValue) {
|
|
131
|
+
return { ok: false, reason: "missing", envId: null, diskIds };
|
|
132
|
+
}
|
|
133
|
+
if (diskIds.length === 0) {
|
|
134
|
+
return { ok: false, reason: "no-on-disk-account", envId: envValue, diskIds };
|
|
135
|
+
}
|
|
136
|
+
if (!diskIds.includes(envValue)) {
|
|
137
|
+
return { ok: false, reason: "mismatch", envId: envValue, diskIds };
|
|
138
|
+
}
|
|
139
|
+
return { ok: true, envId: envValue, diskIds };
|
|
140
|
+
}
|
|
@@ -43,7 +43,7 @@ if [ "$AGENT_TYPE" = "admin" ]; then
|
|
|
43
43
|
# Patterns intentionally cover both absolute (*/...) and relative
|
|
44
44
|
# (no leading slash) paths so an agent can't bypass via cwd-relative writes.
|
|
45
45
|
*/platform/lib/entitlement/*|platform/lib/entitlement/*|*/entitlement.json|entitlement.json)
|
|
46
|
-
echo "Blocked: Admin agent cannot modify entitlement files at $FILE_PATH
|
|
46
|
+
echo "Blocked: Admin agent cannot modify entitlement files at $FILE_PATH. Effective tier and purchasedPlugins derive from a Rubytech-signed payload." >&2
|
|
47
47
|
echo "[entitlement] tool-deny: tool=${TOOL_NAME} path=${FILE_PATH} field=entitlement-file" >&2
|
|
48
48
|
exit 2
|
|
49
49
|
;;
|
|
@@ -76,7 +76,7 @@ if [ "$AGENT_TYPE" = "admin" ]; then
|
|
|
76
76
|
;;
|
|
77
77
|
# Entitlement files via shell (Task 831) — same surface, blocked symmetrically
|
|
78
78
|
*"platform/lib/entitlement/"*|*"entitlement.json"*)
|
|
79
|
-
echo "Blocked: Admin agent cannot reference entitlement files via shell
|
|
79
|
+
echo "Blocked: Admin agent cannot reference entitlement files via shell." >&2
|
|
80
80
|
echo "[entitlement] tool-deny: tool=Bash path=entitlement-file field=entitlement-file" >&2
|
|
81
81
|
exit 2
|
|
82
82
|
;;
|
|
@@ -19,7 +19,7 @@ export function getDriver() {
|
|
|
19
19
|
// vector for Task 577's orphan data in Maxy's DB from a realagent install.
|
|
20
20
|
const uri = process.env.NEO4J_URI;
|
|
21
21
|
if (!uri) {
|
|
22
|
-
throw new Error("[admin] NEO4J_URI unset — refusing to default to bolt://localhost:7687
|
|
22
|
+
throw new Error("[admin] NEO4J_URI unset — refusing to default to bolt://localhost:7687");
|
|
23
23
|
}
|
|
24
24
|
const user = process.env.NEO4J_USER ?? "neo4j";
|
|
25
25
|
const password = readPassword();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"neo4j.js","sourceRoot":"","sources":["../../src/lib/neo4j.ts"],"names":[],"mappings":"AAAA,OAAO,KAA0B,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,IAAI,MAAM,GAAkB,IAAI,CAAC;AAEjC,SAAS,YAAY;IACnB,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAElE,MAAM,YAAY,GAAG,OAAO,CAC1B,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,EACxE,wBAAwB,CACzB,CAAC;IAEF,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,yCAAyC,YAAY,gCAAgC,CACtF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,wEAAwE;QACxE,2EAA2E;QAC3E,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;QAClC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CACb,
|
|
1
|
+
{"version":3,"file":"neo4j.js","sourceRoot":"","sources":["../../src/lib/neo4j.ts"],"names":[],"mappings":"AAAA,OAAO,KAA0B,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,IAAI,MAAM,GAAkB,IAAI,CAAC;AAEjC,SAAS,YAAY;IACnB,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAElE,MAAM,YAAY,GAAG,OAAO,CAC1B,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,EACxE,wBAAwB,CACzB,CAAC;IAEF,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,yCAAyC,YAAY,gCAAgC,CACtF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,wEAAwE;QACxE,2EAA2E;QAC3E,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;QAClC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CACb,wEAAwE,CACzE,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC;QAC/C,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;QAChC,OAAO,CAAC,KAAK,CAAC,8BAA8B,GAAG,EAAE,CAAC,CAAC;QACnD,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,SAAS,EAAE,CAAC,OAAO,EAAE,CAAC;AAC/B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,GAAG,IAAI,CAAC;IAChB,CAAC;AACH,CAAC"}
|
|
@@ -50,7 +50,7 @@ require_stream_log_path() {
|
|
|
50
50
|
echo " This script tees subprocess output into a per-conversation" >&2
|
|
51
51
|
echo " stream log; it is meaningless without a target path." >&2
|
|
52
52
|
echo " The platform sets STREAM_LOG_PATH automatically for every" >&2
|
|
53
|
-
echo " \`claude\` spawn
|
|
53
|
+
echo " \`claude\` spawn. If you are invoking this" >&2
|
|
54
54
|
echo " script by hand, export it yourself, for example:" >&2
|
|
55
55
|
echo " export STREAM_LOG_PATH=\"\${HOME}/.maxy/logs/manual-invocation.log\"" >&2
|
|
56
56
|
exit 1
|
|
@@ -86,14 +86,41 @@ fi
|
|
|
86
86
|
# on Ubuntu Noble laptop. The installer writes this file under platform/config/
|
|
87
87
|
# during installSystemDeps; the spawn at step=browser-spawn fails loud if the
|
|
88
88
|
# file is absent rather than silently falling back to /usr/bin/chromium.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
#
|
|
90
|
+
# Task 957 — four discrete branches (env-unset / file-missing / file-empty /
|
|
91
|
+
# binary-not-executable) so the failure message names the actual condition.
|
|
92
|
+
# Pre-957 the four were conflated into one "cannot resolve a non-snap Chromium
|
|
93
|
+
# binary" message, which masked the env-propagation defect that closed three
|
|
94
|
+
# prior tasks (836, 562, 862) and silently re-fired here.
|
|
95
|
+
if [ -z "${MAXY_PLATFORM_ROOT:-}" ]; then
|
|
96
|
+
phase_line setup-tunnel step=chromium-resolve result=error reason=env-unset var=MAXY_PLATFORM_ROOT
|
|
97
|
+
echo "ERROR: setup-tunnel.sh: MAXY_PLATFORM_ROOT is unset in the script's env." >&2
|
|
98
|
+
echo " The action runner must declare MAXY_PLATFORM_ROOT in the cloudflare-setup" >&2
|
|
99
|
+
echo " whitelist env: block (platform/ui/server/lib/action-runner.ts) — systemd-run --user" >&2
|
|
100
|
+
echo " does not inherit the parent process env into the transient unit." >&2
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
SETUP_TUNNEL_CHROMIUM_PATH_FILE="${MAXY_PLATFORM_ROOT}/config/chromium-binary.path"
|
|
104
|
+
if [ ! -r "${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" ]; then
|
|
105
|
+
phase_line setup-tunnel step=chromium-resolve result=error reason=path-file-missing path="${SETUP_TUNNEL_CHROMIUM_PATH_FILE}"
|
|
106
|
+
echo "ERROR: setup-tunnel.sh: chromium-binary.path file is missing or unreadable." >&2
|
|
107
|
+
echo " Expected: ${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" >&2
|
|
108
|
+
echo " Re-run the installer to provision Chromium." >&2
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
SETUP_TUNNEL_CHROMIUM_BIN="$(head -n1 "${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" | tr -d '[:space:]')"
|
|
112
|
+
if [ -z "${SETUP_TUNNEL_CHROMIUM_BIN}" ]; then
|
|
113
|
+
phase_line setup-tunnel step=chromium-resolve result=error reason=path-file-empty path="${SETUP_TUNNEL_CHROMIUM_PATH_FILE}"
|
|
114
|
+
echo "ERROR: setup-tunnel.sh: chromium-binary.path is empty." >&2
|
|
115
|
+
echo " File: ${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" >&2
|
|
116
|
+
echo " Re-run the installer to provision Chromium." >&2
|
|
117
|
+
exit 1
|
|
92
118
|
fi
|
|
93
|
-
if [
|
|
94
|
-
|
|
95
|
-
echo "
|
|
96
|
-
echo "
|
|
119
|
+
if [ ! -x "${SETUP_TUNNEL_CHROMIUM_BIN}" ]; then
|
|
120
|
+
phase_line setup-tunnel step=chromium-resolve result=error reason=binary-not-executable bin="${SETUP_TUNNEL_CHROMIUM_BIN}" path="${SETUP_TUNNEL_CHROMIUM_PATH_FILE}"
|
|
121
|
+
echo "ERROR: setup-tunnel.sh: ${SETUP_TUNNEL_CHROMIUM_BIN} is not an executable file." >&2
|
|
122
|
+
echo " Resolved from: ${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" >&2
|
|
123
|
+
echo " Re-run the installer to provision Chromium." >&2
|
|
97
124
|
exit 1
|
|
98
125
|
fi
|
|
99
126
|
|
|
@@ -644,7 +671,7 @@ if ! command -v systemd-run >/dev/null 2>&1; then
|
|
|
644
671
|
reason=systemd-run-missing
|
|
645
672
|
echo "ERROR: systemd-run is not in PATH." >&2
|
|
646
673
|
echo " The script dispatches the ${BRAND}.service restart to a transient" >&2
|
|
647
|
-
echo " systemd user unit so it does not kill its own cgroup
|
|
674
|
+
echo " systemd user unit so it does not kill its own cgroup." >&2
|
|
648
675
|
echo " Install systemd userspace (standard on supported Maxy Pi images)." >&2
|
|
649
676
|
exit 1
|
|
650
677
|
fi
|
|
@@ -663,7 +690,7 @@ phase_line setup-tunnel step=service-restart-dispatched \
|
|
|
663
690
|
# failure reason (e.g. "Failed to connect to bus" when linger is disabled).
|
|
664
691
|
SYSTEMD_RUN_ERR="$(mktemp -t maxy-systemd-run-err.XXXXXX)"
|
|
665
692
|
if systemd-run --user --unit="${TRANSIENT_UNIT}.service" \
|
|
666
|
-
--description="Detached restart of ${BRAND}.service
|
|
693
|
+
--description="Detached restart of ${BRAND}.service" \
|
|
667
694
|
--on-active="${RESTART_DELAY}s" \
|
|
668
695
|
--collect \
|
|
669
696
|
/bin/systemctl --user restart "${BRAND}.service" 2>"${SYSTEMD_RUN_ERR}"; then
|
|
@@ -19,7 +19,7 @@ export function getDriver() {
|
|
|
19
19
|
// vector for Task 577's orphan data in Maxy's DB from a realagent install.
|
|
20
20
|
const uri = process.env.NEO4J_URI;
|
|
21
21
|
if (!uri) {
|
|
22
|
-
throw new Error("[contacts] NEO4J_URI unset — refusing to default to bolt://localhost:7687
|
|
22
|
+
throw new Error("[contacts] NEO4J_URI unset — refusing to default to bolt://localhost:7687");
|
|
23
23
|
}
|
|
24
24
|
const user = process.env.NEO4J_USER ?? "neo4j";
|
|
25
25
|
const password = readPassword();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"neo4j.js","sourceRoot":"","sources":["../../src/lib/neo4j.ts"],"names":[],"mappings":"AAAA,OAAO,KAA0B,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,IAAI,MAAM,GAAkB,IAAI,CAAC;AAEjC,SAAS,YAAY;IACnB,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAElE,MAAM,YAAY,GAAG,OAAO,CAC1B,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,EACxE,wBAAwB,CACzB,CAAC;IAEF,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,yCAAyC,YAAY,gCAAgC,CACtF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,wEAAwE;QACxE,2EAA2E;QAC3E,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;QAClC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CACb,
|
|
1
|
+
{"version":3,"file":"neo4j.js","sourceRoot":"","sources":["../../src/lib/neo4j.ts"],"names":[],"mappings":"AAAA,OAAO,KAA0B,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,IAAI,MAAM,GAAkB,IAAI,CAAC;AAEjC,SAAS,YAAY;IACnB,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAElE,MAAM,YAAY,GAAG,OAAO,CAC1B,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,EACxE,wBAAwB,CACzB,CAAC;IAEF,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,yCAAyC,YAAY,gCAAgC,CACtF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,wEAAwE;QACxE,2EAA2E;QAC3E,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;QAClC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CACb,2EAA2E,CAC5E,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC;QAC/C,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;QAChC,OAAO,CAAC,KAAK,CAAC,iCAAiC,GAAG,EAAE,CAAC,CAAC;QACtD,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,SAAS,EAAE,CAAC,OAAO,EAAE,CAAC;AAC/B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,GAAG,IAAI,CAAC;IAChB,CAAC;AACH,CAAC"}
|