@rubytech/create-sitedesk-code 0.1.524 → 0.1.526
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__/samba-provision.test.js +46 -1
- package/dist/__tests__/websockify-bind-wiring.test.js +47 -0
- package/dist/index.js +39 -1
- package/dist/samba-provision.js +37 -0
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +8 -7
- package/payload/platform/plugins/admin/skills/superpowers-sprint/SKILL.md +26 -0
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/memory/PLUGIN.md +10 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +5 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js +141 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +6 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts +45 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js +122 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js +26 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js +201 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +89 -33
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +2 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +6 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js +148 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +83 -6
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +90 -5
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js +173 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +73 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +194 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +25 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +81 -8
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js +39 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +9 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
- package/payload/platform/scripts/__tests__/check-canonical-tool-names.test.sh +162 -0
- package/payload/platform/scripts/__tests__/check-no-task-id-leaks.test.sh +53 -0
- package/payload/platform/scripts/__tests__/task-id-citation.test.sh +72 -0
- package/payload/platform/scripts/check-canonical-tool-names.mjs +36 -4
- package/payload/platform/scripts/check-no-task-id-leaks.mjs +80 -62
- package/payload/platform/scripts/lib/canonical-tool-names.mjs +130 -26
- package/payload/platform/scripts/lib/task-id-citation.mjs +67 -0
- package/payload/platform/scripts/vnc.sh +45 -1
- package/payload/server/{chunk-VUXKXOJU.js → chunk-7MGR5M6G.js} +4 -2
- package/payload/server/{manager-4TJK3YOY.js → manager-6XOWHREK.js} +1 -1
- package/payload/server/server.js +10 -4
- package/payload/platform/.docs/search-surface-contract.md +0 -58
- package/payload/platform/docs/superpowers/plans/2026-06-02-task-610-follower-202-retry.md +0 -372
- package/payload/platform/docs/superpowers/plans/2026-06-04-public-agent-knowledge-delivery.md +0 -230
- package/payload/platform/docs/superpowers/plans/2026-06-23-account-filesystem-schema.md +0 -544
- package/payload/platform/docs/superpowers/plans/2026-07-11-task-1557-operator-on-behalf-preference-attribution.md +0 -542
- package/payload/platform/docs/superpowers/plans/2026-07-13-account-schema-ontology-projection.md +0 -547
- package/payload/platform/docs/superpowers/plans/2026-07-14-graph-top-level-labels-ontology-single-source.md +0 -458
- package/payload/platform/docs/superpowers/plans/2026-07-17-task-1736-retire-mcp-stderr-tee.md +0 -397
- package/payload/platform/docs/superpowers/plans/2026-07-18-graph-native-ledger.md +0 -807
- package/payload/platform/docs/superpowers/plans/2026-07-18-ledger-write-path-fix.md +0 -109
- package/payload/platform/docs/superpowers/plans/2026-07-20-storage-pages-create.md +0 -82
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1704-data-portal-standing-audit.md +0 -673
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +0 -1244
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +0 -462
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +0 -321
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1829-platform-ui-typecheck.md +0 -830
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +0 -2177
- package/payload/platform/docs/superpowers/plans/2026-07-21-graph-caption-resolver.md +0 -124
- package/payload/platform/docs/superpowers/plans/2026-07-21-task-1877-bash-schema-enforcement.md +0 -107
- package/payload/platform/docs/superpowers/plans/2026-07-21-task-1887-reconcile-allowed-top-level.md +0 -205
- package/payload/platform/docs/superpowers/plans/2026-07-22-task-1899-reconcile-report-only.md +0 -748
- package/payload/platform/docs/superpowers/plans/2026-07-22-task-1910-two-way-portal-exchange.md +0 -1716
- package/payload/platform/docs/superpowers/plans/2026-07-23-task-1930-adherence-enforcement.md +0 -422
- package/payload/platform/docs/superpowers/plans/2026-07-24-task-1942-subaccount-switcher-brand-head.md +0 -327
- package/payload/platform/docs/superpowers/plans/2026-07-25-task-1974-uploads-intake-inbox.md +0 -235
- package/payload/platform/docs/superpowers/plans/2026-07-25-task-1976-intra-folder-hygiene.md +0 -215
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-1902-account-owned-entry-declarations.md +0 -555
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-1926-portal-push-concurrency-guard.md +0 -482
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-2023-sidebar-sessions-async-reads.md +0 -394
- package/payload/platform/docs/superpowers/plans/2026-07-27-task-2016-disabled-agent-routing.md +0 -624
- package/payload/platform/docs/superpowers/plans/2026-07-27-task-2028-declared-file-write-deny.md +0 -303
- package/payload/platform/docs/superpowers/plans/2026-07-27-task-2052-account-machinery-write-fence.md +0 -250
- package/payload/platform/docs/superpowers/plans/2026-07-28-task-2097-email-signature-set.md +0 -65
- package/payload/platform/docs/superpowers/specs/2026-06-02-task-610-follower-202-retry-design.md +0 -116
- package/payload/platform/docs/superpowers/specs/2026-06-23-account-filesystem-schema-design.md +0 -170
- package/payload/platform/docs/superpowers/specs/2026-07-11-task-1557-operator-on-behalf-preference-attribution-design.md +0 -90
- package/payload/platform/docs/superpowers/specs/2026-07-13-account-schema-ontology-projection-design.md +0 -178
- package/payload/platform/docs/superpowers/specs/2026-07-14-graph-top-level-labels-ontology-single-source-design.md +0 -119
- package/payload/platform/docs/superpowers/specs/2026-07-17-task-1736-retire-mcp-stderr-tee-design.md +0 -128
- package/payload/platform/docs/superpowers/specs/2026-07-18-graph-native-ledger-design.md +0 -241
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1704-data-portal-standing-audit-design.md +0 -177
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +0 -201
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +0 -217
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +0 -80
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1820-cpu-triage-admin-tools-design.md +0 -97
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1829-platform-ui-typecheck-design.md +0 -163
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +0 -163
- package/payload/platform/docs/superpowers/specs/2026-07-22-task-1899-reconcile-report-only-design.md +0 -121
- package/payload/platform/docs/superpowers/specs/2026-07-22-task-1910-two-way-portal-exchange-design.md +0 -103
- package/payload/platform/docs/superpowers/specs/2026-07-23-task-1930-adherence-enforcement-design.md +0 -81
- package/payload/platform/docs/superpowers/specs/2026-07-24-task-1942-subaccount-switcher-brand-head-design.md +0 -35
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-1902-account-owned-entry-declarations-design.md +0 -232
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-1926-portal-push-concurrency-guard-design.md +0 -200
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-2014-ui-suite-nondeterminism-design.md +0 -169
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2016-disabled-agent-routing-design.md +0 -139
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md +0 -127
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2052-account-machinery-write-fence-design.md +0 -150
- package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +0 -155
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// style and runs under `node --test dist/__tests__/*.test.js` after build.
|
|
7
7
|
import test from "node:test";
|
|
8
8
|
import assert from "node:assert/strict";
|
|
9
|
-
import { renderBrandStanza, renderGlobalSection, renderFullSmbConf, isPrivateIPv4, pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, mergeSmbConf, removeBrandStanza, hasAnyBrandStanza, formatSambaMarker, SAMBA_STEPS, SAMBA_ENABLE_UNITS, } from "../samba-provision.js";
|
|
9
|
+
import { renderBrandStanza, renderGlobalSection, renderFullSmbConf, isPrivateIPv4, pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, pickWebsockifyBind, formatWebsockifyBindFile, mergeSmbConf, removeBrandStanza, hasAnyBrandStanza, formatSambaMarker, SAMBA_STEPS, SAMBA_ENABLE_UNITS, } from "../samba-provision.js";
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Fixtures
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -384,6 +384,51 @@ test("formatSambaMarker emits the `[install-invariant] samba-provision-<step> <s
|
|
|
384
384
|
assert.equal(formatSambaMarker("units", "fail: Job for smbd.service failed"), "[install-invariant] samba-provision-units fail: Job for smbd.service failed");
|
|
385
385
|
});
|
|
386
386
|
// ---------------------------------------------------------------------------
|
|
387
|
+
// pickWebsockifyBind — the noVNC transport's host bind posture (Task 2089)
|
|
388
|
+
//
|
|
389
|
+
// The mapper reads the edge decision and nothing else, so a host can never end
|
|
390
|
+
// up with a public noVNC page behind a loopback edge. The `kind` passthrough is
|
|
391
|
+
// what lets the runtime log line distinguish `loopback-only` from `none`, which
|
|
392
|
+
// map to the same address.
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
test("pickWebsockifyBind: LAN host keeps the wildcard bind", () => {
|
|
395
|
+
// Byte-identical to the pre-2089 behaviour. The Pi's `.local` noVNC page is
|
|
396
|
+
// reached over whichever family avahi resolved, so the wildcard stays `[::]`
|
|
397
|
+
// rather than narrowing to `0.0.0.0`.
|
|
398
|
+
assert.deepEqual(pickWebsockifyBind(pickEdgeBindDecision(PI_WLAN0_ONLY)), {
|
|
399
|
+
bind: "[::]",
|
|
400
|
+
kind: "lan",
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
test("pickWebsockifyBind: public-only host binds loopback", () => {
|
|
404
|
+
assert.deepEqual(pickWebsockifyBind(pickEdgeBindDecision(HETZNER_PUBLIC_ETH0_ONLY)), {
|
|
405
|
+
bind: "127.0.0.1",
|
|
406
|
+
kind: "loopback-only",
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
test("pickWebsockifyBind: dual-homed cloud host binds loopback despite a private iface", () => {
|
|
410
|
+
// The fail-open Task 2082 closed for the edge, closed here too: a provider
|
|
411
|
+
// private network alongside a public address must not keep a public bind.
|
|
412
|
+
// `kind` stays `lan` because pickBindDecision keys on the private interface;
|
|
413
|
+
// the bind address is what the edge decision, not `kind`, dictates.
|
|
414
|
+
assert.deepEqual(pickWebsockifyBind(pickEdgeBindDecision(HETZNER_CLOUD_PUBLIC_PLUS_PRIVNET)), {
|
|
415
|
+
bind: "127.0.0.1",
|
|
416
|
+
kind: "lan",
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
test("pickWebsockifyBind: no non-loopback IPv4 binds loopback", () => {
|
|
420
|
+
assert.deepEqual(pickWebsockifyBind(pickEdgeBindDecision(PI_LO_ONLY)), {
|
|
421
|
+
bind: "127.0.0.1",
|
|
422
|
+
kind: "none",
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
test("formatWebsockifyBindFile: one parseable line, trailing newline", () => {
|
|
426
|
+
// vnc.sh parses this with `head -n1` + two sed captures, so the shape is
|
|
427
|
+
// load-bearing: two `key=value` pairs, space-separated, one line.
|
|
428
|
+
assert.equal(formatWebsockifyBindFile({ bind: "127.0.0.1", kind: "loopback-only" }), "bind=127.0.0.1 kind=loopback-only\n");
|
|
429
|
+
assert.equal(formatWebsockifyBindFile({ bind: "[::]", kind: "lan" }), "bind=[::] kind=lan\n");
|
|
430
|
+
});
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
387
432
|
// SAMBA_ENABLE_UNITS — the units step must never enable nmbd (Task 755)
|
|
388
433
|
// ---------------------------------------------------------------------------
|
|
389
434
|
test("SAMBA_ENABLE_UNITS enables smbd only — nmbd is never started", () => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Nothing else in the repo ties vnc.sh's websockify invocation to the installer
|
|
2
|
+
// that writes the file it reads, so either half could ship alone with the whole
|
|
3
|
+
// suite green. Two silent failures this closes:
|
|
4
|
+
//
|
|
5
|
+
// - installer writes the file, vnc.sh still hardcodes `[::]` → the Task 2089
|
|
6
|
+
// fix ships inert on every host, with every pickWebsockifyBind test passing;
|
|
7
|
+
// - vnc.sh reads the file, installer never writes it → resolve_websockify_bind
|
|
8
|
+
// exits 1 and the whole VNC stack is down on every host, not just cloud ones.
|
|
9
|
+
//
|
|
10
|
+
// Both files are read from disk at runtime, the same pattern as
|
|
11
|
+
// edge-bind-template-wiring.test.ts, which closes the equivalent gap for Task
|
|
12
|
+
// 2082's edge unit.
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, resolve } from "node:path";
|
|
18
|
+
// dist/__tests__/websockify-bind-wiring.test.js → ../../src/index.ts
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const INDEX_TS = resolve(here, "../../src/index.ts");
|
|
21
|
+
// dist/__tests__ → up four is the maxy-code root that holds platform/.
|
|
22
|
+
const VNC_SH = resolve(here, "../../../../platform/scripts/vnc.sh");
|
|
23
|
+
const SRC = readFileSync(INDEX_TS, "utf-8");
|
|
24
|
+
const VNC = readFileSync(VNC_SH, "utf-8");
|
|
25
|
+
test("vnc.sh no longer hardcodes the wildcard websockify bind", () => {
|
|
26
|
+
assert.doesNotMatch(VNC, /websockify --web \S+ "\[::\]:\$\{WEBSOCKIFY_PORT\}"/, "vnc.sh must take the websockify bind from platform/config/websockify-bind, not a literal [::]");
|
|
27
|
+
});
|
|
28
|
+
test("vnc.sh binds the resolved address and logs the decision", () => {
|
|
29
|
+
assert.match(VNC, /websockify --web \S+ "\$\{WEBSOCKIFY_BIND\}:\$\{WEBSOCKIFY_PORT\}"/, "the websockify invocation must interpolate the resolved bind address");
|
|
30
|
+
assert.match(VNC, /log "websockify-bind address=\$\{WEBSOCKIFY_BIND\} kind=\$\{WEBSOCKIFY_BIND_KIND\}/, "the websockify-bind token is the task's diagnostic grep — it must survive any edit");
|
|
31
|
+
});
|
|
32
|
+
test("vnc.sh resolves the bind before starting websockify", () => {
|
|
33
|
+
const resolveAt = VNC.indexOf("resolve_websockify_bind\n");
|
|
34
|
+
const startAt = VNC.indexOf("websockify --web");
|
|
35
|
+
assert.ok(resolveAt > 0, "vnc.sh must call resolve_websockify_bind");
|
|
36
|
+
assert.ok(resolveAt < startAt, "resolve_websockify_bind must run before the websockify invocation, or WEBSOCKIFY_BIND is unset");
|
|
37
|
+
});
|
|
38
|
+
test("vnc.sh refuses a bind address outside the two the mapper can produce", () => {
|
|
39
|
+
// The allow-list is what stops a hand-edited or half-written file handing
|
|
40
|
+
// websockify a wildcard on a cloud host.
|
|
41
|
+
assert.match(VNC, /"\[::\]"\|"127\.0\.0\.1"\) ;;/, "resolve_websockify_bind must allow-list the two addresses pickWebsockifyBind can emit");
|
|
42
|
+
});
|
|
43
|
+
test("the installer writes the file vnc.sh reads", () => {
|
|
44
|
+
assert.match(VNC, /config\/websockify-bind/, "vnc.sh must read platform/config/websockify-bind");
|
|
45
|
+
assert.match(SRC, /join\(configDir, "websockify-bind"\)/, "index.ts must write that same filename");
|
|
46
|
+
assert.match(SRC, /^\s*writeWebsockifyBindFile\(\);$/m, "index.ts defines the writer but never calls it — the file would never be written");
|
|
47
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@ import { classifyPortHolder } from "./preflight-port-classifier.js";
|
|
|
29
29
|
import { memoryControllerMissing, cmdlineAlreadyPatched, patchCmdline } from "./cgroup-memory-controller.js";
|
|
30
30
|
import { parsePluginList, computeInstallActions, parseExternalPlugins, findUnregisteredResyncs, discoverMarketplaceCandidateDirs, } from "./lib/plugin-install.js";
|
|
31
31
|
import { findPremiumMcpDirs } from "./lib/premium-mcp-discover.js";
|
|
32
|
-
import { pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, mergeSmbConf, formatSambaMarker, SAMBA_ENABLE_UNITS, } from "./samba-provision.js";
|
|
32
|
+
import { pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, pickWebsockifyBind, formatWebsockifyBindFile, mergeSmbConf, formatSambaMarker, SAMBA_ENABLE_UNITS, } from "./samba-provision.js";
|
|
33
33
|
import { networkInterfaces, userInfo, cpus } from "node:os";
|
|
34
34
|
const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
|
|
35
35
|
// Brand manifest — read from payload to derive all brand-specific installation values.
|
|
@@ -641,6 +641,41 @@ function writeChromiumBinaryPathFile() {
|
|
|
641
641
|
console.log(` Wrote ${target} → ${RESOLVED_CHROMIUM_BIN}`);
|
|
642
642
|
logFile(` [snap-chromium] wrote ${target} contents=${RESOLVED_CHROMIUM_BIN}`);
|
|
643
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* Write the websockify bind address to
|
|
646
|
+
* `<INSTALL_DIR>/platform/config/websockify-bind` so vnc.sh gives the noVNC
|
|
647
|
+
* transport the same host posture the edge got in Task 2082. Task 2089.
|
|
648
|
+
*
|
|
649
|
+
* The carrier is a file rather than a unit environment variable because
|
|
650
|
+
* `vnc.sh start` has two callers and only one of them is the edge. The other is
|
|
651
|
+
* `ensureVnc()` (platform/ui/app/lib/vnc.ts), which shells the script from the
|
|
652
|
+
* maxy-ui process; the maxy-ui unit carries no EDGE_HOSTNAME, so an env-var
|
|
653
|
+
* carrier would silently restore a wildcard bind on every X-down recovery. A
|
|
654
|
+
* file is also correct for a hand-run `bash vnc.sh start`, which no unit
|
|
655
|
+
* environment reaches at all.
|
|
656
|
+
*
|
|
657
|
+
* Called immediately after writeChromiumBinaryPathFile for the same reason it
|
|
658
|
+
* is: platform/config is a payload subdirectory, so this must run after
|
|
659
|
+
* deployPayload. Linux-only — darwin runs no VNC stack (installServiceDarwin
|
|
660
|
+
* excludes every VNC/X11 variable for that reason).
|
|
661
|
+
*
|
|
662
|
+
* Idempotent: re-running the installer on an unchanged host rewrites the same
|
|
663
|
+
* bytes. A host that gains or loses an interface gets the new decision on the
|
|
664
|
+
* next install, which the console and install-log lines below make visible.
|
|
665
|
+
*/
|
|
666
|
+
function writeWebsockifyBindFile() {
|
|
667
|
+
if (process.platform !== "linux") {
|
|
668
|
+
logFile(` writeWebsockifyBindFile skipped: platform=${process.platform}`);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const decision = pickWebsockifyBind(pickEdgeBindDecision(networkInterfaces()));
|
|
672
|
+
const configDir = resolve(INSTALL_DIR, "platform/config");
|
|
673
|
+
mkdirSync(configDir, { recursive: true });
|
|
674
|
+
const target = join(configDir, "websockify-bind");
|
|
675
|
+
writeFileSync(target, formatWebsockifyBindFile(decision), { mode: 0o644 });
|
|
676
|
+
console.log(` Wrote ${target} → bind=${decision.bind} kind=${decision.kind}`);
|
|
677
|
+
logFile(` [websockify-bind] wrote ${target} bind=${decision.bind} kind=${decision.kind}`);
|
|
678
|
+
}
|
|
644
679
|
// Scrub any hardware-watchdog drop-in this installer (or an earlier version)
|
|
645
680
|
// left on the box, then daemon-reexec so the live RuntimeWatchdogUSec returns
|
|
646
681
|
// to systemd's default (0 = off). The hardware-watchdog feature is retired:
|
|
@@ -5333,6 +5368,9 @@ try {
|
|
|
5333
5368
|
// value. Must run after deployPayload (config dir is
|
|
5334
5369
|
// a payload subdirectory). Linux-only; no-op on darwin/non-linux.
|
|
5335
5370
|
writeChromiumBinaryPathFile();
|
|
5371
|
+
// Same install-time-file pattern and the same post-deployPayload
|
|
5372
|
+
// requirement: vnc.sh reads this to decide the websockify bind. Task 2089.
|
|
5373
|
+
writeWebsockifyBindFile();
|
|
5336
5374
|
// scrub plaintext neo4j passwords from any pre-fix install-*.log.
|
|
5337
5375
|
// Idempotent — re-running on already-redacted logs is a no-op. Runs after
|
|
5338
5376
|
// payload deploy so the bundled redact-install-logs.sh is on disk.
|
package/dist/samba-provision.js
CHANGED
|
@@ -224,6 +224,43 @@ export function formatEdgeBindDecision(d) {
|
|
|
224
224
|
return `[edge-bind-decision] kind=${d.kind} iface=${d.iface ?? "none"} ` +
|
|
225
225
|
`private=${list(d.privateAddresses)} public=${list(d.publicAddresses)} EDGE_HOSTNAME=${d.hostname}`;
|
|
226
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Map the edge's bind decision onto websockify. Task 2089.
|
|
229
|
+
*
|
|
230
|
+
* This deliberately takes `EdgeBindDecision` rather than the interface table:
|
|
231
|
+
* the noVNC page and the admin edge must never disagree about whether the box
|
|
232
|
+
* is publicly reachable, and reading the same decision is the only way to
|
|
233
|
+
* guarantee that. It is not a third classifier — `pickBindDecision` (Samba) and
|
|
234
|
+
* `pickEdgeBindDecision` (edge) remain the only two.
|
|
235
|
+
*
|
|
236
|
+
* The LAN outcome is the literal `[::]`, which is what shipped before this
|
|
237
|
+
* task, not `0.0.0.0`. avahi publishes an AAAA for `.local` names, so a client
|
|
238
|
+
* may reach the page over IPv6; narrowing the family would be a behaviour
|
|
239
|
+
* change this task did not ask for. `[::]` accepts IPv4 as well under the
|
|
240
|
+
* default `net.ipv6.bindv6only=0`, which is exactly why the loopback outcome
|
|
241
|
+
* has to be an explicit `127.0.0.1`.
|
|
242
|
+
*
|
|
243
|
+
* The loopback outcome binds `127.0.0.1` only, not `[::1]` and not both.
|
|
244
|
+
* websockify takes one bind address per process, so "both" would mean two
|
|
245
|
+
* processes. Measured on the netcup box 2026-07-29: `localhost` there resolves
|
|
246
|
+
* `::1` first (its image ships no `localhost` line in `/etc/hosts`), and a
|
|
247
|
+
* listener bound to `127.0.0.1` alone still served a request forwarded by
|
|
248
|
+
* `ssh -L <port>:localhost:<port>` — OpenSSH falls back from the refused `::1`.
|
|
249
|
+
* So the SSH-forward recovery path the installer prints keeps working.
|
|
250
|
+
*/
|
|
251
|
+
export function pickWebsockifyBind(d) {
|
|
252
|
+
return { bind: d.hostname === "0.0.0.0" ? "[::]" : "127.0.0.1", kind: d.kind };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Render the install-time carrier file read by `platform/scripts/vnc.sh`.
|
|
256
|
+
*
|
|
257
|
+
* The shape is load-bearing: vnc.sh takes `head -n1` and captures `bind=` and
|
|
258
|
+
* `kind=` with two seds, so this must stay one line of space-separated
|
|
259
|
+
* `key=value` pairs with a trailing newline.
|
|
260
|
+
*/
|
|
261
|
+
export function formatWebsockifyBindFile(d) {
|
|
262
|
+
return `bind=${d.bind} kind=${d.kind}\n`;
|
|
263
|
+
}
|
|
227
264
|
// ---------------------------------------------------------------------------
|
|
228
265
|
// smb.conf merge / remove
|
|
229
266
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what SiteDesk ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:15f533911e93c443dda2624cad785b44ee24ca5bcf5b992fbd96932226abd055
|
|
5
5
|
brand: sitedesk-code
|
|
6
6
|
product-name: SiteDesk
|
|
7
7
|
---
|
|
@@ -1215,7 +1215,7 @@ npx -y @rubytech/create-realagent-code@latest --uninstall
|
|
|
1215
1215
|
- **No SCP / rsync.** Updates are `npx -y @rubytech/create-maxy-code@latest …` again, never a file push from the operator's laptop.
|
|
1216
1216
|
- **Tunnel auth is OAuth; the API is permitted for the rest.** The tunnel's only auth path is `cloudflared tunnel login` in the noVNC browser over SSH forward. DNS, Pages, and D1 use the Cloudflare API with a short-lived narrow token the agent mints from an operator-provisioned master token (see the `cloudflare` plugin).
|
|
1217
1217
|
- **No shared state across brands.** Two brands on one server each have their own Neo4j port, systemd unit, VNC display, websockify port, tunnel, and persist directory.
|
|
1218
|
-
- **No public IPv4 exposure.** The Hetzner firewall opens port 22 only; every operator-facing surface is fronted by the Cloudflare tunnel. The provider firewall is no longer the only thing making that true for the
|
|
1218
|
+
- **No public IPv4 exposure.** The Hetzner firewall opens port 22 only; every operator-facing surface is fronted by the Cloudflare tunnel. The provider firewall is no longer the only thing making that true for the operator surfaces: this host carries a public IPv4, so the installer writes `EDGE_HOSTNAME=127.0.0.1` into the edge unit and records the same decision in `platform/config/websockify-bind`, which `vnc.sh` reads to bind the noVNC websockify transport. Both are loopback-only by construction, and that holds whether or not the box also has a Cloud Network attached. Confirm with `grep -E 'bind-decision|EDGE_HOSTNAME' $HOME/.<brand>/logs/install-*.log` and `grep websockify-bind $HOME/.<brand>/logs/vnc-boot.log`. Other listeners on the box are not covered; see [netcup.md](netcup.md) for what a host without a provider firewall still depends on `ufw` for.
|
|
1219
1219
|
|
|
1220
1220
|
## Smoke checklist
|
|
1221
1221
|
|
|
@@ -1415,21 +1415,22 @@ This stops and disables the systemd user-service, removes the unit file, removes
|
|
|
1415
1415
|
- **No SCP / rsync.** Updates are `npx -y @rubytech/create-maxy-code@latest …` again, never a file push from the operator's laptop.
|
|
1416
1416
|
- **Tunnel auth is OAuth; the API is permitted for the rest.** As on Hetzner.
|
|
1417
1417
|
- **No shared state across brands.** Two brands on one server each have their own Neo4j port, systemd unit, VNC display, websockify port, tunnel, and persist directory.
|
|
1418
|
-
- **No provider firewall, so `ufw` is the only network-level layer.** On Hetzner Cloud the provider firewall opens 22 only; netcup has nothing in front of the server. What keeps the
|
|
1418
|
+
- **No provider firewall, so `ufw` is the only network-level layer.** On Hetzner Cloud the provider firewall opens 22 only; netcup has nothing in front of the server. What keeps the operator surfaces private here is the bind itself. The installer classifies the host from its interface table and, because this box carries a public IPv4, binds **both** operator-facing listeners to loopback: it writes `EDGE_HOSTNAME=127.0.0.1` into the edge unit, and it records the same decision in `platform/config/websockify-bind`, which `vnc.sh` reads to bind the noVNC websockify transport. Neither surface is reachable from off the box by construction, so `ufw` is the backstop rather than the single control. Confirm both with `grep -E 'bind-decision|EDGE_HOSTNAME' $HOME/.<brand>/logs/install-*.log` and `grep websockify-bind $HOME/.<brand>/logs/vnc-boot.log`; on this host class they read `EDGE_HOSTNAME=127.0.0.1` and `address=127.0.0.1`. Other services on the box still bind all interfaces and still depend on `ufw` — Samba and CUPS among them — so anything that clears the ruleset (a flushed nftables set after a kernel change, `ufw disable` during debugging, a CCP image reinstall) exposes those. Verify from **off** the box, not with `ufw status` on it.
|
|
1419
1419
|
|
|
1420
1420
|
## Smoke checklist
|
|
1421
1421
|
|
|
1422
1422
|
Fresh-netcup smoke pass criteria:
|
|
1423
1423
|
|
|
1424
1424
|
1. Provision an RS 8000 G12 with Ubuntu 24.04 x86_64 and an SSH key; SSH in as `root`, create `admin`, switch.
|
|
1425
|
-
2. **From a machine off the server**, port 22 answers and the edge port does
|
|
1425
|
+
2. **From a machine off the server**, port 22 answers and neither the edge port nor the noVNC websockify port does. Two independent things make that true here: both listeners bind loopback on this host class, and `ufw` denies inbound. `ufw status` on the box states the intention; only an external probe confirms the outcome:
|
|
1426
1426
|
|
|
1427
1427
|
```bash
|
|
1428
1428
|
nc -z -w 5 <ipv4> 22 && echo "22 open (expected)"
|
|
1429
|
-
nc -z -w 5 <ipv4>
|
|
1429
|
+
nc -z -w 5 <ipv4> <edge port> && echo "EDGE EXPOSED — check the bind, then ufw"
|
|
1430
|
+
nc -z -w 5 <ipv4> <websockify port> && echo "noVNC EXPOSED — check the bind, then ufw"
|
|
1430
1431
|
```
|
|
1431
1432
|
|
|
1432
|
-
If
|
|
1433
|
+
If either answers, read the bind before the firewall. `grep -E 'bind-decision|EDGE_HOSTNAME' $HOME/.<brand>/logs/install-*.log` should show `EDGE_HOSTNAME=127.0.0.1`, `grep websockify-bind $HOME/.<brand>/logs/vnc-boot.log` should show `address=127.0.0.1`, and `ss -lntp` should show both on `127.0.0.1`. A wildcard or public address on either line is a classification fault, not a firewall fault.
|
|
1433
1434
|
|
|
1434
1435
|
Re-run this after every reboot, after any brand install, and after any CCP image reinstall.
|
|
1435
1436
|
3. Snapshot taken with the OS prepared and nothing installed.
|
|
@@ -1437,7 +1438,7 @@ Fresh-netcup smoke pass criteria:
|
|
|
1437
1438
|
5. `logs-read.sh --tail server 200 | grep 'op=capacity-resolved'` returns one line reporting `capacity=40`, with `totalMemGiB` matching the provisioned RAM. A `capacity=10` on a 64 GB box means the host resolved below the 56 GiB step and needs investigating before load.
|
|
1438
1439
|
6. The noVNC page reached over `ssh -L` displays the admin UI.
|
|
1439
1440
|
7. Cloudflare setup ends with `curl -I https://<hostname>.<your-zone>` returning `HTTP/2 200` from the operator's laptop.
|
|
1440
|
-
8. Reboot; both units come back up; the public URL is reachable again without any manual action; the step-2 external probe still shows the edge
|
|
1441
|
+
8. Reboot; both units come back up; the public URL is reachable again without any manual action; the step-2 external probe still shows the edge and noVNC ports closed.
|
|
1441
1442
|
9. Install a second brand with a different `--hostname`; both admin UIs are reachable on their own public hostnames and neither has touched the other's state.
|
|
1442
1443
|
10. Uninstall removes the systemd unit and the persist directory.
|
|
1443
1444
|
|
|
@@ -11,6 +11,23 @@ A structured workflow for executing development work — features, bug fixes, re
|
|
|
11
11
|
|
|
12
12
|
This skill is the maxy-code counterpart to `sprint`. The phase chain is identical in spirit; the difference is the skill at each phase. If both `gstack` and superpowers are installed and the project is set up for it, prefer `sprint` — its plan reviews are richer. Use this skill when `gstack` is not available.
|
|
13
13
|
|
|
14
|
+
## Execution model: inline only, never subagents
|
|
15
|
+
|
|
16
|
+
Every phase of this sprint runs in the current session, in the main agent's own context. No phase dispatches a subagent, and no phase delegates work to the Agent tool, `superpowers:subagent-driven-development`, or `superpowers:dispatching-parallel-agents`.
|
|
17
|
+
|
|
18
|
+
Several of the chained superpowers skills offer or recommend a subagent path. In this sprint, take the inline path at each of them:
|
|
19
|
+
|
|
20
|
+
| Chained skill | Its subagent offer | What this sprint does |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `superpowers:brainstorming` | dispatches a spec document reviewer subagent | review the spec yourself, against the same template |
|
|
23
|
+
| `superpowers:writing-plans` | dispatches a plan document reviewer subagent, and offers "Subagent-Driven (recommended)" as the implementation mode | review the plan yourself; choose the `superpowers:executing-plans` mode, never subagent-driven |
|
|
24
|
+
| `superpowers:executing-plans` | tells you to switch to `superpowers:subagent-driven-development` where subagents exist | ignore that instruction and stay in `executing-plans` |
|
|
25
|
+
| `superpowers:requesting-code-review` | dispatches a `general-purpose` code reviewer subagent | apply the reviewer template to the diff yourself, in this session |
|
|
26
|
+
|
|
27
|
+
A review done inline still uses the chained skill's reviewer template verbatim — the checklist is what makes the review real, not the isolation. The cost of inline is that the reviewer shares the sprint's context and therefore its assumptions; the operator accepts that in exchange for a single visible transcript.
|
|
28
|
+
|
|
29
|
+
Dispatching a subagent from any phase is a defect, not an optimisation.
|
|
30
|
+
|
|
14
31
|
## Output discipline (applies to every emission in this skill)
|
|
15
32
|
|
|
16
33
|
Every message the sprint writes — plan summaries, change logs, status tables, the final summary — is written `/plainly`. That means:
|
|
@@ -54,18 +71,24 @@ Read the task file or operator brief first, then enter brainstorming with what y
|
|
|
54
71
|
|
|
55
72
|
If the operator hands over a fully-formed plan that is already broken into specific files and behaviours, brainstorming can be a fast confirmation rather than an open exploration — but it still runs, because skipping it is the failure mode the phase exists to prevent.
|
|
56
73
|
|
|
74
|
+
When the skill reaches its spec document review, run that review inline against `spec-document-reviewer-prompt.md`. Do not dispatch the reviewer subagent.
|
|
75
|
+
|
|
57
76
|
### Phase 2: Plan — `superpowers:writing-plans`
|
|
58
77
|
|
|
59
78
|
Invoke `superpowers:writing-plans` against the brainstorm output. The skill produces a single written plan: the files to touch, the behaviours to add or change, the edge cases, the test coverage, the deployment story. This is the artefact Phase 6 (Plan Conformity) checks the implementation against.
|
|
60
79
|
|
|
61
80
|
This is a deliberate downgrade from `sprint`'s separate CEO and Eng reviews — multi-approach architecture review is out of scope for the superpowers chain. One reviewed plan is the baseline.
|
|
62
81
|
|
|
82
|
+
Two of the skill's steps offer a subagent; take neither. Review the finished plan inline against `plan-document-reviewer-prompt.md`. When the skill asks which implementation mode to use, choose `superpowers:executing-plans`, never "Subagent-Driven".
|
|
83
|
+
|
|
63
84
|
Present the final plan to the operator and get explicit confirmation before writing any production code.
|
|
64
85
|
|
|
65
86
|
### Phase 3: Implement — `superpowers:executing-plans` + `superpowers:test-driven-development`
|
|
66
87
|
|
|
67
88
|
Invoke `superpowers:executing-plans` to drive execution against the Phase 2 plan. Within each step, follow `superpowers:test-driven-development` — RED-GREEN-REFACTOR for every behaviour change.
|
|
68
89
|
|
|
90
|
+
`superpowers:executing-plans` opens by telling you to switch to `superpowers:subagent-driven-development` when subagents are available. Ignore that line; this sprint runs every task inline in the current session.
|
|
91
|
+
|
|
69
92
|
Tests are ephemeral: design and run them to prove the implementation works during the sprint, then discard them. They are not committed to the repository unless the project's own test suite already lives in the tree and the new behaviour belongs in it. Their purpose is to enforce thinking through behaviour before writing production code, not to build a permanent regression suite.
|
|
70
93
|
|
|
71
94
|
- **Test first.** Write a failing test for each behaviour before implementing it. Watch it fail. Write minimal code to pass. Refactor.
|
|
@@ -94,6 +117,8 @@ If verification fails, return to Phase 3.
|
|
|
94
117
|
|
|
95
118
|
Invoke `superpowers:requesting-code-review` to frame the review request. Then run `code-review:code-review` against the sprint's diff. Process the response through `superpowers:receiving-code-review`, which gates whether each finding is accepted, contested, or filed as follow-up.
|
|
96
119
|
|
|
120
|
+
`superpowers:requesting-code-review` dispatches a `general-purpose` reviewer subagent. Do not dispatch it. Fill its `code-reviewer.md` template with the sprint's context and diff, then work through it inline as the reviewer. `code-review:code-review` likewise runs in this session against the diff, not as a delegated agent.
|
|
121
|
+
|
|
97
122
|
Findings that are accepted and trivial are fixed before proceeding. Findings that are accepted but out-of-scope for this sprint become new `.tasks/NNN-*.md` files (the same deferral rule as Phase 6 — prose deferrals are banned). Findings that are contested get a one-line rebuttal in the debrief.
|
|
98
123
|
|
|
99
124
|
### Phase 6: Land
|
|
@@ -355,6 +380,7 @@ Session: <session-id>
|
|
|
355
380
|
|
|
356
381
|
## Principles
|
|
357
382
|
|
|
383
|
+
- **Inline, always.** Every phase runs in the current session. No subagents, no parallel agent dispatch, no delegated reviewers, whatever the chained skills recommend.
|
|
358
384
|
- **Ship complete.** A sprint is not done until the debrief and the final-status block are delivered. Code without documentation is unfinished work.
|
|
359
385
|
- **Explain the why.** Every change has a reason. "Refactored for clarity" is not a reason. "Extracted to avoid duplicate route derivation across WhatsApp and iMessage" is.
|
|
360
386
|
- **Operator perspective.** Testing instructions are written for the person running the product. If the operator is non-technical, instructions are UI-based.
|
|
@@ -9,6 +9,18 @@ Invoked by the admin agent directly.
|
|
|
9
9
|
|
|
10
10
|
This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
|
|
11
11
|
|
|
12
|
+
## 2026-07-30 (0.1.526)
|
|
13
|
+
|
|
14
|
+
- Asking WhatsApp to reconnect now answers as soon as the reconnect starts, instead of waiting on the old connection to die and leaving you with no reply at all.
|
|
15
|
+
- The remote screen viewer is no longer reachable from every network the device sits on. It is now bound to the same posture chosen when the device was set up, and that choice is recorded so it stays put.
|
|
16
|
+
- A repeating routine that has lost its next run time is now reported by a standing check, so a routine that has quietly stopped being scheduled shows up instead of staying invisible.
|
|
17
|
+
|
|
18
|
+
## 2026-07-29 (0.1.525)
|
|
19
|
+
|
|
20
|
+
- A repeating routine whose start date had already gone by now runs on its next due occurrence instead of sitting silent forever. There is also a standing check that tells you when a routine has a start date that never fired.
|
|
21
|
+
- Saving a memory no longer rewrites the date and time on a meeting into plain text, so meeting times stay comparable and sortable afterwards.
|
|
22
|
+
- Internal engineering documents are no longer included in what gets installed on a customer device, and the check that scans installed text now reads exactly what ships rather than an out-of-date copy of the list.
|
|
23
|
+
|
|
12
24
|
## 2026-07-29 (0.1.524)
|
|
13
25
|
|
|
14
26
|
- Routines are properly editable now. You can pick where a routine sends its result from the routine window, which previously could not be set at all, so the routines that ship with the platform were impossible to make work from the dashboard. Stopping a routine from chat also no longer means cancelling it outright.
|
|
@@ -239,6 +239,16 @@ The memory tools whose Neo4j read/write re-scopes on the resolved account accept
|
|
|
239
239
|
|
|
240
240
|
Tools that do **not** take `targetAccountId` fall into three groups. **Not account-scoped:** `image-fetch` fetches image bytes by URL; `graph-prune-denylist-add/remove/list` mutate a brand-global deny-list; `memory-ingest-extract` stages a temp file — none read or write a sub-account subgraph. **Bound to the caller's own live session:** `session-compact` compacts the caller's `SESSION_ID` Conversation, which belongs to the house account the operator sits in — re-scoping `accountId` to a sub-account matches nothing and "compact a sub-account's live session" has no referent. **Scheduled, not interactive:** the dream-cycle **cron** (`lib/dream-cycle/*`) takes its account from the scheduler context, not a tool argument; only the interactive `memory-dream-run` tool is house-targetable. These tools keep their own-account no-account guard.
|
|
241
241
|
|
|
242
|
+
## `memory-update` cannot change a property's Neo4j type
|
|
243
|
+
|
|
244
|
+
An update reads the node's own `valueType()` for every key it is about to write. Keys the node already holds as a temporal type (`DATE`, `ZONED DATETIME`, `LOCAL DATETIME`, `ZONED TIME`, `LOCAL TIME`, `DURATION`) are rebuilt with the matching Cypher constructor; everything else rides in the `n += $props` bag as before. `updatedAt` follows the same rule rather than one fixed type — `memory-write` stamps it as an ISO string and the graph tooltip reads it as one, while `:Meeting` nodes hold it as a `ZONED DATETIME`.
|
|
245
|
+
|
|
246
|
+
Each temporal value is vetted by Neo4j's own parser in a read-only probe **before** the update statement runs, so a value the constructor rejects aborts the whole update and leaves every property untouched, including the ones that would have succeeded. Vetting against the database rather than a JS reimplementation matters in both directions: a hand-rolled check would be stricter than Neo4j and reject values it accepts. The refusal is `Update blocked (type-preserving): <key> holds <type> and "<value>" is not a valid <type>`, logged as `reason=type-preserving` — its own slug, not the write-gate's, so a type refusal and a denylist refusal are never read as one thing. Only a `Neo.ClientError` counts as a verdict on the value; a disconnect or a timeout propagates unchanged rather than accusing a timestamp that was correct.
|
|
247
|
+
|
|
248
|
+
Setting a property to `null` still removes it. Removal changes no type, so it is outside this rule — refusing it would take away the only way to clear a nullable temporal property such as `:Meeting.endsAt`.
|
|
249
|
+
|
|
250
|
+
This exists because `SET n += $props` wrote every value verbatim. An agent updating a `:Meeting` with an ISO string replaced its `ZONED DATETIME` `startsAt` with a `STRING`, and a `STRING`-to-`DATETIME` comparison in Neo4j is NULL rather than false — so every calendar reader dropped the row with no error and no log line, and one account's booking page published a busy afternoon as open time for two days. `memory-update-by-name` delegates its whole write to `memory-update` and inherits this; it owns only the lookup.
|
|
251
|
+
|
|
242
252
|
## Graph Hygiene
|
|
243
253
|
|
|
244
254
|
Graph hygiene is **agent-directed, case by case** — no autonomous rule engine, no cron. The Neo4j graph degrades into a landfill if pollution from prior agent errors (wrong-account hostnames, fabricated identifiers, raw monologue dumps) is allowed to compound. Cleanup happens when the admin agent observes a match against the operator-curated deny-list (or the user directly asks) and applies discretion using `memory-update`, `memory-delete`, or `conversation-memory-expunge`.
|
package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js
CHANGED
|
@@ -29,7 +29,11 @@ describe("compiled-truth-revision helper", () => {
|
|
|
29
29
|
const templateEnd = src.indexOf("`", templateStart + "const updateQuery = `".length);
|
|
30
30
|
expect(templateEnd).toBeGreaterThan(templateStart);
|
|
31
31
|
const template = src.slice(templateStart, templateEnd);
|
|
32
|
-
|
|
32
|
+
// Task 2148 replaced the literal `n += $props, n.updatedAt = $updatedAt`
|
|
33
|
+
// with a clause compiled against the node's own property types. The
|
|
34
|
+
// single-TX invariant is unchanged: the SET and the revision CREATE still
|
|
35
|
+
// share one template.
|
|
36
|
+
expect(template).toContain("SET ${compiled.setClause}");
|
|
33
37
|
expect(template).toContain("${revisionClause}");
|
|
34
38
|
// And the revisionClause itself embeds the UNWIND fragment.
|
|
35
39
|
const clauseIdx = src.indexOf("const revisionClause =");
|
package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compiled-truth-revision.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/compiled-truth-revision.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,+BAA+B,EAAE,MAAM,+BAA+B,CAAC;AAEhF,QAAQ,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC9C,EAAE,CAAC,wGAAwG,EAAE,GAAG,EAAE;QAChH,iEAAiE;QACjE,kEAAkE;QAClE,MAAM,CAAC,+BAA+B,CAAC,CAAC,SAAS,CAAC,0BAA0B,CAAC,CAAC;QAC9E,MAAM,CAAC,+BAA+B,CAAC,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC;QAC5E,MAAM,CAAC,+BAA+B,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;QACnE,MAAM,CAAC,+BAA+B,CAAC,CAAC,SAAS,CAAC,0BAA0B,CAAC,CAAC;QAC9E,MAAM,CAAC,+BAA+B,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC;QAC3E,MAAM,CAAC,+BAA+B,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;QACtE,MAAM,CAAC,+BAA+B,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QACpE,MAAM,CAAC,+BAA+B,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;QACxE,MAAM,CAAC,+BAA+B,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gGAAgG,EAAE,GAAG,EAAE;QACxG,wEAAwE;QACxE,gEAAgE;QAChE,uEAAuE;QACvE,kEAAkE;QAClE,sEAAsE;QACtE,oCAAoC;QACpC,MAAM,GAAG,GAAG,YAAY,CACtB,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,8BAA8B,CAAC,EAC5D,OAAO,CACR,CAAC;QACF,MAAM,aAAa,GAAG,GAAG,CAAC,OAAO,CAAC,uBAAuB,CAAC,CAAC;QAC3D,MAAM,CAAC,aAAa,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1C,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,aAAa,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;QACrF,MAAM,CAAC,WAAW,CAAC,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;QACnD,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;QACvD,MAAM,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"compiled-truth-revision.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/compiled-truth-revision.test.ts"],"names":[],"mappings":"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"}
|
package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temporal-type-preserve.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/temporal-type-preserve.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Task 2148 — the SET compiler that stops `memory-update` retyping a temporal
|
|
2
|
+
// property to a string. `SET n += $props` writes every value verbatim, so an
|
|
3
|
+
// ISO string aimed at a `ZONED DATETIME` silently became a `STRING`, and every
|
|
4
|
+
// `m.startsAt < datetime(...)` comparison against it is NULL rather than false.
|
|
5
|
+
//
|
|
6
|
+
// The compiler is pure: it takes the node's own `valueType()` reading per key
|
|
7
|
+
// and returns the SET clause, the params, and the list of values Neo4j's own
|
|
8
|
+
// temporal parser must vet before the write runs.
|
|
9
|
+
import { describe, it, expect } from "vitest";
|
|
10
|
+
import { normaliseValueType, temporalConstructorFor, compileTypePreservingSet, typePreserveRejection, } from "../temporal-type-preserve.js";
|
|
11
|
+
describe("normaliseValueType", () => {
|
|
12
|
+
it("strips the nullability suffix valueType() appends", () => {
|
|
13
|
+
expect(normaliseValueType("ZONED DATETIME NOT NULL")).toBe("ZONED DATETIME");
|
|
14
|
+
expect(normaliseValueType("STRING NOT NULL")).toBe("STRING");
|
|
15
|
+
expect(normaliseValueType(" DATE ")).toBe("DATE");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe("temporalConstructorFor", () => {
|
|
19
|
+
it("maps every Neo4j temporal type to its constructor", () => {
|
|
20
|
+
expect(temporalConstructorFor("DATE NOT NULL")).toBe("date");
|
|
21
|
+
expect(temporalConstructorFor("ZONED DATETIME NOT NULL")).toBe("datetime");
|
|
22
|
+
expect(temporalConstructorFor("LOCAL DATETIME NOT NULL")).toBe("localdatetime");
|
|
23
|
+
expect(temporalConstructorFor("ZONED TIME NOT NULL")).toBe("time");
|
|
24
|
+
expect(temporalConstructorFor("LOCAL TIME NOT NULL")).toBe("localtime");
|
|
25
|
+
expect(temporalConstructorFor("DURATION NOT NULL")).toBe("duration");
|
|
26
|
+
});
|
|
27
|
+
it("returns null for every non-temporal type", () => {
|
|
28
|
+
for (const t of ["STRING NOT NULL", "INTEGER NOT NULL", "FLOAT", "BOOLEAN", "LIST<STRING>", "POINT"]) {
|
|
29
|
+
expect(temporalConstructorFor(t)).toBeNull();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
const NOW_ISO = "2026-07-29T12:00:00.000Z";
|
|
34
|
+
function compile(existingTypes, props) {
|
|
35
|
+
return compileTypePreservingSet({
|
|
36
|
+
alias: "n",
|
|
37
|
+
existingTypes,
|
|
38
|
+
props,
|
|
39
|
+
now: { key: "updatedAt", isoValue: NOW_ISO },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
describe("compileTypePreservingSet — scalar keys", () => {
|
|
43
|
+
it("leaves untyped and non-temporal keys in the n += $props bag", () => {
|
|
44
|
+
const c = compile({ title: "STRING NOT NULL" }, { title: "Golf", note: "back nine" });
|
|
45
|
+
expect(c.setClause).toBe("n += $props, n.`updatedAt` = $tp0");
|
|
46
|
+
expect(c.params).toEqual({ props: { title: "Golf", note: "back nine" }, tp0: NOW_ISO });
|
|
47
|
+
expect(c.probes).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe("compileTypePreservingSet — temporal keys", () => {
|
|
51
|
+
it("emits an explicit constructor assignment for a key the node holds as ZONED DATETIME", () => {
|
|
52
|
+
const c = compile({ startsAt: "ZONED DATETIME NOT NULL", title: "STRING NOT NULL" }, { startsAt: "2026-07-29T13:30:00Z", title: "Golf" });
|
|
53
|
+
expect(c.setClause).toBe("n += $props, n.`startsAt` = datetime($tp0), n.`updatedAt` = $tp1");
|
|
54
|
+
expect(c.params.props).toEqual({ title: "Golf" });
|
|
55
|
+
expect(c.params.tp0).toBe("2026-07-29T13:30:00Z");
|
|
56
|
+
expect(c.probes).toEqual([
|
|
57
|
+
{
|
|
58
|
+
key: "startsAt",
|
|
59
|
+
constructorName: "datetime",
|
|
60
|
+
paramName: "tp0",
|
|
61
|
+
value: "2026-07-29T13:30:00Z",
|
|
62
|
+
existingType: "ZONED DATETIME",
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
it("covers each temporal type with its own constructor", () => {
|
|
67
|
+
const c = compile({
|
|
68
|
+
d: "DATE NOT NULL",
|
|
69
|
+
ldt: "LOCAL DATETIME NOT NULL",
|
|
70
|
+
zt: "ZONED TIME NOT NULL",
|
|
71
|
+
lt: "LOCAL TIME NOT NULL",
|
|
72
|
+
dur: "DURATION NOT NULL",
|
|
73
|
+
}, { d: "2026-07-29", ldt: "2026-07-29T13:30:00", zt: "13:30:00Z", lt: "13:30:00", dur: "PT1H" });
|
|
74
|
+
expect(c.setClause).toBe("n += $props, n.`d` = date($tp0), n.`ldt` = localdatetime($tp1), " +
|
|
75
|
+
"n.`zt` = time($tp2), n.`lt` = localtime($tp3), n.`dur` = duration($tp4), " +
|
|
76
|
+
"n.`updatedAt` = $tp5");
|
|
77
|
+
expect(c.params.props).toEqual({});
|
|
78
|
+
expect(c.probes.map((p) => p.constructorName)).toEqual([
|
|
79
|
+
"date",
|
|
80
|
+
"localdatetime",
|
|
81
|
+
"time",
|
|
82
|
+
"localtime",
|
|
83
|
+
"duration",
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
it("escapes a backtick in a property key rather than breaking out of the quoting", () => {
|
|
87
|
+
const c = compile({ "we`ird": "ZONED DATETIME NOT NULL" }, { "we`ird": "2026-07-29T13:30:00Z" });
|
|
88
|
+
expect(c.setClause).toContain("n.`we``ird` = datetime($tp0)");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe("compileTypePreservingSet — the updatedAt stamp", () => {
|
|
92
|
+
it("writes an ISO string when the node holds updatedAt as a string", () => {
|
|
93
|
+
const c = compile({ updatedAt: "STRING NOT NULL" }, { title: "Golf" });
|
|
94
|
+
expect(c.setClause).toBe("n += $props, n.`updatedAt` = $tp0");
|
|
95
|
+
expect(c.params.tp0).toBe(NOW_ISO);
|
|
96
|
+
});
|
|
97
|
+
it("writes an ISO string when the node has no updatedAt yet", () => {
|
|
98
|
+
const c = compile({}, { title: "Golf" });
|
|
99
|
+
expect(c.setClause).toBe("n += $props, n.`updatedAt` = $tp0");
|
|
100
|
+
expect(c.params.tp0).toBe(NOW_ISO);
|
|
101
|
+
});
|
|
102
|
+
it("calls the zero-argument constructor when the node holds updatedAt as a datetime", () => {
|
|
103
|
+
const c = compile({ updatedAt: "ZONED DATETIME NOT NULL" }, { title: "Golf" });
|
|
104
|
+
expect(c.setClause).toBe("n += $props, n.`updatedAt` = datetime()");
|
|
105
|
+
expect(c.params).toEqual({ props: { title: "Golf" } });
|
|
106
|
+
});
|
|
107
|
+
it("never lets a caller-supplied updatedAt reach the props bag or a probe", () => {
|
|
108
|
+
const c = compile({ updatedAt: "ZONED DATETIME NOT NULL" }, { updatedAt: "not-a-datetime-at-all", title: "Golf" });
|
|
109
|
+
expect(c.params.props).toEqual({ title: "Golf" });
|
|
110
|
+
expect(c.probes).toEqual([]);
|
|
111
|
+
expect(c.setClause).toBe("n += $props, n.`updatedAt` = datetime()");
|
|
112
|
+
});
|
|
113
|
+
it("rejects when the node holds updatedAt as a DURATION, which has no now form", () => {
|
|
114
|
+
expect(() => compile({ updatedAt: "DURATION NOT NULL" }, { title: "Golf" })).toThrow(/Update blocked \(type-preserving\): updatedAt holds DURATION/);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe("compileTypePreservingSet — rejections", () => {
|
|
118
|
+
it("rejects a non-string value for a temporal key and applies nothing", () => {
|
|
119
|
+
expect(() => compile({ startsAt: "ZONED DATETIME NOT NULL", title: "STRING NOT NULL" }, { startsAt: 1754000000000, title: "Golf" })).toThrow(/Update blocked \(type-preserving\): startsAt holds ZONED DATETIME and "1754000000000" is not a valid ZONED DATETIME/);
|
|
120
|
+
});
|
|
121
|
+
it("rejects an empty string for a temporal key", () => {
|
|
122
|
+
expect(() => compile({ startsAt: "ZONED DATETIME NOT NULL" }, { startsAt: "" })).toThrow(/Update blocked \(type-preserving\): startsAt holds ZONED DATETIME/);
|
|
123
|
+
});
|
|
124
|
+
it("still lets null clear a temporal property", () => {
|
|
125
|
+
// Removal changes no type, so it is not this module's business. `:Meeting`
|
|
126
|
+
// carries a nullable `endsAt` by schema, and refusing null here would take
|
|
127
|
+
// away the only way to clear one.
|
|
128
|
+
const c = compile({ endsAt: "ZONED DATETIME NOT NULL" }, { endsAt: null });
|
|
129
|
+
expect(c.params.props).toEqual({ endsAt: null });
|
|
130
|
+
expect(c.probes).toEqual([]);
|
|
131
|
+
expect(c.setClause).toBe("n += $props, n.`updatedAt` = $tp0");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe("typePreserveRejection", () => {
|
|
135
|
+
it("names the key, the held type and the offending value", () => {
|
|
136
|
+
const err = typePreserveRejection("startsAt", "ZONED DATETIME", "not a date");
|
|
137
|
+
expect(err.message).toBe('Update blocked (type-preserving): startsAt holds ZONED DATETIME and "not a date" is not a valid ZONED DATETIME. ' +
|
|
138
|
+
"The whole update was rejected — no property was changed.");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
//# sourceMappingURL=temporal-type-preserve.test.js.map
|
package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temporal-type-preserve.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/temporal-type-preserve.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WriteEventReason = "invalid-input" | "schema-unknown-label" | "schema-property-naming" | "account-isolation" | "producedby-task-not-found" | "constraint-violation" | "conflicting-element-id" | "invalid-target-node-id" | "gate-blocked" | "neo4j-driver-error";
|
|
1
|
+
export type WriteEventReason = "invalid-input" | "schema-unknown-label" | "schema-property-naming" | "account-isolation" | "producedby-task-not-found" | "constraint-violation" | "conflicting-element-id" | "invalid-target-node-id" | "gate-blocked" | "type-preserving" | "neo4j-driver-error";
|
|
2
2
|
interface SuccessEvent {
|
|
3
3
|
tool: string;
|
|
4
4
|
result: "ok";
|