@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
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
// rule covers test titles, and test bodies that assert on citations are
|
|
25
25
|
// equivalent to titles for this gate's purpose.
|
|
26
26
|
|
|
27
|
-
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
27
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
|
|
28
28
|
import { dirname, join, relative, sep, basename } from 'node:path'
|
|
29
29
|
import { fileURLToPath } from 'node:url'
|
|
30
|
+
import { findCitations } from './lib/task-id-citation.mjs'
|
|
30
31
|
|
|
31
32
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
|
|
32
33
|
// REPO_ROOT is overridable so the `.test.sh` self-test can point the gate at a
|
|
@@ -40,7 +41,6 @@ const SCAN_ROOTS = [
|
|
|
40
41
|
join(REPO_ROOT, 'platform', 'plugins'),
|
|
41
42
|
join(REPO_ROOT, 'platform', 'ui', 'server'),
|
|
42
43
|
join(REPO_ROOT, 'platform', 'ui', 'app'),
|
|
43
|
-
join(REPO_ROOT, 'packages', 'create-maxy', 'src'),
|
|
44
44
|
]
|
|
45
45
|
|
|
46
46
|
const ALLOWED_EXTS = new Set(['.sh', '.ts', '.tsx', '.js', '.mjs', '.cjs'])
|
|
@@ -52,27 +52,70 @@ const TASK_ID_RE = /\(Task \d+\)/
|
|
|
52
52
|
// Markdown mode. Unlike code files — where a citation is legal in a comment and
|
|
53
53
|
// only the parenthetical operator-visible form is rejected — a shipped `.md`
|
|
54
54
|
// loads into a client-facing agent's context wholesale, so ANY citation form
|
|
55
|
-
// leaks.
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// file-path reference (file paths are out of this gate's scope). The 2-digit
|
|
65
|
-
// floor keeps single-digit domain counts ("task 1 of 3") out.
|
|
66
|
-
const MD_TASK_ID_RE = /\bTasks?[\s-]+\d{2,4}|\btasks?\s+\d{2,4}/
|
|
55
|
+
// leaks. The regex and the paragraph-aware scan live in
|
|
56
|
+
// `lib/task-id-citation.mjs`, shared with the bundle's payload audit so the two
|
|
57
|
+
// surfaces cannot disagree about what a citation is.
|
|
58
|
+
//
|
|
59
|
+
// The roots are what the client payload CARRIES, not a list somebody maintains.
|
|
60
|
+
// `bundle.js` copies all of `platform/` and all of `premium-plugins/` into the
|
|
61
|
+
// payload, so those two trees are the roots. Naming `platform/plugins` and
|
|
62
|
+
// `platform/templates` instead is what let `platform/docs` ship 296 citations
|
|
63
|
+
// to every install without this gate ever looking at it.
|
|
67
64
|
const MD_SCAN_ROOTS = [
|
|
68
|
-
join(REPO_ROOT, 'platform'
|
|
69
|
-
join(REPO_ROOT, 'platform', 'templates'),
|
|
70
|
-
// Premium bundles are copied verbatim into the client payload
|
|
71
|
-
// (`payload/premium-plugins/<bundle>/`), so their shipped markdown loads into
|
|
72
|
-
// the client agent the same as core plugins — any citation form leaks.
|
|
65
|
+
join(REPO_ROOT, 'platform'),
|
|
73
66
|
join(REPO_ROOT, 'premium-plugins'),
|
|
74
67
|
]
|
|
75
68
|
|
|
69
|
+
// The trees under the roots above that `bundle.js` keeps OUT of the payload,
|
|
70
|
+
// mirroring its filter. A path here is unscanned because it does not ship; a
|
|
71
|
+
// path not here is scanned because it does. Each entry names the exclusion in
|
|
72
|
+
// `bundle.js` that makes it true, so the two files can be checked against each
|
|
73
|
+
// other by reading them. `.docs/` and `.tasks/` need no entry: they sit outside
|
|
74
|
+
// both roots and stay citation-rich.
|
|
75
|
+
//
|
|
76
|
+
// This list is a hand-kept mirror, and that is the one place the two surfaces
|
|
77
|
+
// can still drift. An exclusion added to `bundle.js` and forgotten here makes
|
|
78
|
+
// the gate scan a tree that does not ship, which is noisy but fail-safe. An
|
|
79
|
+
// exclusion REMOVED from `bundle.js` and forgotten here is the dangerous
|
|
80
|
+
// direction: a tree starts shipping and stops being scanned. The payload audit
|
|
81
|
+
// in `bundle.js` is what catches that one, because it consults neither list.
|
|
82
|
+
const MD_NOT_SHIPPED = [
|
|
83
|
+
// bundle.js `src.startsWith(UI_DIR)` — ui ships as a compiled bundle, not source.
|
|
84
|
+
join(REPO_ROOT, 'platform', 'ui'),
|
|
85
|
+
// bundle.js `src.startsWith(DOCS_DIR)` — sprint plans and specs are
|
|
86
|
+
// development records and do not ship.
|
|
87
|
+
join(REPO_ROOT, 'platform', 'docs'),
|
|
88
|
+
// bundle.js `src.startsWith(DOT_DOCS_DIR)` — same, for the dotted twin. The
|
|
89
|
+
// widened scan is what found this one: a `platform/*/` census does not match
|
|
90
|
+
// a dotdir, so `platform/.docs` shipped unseen by both the old roots and the
|
|
91
|
+
// measurement that motivated the change.
|
|
92
|
+
join(REPO_ROOT, 'platform', '.docs'),
|
|
93
|
+
// bundle.js `src.includes("/config/accounts/")`, `"/data/accounts/"` and
|
|
94
|
+
// `"/data/uploads/"` — live account data never ships. An account directory
|
|
95
|
+
// holds real markdown (`SCHEMA.md`, `AGENTS.md`), so without these three the
|
|
96
|
+
// gate would read a client's own files and could refuse a commit over a
|
|
97
|
+
// citation in something no payload carries.
|
|
98
|
+
join(REPO_ROOT, 'platform', 'config', 'accounts'),
|
|
99
|
+
join(REPO_ROOT, 'platform', 'data', 'accounts'),
|
|
100
|
+
join(REPO_ROOT, 'platform', 'data', 'uploads'),
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
function isNotShipped(file) {
|
|
104
|
+
return MD_NOT_SHIPPED.some((dir) => file === dir || file.startsWith(dir + sep))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Every root names itself on every run. Before this, a tree outside the roots
|
|
108
|
+
// produced no output at all, so nothing distinguished "scanned and clean" from
|
|
109
|
+
// "never looked at", and a root that stopped existing was swallowed by walk()'s
|
|
110
|
+
// bare catch — which is exactly how the archived `packages/create-maxy`
|
|
111
|
+
// installer tree sat in this list for months pointing at a directory that does
|
|
112
|
+
// not exist, proving nothing. A missing root logs rather than fails, because
|
|
113
|
+
// the `.test.sh` fixture tree deliberately creates only some of them.
|
|
114
|
+
function logRoot(kind, root, files) {
|
|
115
|
+
const exists = existsSync(root) ? 'yes' : 'no'
|
|
116
|
+
console.log(`[task-id-leak] root=${relative(REPO_ROOT, root)} kind=${kind} exists=${exists} files=${files}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
76
119
|
function isCommentLine(line) {
|
|
77
120
|
const trimmed = line.replace(/^\s+/, '')
|
|
78
121
|
if (trimmed.startsWith('#')) return true
|
|
@@ -115,6 +158,7 @@ function* walk(root) {
|
|
|
115
158
|
const leaks = []
|
|
116
159
|
|
|
117
160
|
for (const root of SCAN_ROOTS) {
|
|
161
|
+
let scanned = 0
|
|
118
162
|
for (const file of walk(root)) {
|
|
119
163
|
if (!shouldScanFile(file)) continue
|
|
120
164
|
let content
|
|
@@ -123,6 +167,7 @@ for (const root of SCAN_ROOTS) {
|
|
|
123
167
|
} catch {
|
|
124
168
|
continue
|
|
125
169
|
}
|
|
170
|
+
scanned++
|
|
126
171
|
const lines = content.split('\n')
|
|
127
172
|
for (let i = 0; i < lines.length; i++) {
|
|
128
173
|
const line = lines[i]
|
|
@@ -131,59 +176,30 @@ for (const root of SCAN_ROOTS) {
|
|
|
131
176
|
leaks.push({ file: relative(REPO_ROOT, file), line: i + 1, content: line.trim() })
|
|
132
177
|
}
|
|
133
178
|
}
|
|
179
|
+
logRoot('code', root, scanned)
|
|
134
180
|
}
|
|
135
181
|
|
|
136
|
-
// Markdown scan —
|
|
137
|
-
// skips node_modules/dist/build/.next/payload/__tests__)
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
// Scanned per paragraph, not per line: a markdown soft-wrap can split a single
|
|
142
|
-
// citation across two source lines (`... (Task\n659) ...`), which renders as
|
|
143
|
-
// `(Task 659)` but matches neither line in isolation. Consecutive non-blank
|
|
144
|
-
// lines are joined with a space (the soft-break a renderer would insert) and
|
|
145
|
-
// scanned as one string; a blank line ends the paragraph, so the join never
|
|
146
|
-
// crosses a paragraph break. Every match in the paragraph is reported, mapped
|
|
147
|
-
// back to the source line where the match starts, preserving exact-line and
|
|
148
|
-
// multi-citation reporting.
|
|
149
|
-
const MD_TASK_ID_RE_G = new RegExp(MD_TASK_ID_RE.source, 'g')
|
|
150
|
-
|
|
151
|
-
function flushParagraph(file, joined, lineStarts) {
|
|
152
|
-
if (joined === '') return
|
|
153
|
-
for (const m of joined.matchAll(MD_TASK_ID_RE_G)) {
|
|
154
|
-
let lineNo = lineStarts[0].lineNo
|
|
155
|
-
for (const ls of lineStarts) {
|
|
156
|
-
if (ls.offset <= m.index) lineNo = ls.lineNo
|
|
157
|
-
}
|
|
158
|
-
leaks.push({ file: relative(REPO_ROOT, file), line: lineNo, content: m[0] })
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
182
|
+
// Markdown scan — every `.md` the payload carries. Reuses `walk` (which already
|
|
183
|
+
// skips node_modules/dist/build/.next/payload/__tests__), drops the trees
|
|
184
|
+
// `bundle.js` excludes from the payload, and applies no comment skip: the whole
|
|
185
|
+
// doc is agent content.
|
|
162
186
|
for (const root of MD_SCAN_ROOTS) {
|
|
187
|
+
let scanned = 0
|
|
163
188
|
for (const file of walk(root)) {
|
|
164
189
|
if (!file.endsWith('.md')) continue
|
|
190
|
+
if (isNotShipped(file)) continue
|
|
165
191
|
let content
|
|
166
192
|
try {
|
|
167
193
|
content = readFileSync(file, 'utf-8')
|
|
168
194
|
} catch {
|
|
169
195
|
continue
|
|
170
196
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
for (let i = 0; i < lines.length; i++) {
|
|
175
|
-
if (lines[i].trim() === '') {
|
|
176
|
-
flushParagraph(file, joined, lineStarts)
|
|
177
|
-
joined = ''
|
|
178
|
-
lineStarts = []
|
|
179
|
-
continue
|
|
180
|
-
}
|
|
181
|
-
if (joined !== '') joined += ' '
|
|
182
|
-
lineStarts.push({ offset: joined.length, lineNo: i + 1 })
|
|
183
|
-
joined += lines[i]
|
|
197
|
+
scanned++
|
|
198
|
+
for (const c of findCitations(content)) {
|
|
199
|
+
leaks.push({ file: relative(REPO_ROOT, file), line: c.line, content: c.match })
|
|
184
200
|
}
|
|
185
|
-
flushParagraph(file, joined, lineStarts)
|
|
186
201
|
}
|
|
202
|
+
logRoot('md', root, scanned)
|
|
187
203
|
}
|
|
188
204
|
|
|
189
205
|
if (leaks.length > 0) {
|
|
@@ -194,8 +210,10 @@ if (leaks.length > 0) {
|
|
|
194
210
|
console.error('')
|
|
195
211
|
console.error('Code files: citations belong in inline comments only, never in operator-visible')
|
|
196
212
|
console.error('strings (stderr/stdout, logs, systemd --description=, error messages, console.*).')
|
|
197
|
-
console.error('Shipped markdown (
|
|
198
|
-
console.error('citation in any form — the whole doc loads into a client-facing
|
|
199
|
-
console.error('
|
|
213
|
+
console.error('Shipped markdown (everything under platform/ and premium-plugins/ that the payload')
|
|
214
|
+
console.error('carries): no task citation in any form — the whole doc loads into a client-facing')
|
|
215
|
+
console.error('agent. The trees that do NOT ship are exempt, and MD_NOT_SHIPPED in this file is')
|
|
216
|
+
console.error('the list of them; the dev docs (.docs/, .tasks/) sit outside the roots entirely')
|
|
217
|
+
console.error('and keep their citations.')
|
|
200
218
|
process.exit(1)
|
|
201
219
|
}
|
|
@@ -36,44 +36,61 @@ function extractFrontmatter(md) {
|
|
|
36
36
|
return md.slice(3, end)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// Collect
|
|
40
|
-
//
|
|
39
|
+
// Collect the declared entries under a bare `tools:` block and a bare `hidden:`
|
|
40
|
+
// block. Both contribute names a specialist may list in its `tools:`
|
|
41
41
|
// frontmatter, so both belong to the live set. Task 1998 gave the hidden block
|
|
42
|
-
// the same name+riskClass shape as tools, so the two branches
|
|
43
|
-
//
|
|
42
|
+
// the same name+riskClass shape as tools, so the two branches read alike; this
|
|
43
|
+
// parser stays separate from tool-surface.ts on purpose, so a drift in one
|
|
44
44
|
// shows up as a diff rather than as agreement between a parser and itself.
|
|
45
|
-
|
|
45
|
+
//
|
|
46
|
+
// Field values are returned as raw trimmed strings, or absent when the manifest
|
|
47
|
+
// has no such line. Judging them is the caller's job: the generator wants only
|
|
48
|
+
// names, and the gate wants to decide whether a value is one the session
|
|
49
|
+
// manager would accept.
|
|
50
|
+
export function parseToolEntries(frontmatter) {
|
|
46
51
|
const lines = frontmatter.split('\n')
|
|
47
|
-
const
|
|
52
|
+
const entries = []
|
|
48
53
|
let mode = null // 'tools' | 'hidden' | null
|
|
54
|
+
let current = null
|
|
55
|
+
const flush = () => { if (current) { entries.push(current); current = null } }
|
|
49
56
|
for (const line of lines) {
|
|
50
57
|
if (/^tools\s*:/.test(line)) {
|
|
58
|
+
flush()
|
|
51
59
|
mode = /^tools\s*:\s*$/.test(line) ? 'tools' : null
|
|
52
60
|
continue
|
|
53
61
|
}
|
|
54
62
|
if (/^hidden\s*:/.test(line)) {
|
|
63
|
+
flush()
|
|
55
64
|
mode = /^hidden\s*:\s*$/.test(line) ? 'hidden' : null
|
|
56
65
|
continue
|
|
57
66
|
}
|
|
58
|
-
if (/^\S/.test(line) && line.trim() !== '') { mode = null; continue }
|
|
59
|
-
if (mode ===
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
if (/^\S/.test(line) && line.trim() !== '') { flush(); mode = null; continue }
|
|
68
|
+
if (mode === null) continue
|
|
69
|
+
const name = line.match(/^\s+-\s+name:\s*(.+?)\s*$/)
|
|
70
|
+
if (name) { flush(); current = { block: mode, name: name[1] }; continue }
|
|
71
|
+
// A list item that does not lead with `name:` is malformed — a tools entry
|
|
72
|
+
// with no name, or a hidden entry written as a bare string. loadToolSurface
|
|
73
|
+
// throws on both, so the entry is recorded without a name and reported by
|
|
74
|
+
// validateToolEntries rather than silently dropped.
|
|
75
|
+
if (/^\s+-\s+/.test(line)) { flush(); current = { block: mode }; continue }
|
|
76
|
+
if (current === null) continue
|
|
77
|
+
const pub = line.match(/^\s+publicAllowlist:\s*(.+?)\s*$/)
|
|
78
|
+
if (pub) { current.publicAllowlist = pub[1].trim(); continue }
|
|
79
|
+
const adm = line.match(/^\s+adminAllowlist:\s*(.+?)\s*$/)
|
|
80
|
+
if (adm) { current.adminAllowlist = adm[1].trim(); continue }
|
|
81
|
+
const risk = line.match(/^\s+riskClass:\s*(.+?)\s*$/)
|
|
82
|
+
if (risk) { current.riskClass = risk[1].trim(); continue }
|
|
66
83
|
}
|
|
67
|
-
|
|
84
|
+
flush()
|
|
85
|
+
return entries
|
|
68
86
|
}
|
|
69
87
|
|
|
70
|
-
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (!existsSync(pluginsRoot)) return { plugins, names: [] }
|
|
88
|
+
/** Walk every plugin directory that has a PLUGIN.md with a parseable
|
|
89
|
+
* frontmatter and at least one declared entry. Shared by the name derivation
|
|
90
|
+
* and the field validation so the two can never disagree about which
|
|
91
|
+
* manifests are in scope. */
|
|
92
|
+
function* walkPluginManifests(pluginsRoot) {
|
|
93
|
+
if (!existsSync(pluginsRoot)) return
|
|
77
94
|
for (const entry of readdirSync(pluginsRoot).sort()) {
|
|
78
95
|
if (entry.startsWith('.')) continue
|
|
79
96
|
const dir = join(pluginsRoot, entry)
|
|
@@ -82,10 +99,97 @@ export function computeCanonicalToolNames(pluginsRoot = PLATFORM_PLUGINS_ROOT) {
|
|
|
82
99
|
if (!existsSync(manifest)) continue
|
|
83
100
|
const fm = extractFrontmatter(readFileSync(manifest, 'utf-8'))
|
|
84
101
|
if (fm === null) continue
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
const entries = parseToolEntries(fm)
|
|
103
|
+
if (entries.length === 0) continue
|
|
104
|
+
yield { plugin: entry, entries }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Returns { plugins, names } — `plugins` is the sorted list of maxy plugin
|
|
109
|
+
* directory basenames that declare a tool surface; `names` is the sorted list
|
|
110
|
+
* of canonical long-prefix tool names across all of them. */
|
|
111
|
+
export function computeCanonicalToolNames(pluginsRoot = PLATFORM_PLUGINS_ROOT) {
|
|
112
|
+
const plugins = []
|
|
113
|
+
const names = new Set()
|
|
114
|
+
for (const { plugin, entries } of walkPluginManifests(pluginsRoot)) {
|
|
115
|
+
plugins.push(plugin)
|
|
116
|
+
// A malformed entry has no name to derive a canonical name from. The gate
|
|
117
|
+
// reports it; this list stays exactly what it was before that shape was
|
|
118
|
+
// recorded, so the generated artifact does not move.
|
|
119
|
+
for (const e of entries) {
|
|
120
|
+
if (e.name !== undefined) names.add(canonicalToolName(plugin, e.name))
|
|
121
|
+
}
|
|
89
122
|
}
|
|
90
123
|
return { plugins: plugins.sort(), names: Array.from(names).sort() }
|
|
91
124
|
}
|
|
125
|
+
|
|
126
|
+
/** Every declared entry across every plugin, stamped with its plugin. The
|
|
127
|
+
* build gate validates these against the session manager's RISK_CLASSES; see
|
|
128
|
+
* validateToolEntries. */
|
|
129
|
+
export function computeToolEntries(pluginsRoot = PLATFORM_PLUGINS_ROOT) {
|
|
130
|
+
const out = []
|
|
131
|
+
for (const { plugin, entries } of walkPluginManifests(pluginsRoot)) {
|
|
132
|
+
for (const e of entries) out.push({ plugin, ...e })
|
|
133
|
+
}
|
|
134
|
+
return out
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The gate validates risk classes against the session manager's own list rather
|
|
138
|
+
// than a copy, so the two cannot drift. The gate is plain .mjs run by bare node
|
|
139
|
+
// and the declaration is TypeScript, so it is read as text.
|
|
140
|
+
const RISK_CLASSES_RE = /export const RISK_CLASSES:[^=]*=\s*\[([^\]]*)\]/
|
|
141
|
+
|
|
142
|
+
/** The valid risk classes, read from tool-surface.ts. Throws rather than
|
|
143
|
+
* returning a fallback: a gate that validated against a hardcoded copy could
|
|
144
|
+
* drift from the parser it protects, and one that validated against an empty
|
|
145
|
+
* set would accept every value. */
|
|
146
|
+
export function readRiskClasses(tsPath) {
|
|
147
|
+
let src
|
|
148
|
+
try {
|
|
149
|
+
src = readFileSync(tsPath, 'utf-8')
|
|
150
|
+
} catch {
|
|
151
|
+
throw new Error(`cannot read ${tsPath}, which declares RISK_CLASSES — refusing to validate risk classes without it`)
|
|
152
|
+
}
|
|
153
|
+
const m = src.match(RISK_CLASSES_RE)
|
|
154
|
+
if (m === null) {
|
|
155
|
+
throw new Error(`no 'export const RISK_CLASSES ... = [...]' declaration in ${tsPath} — refusing to validate risk classes against a set that cannot be read`)
|
|
156
|
+
}
|
|
157
|
+
const classes = m[1]
|
|
158
|
+
.split(',')
|
|
159
|
+
.map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
160
|
+
.filter((s) => s.length > 0)
|
|
161
|
+
if (classes.length === 0) {
|
|
162
|
+
throw new Error(`RISK_CLASSES in ${tsPath} parsed to an empty set — refusing to validate against it`)
|
|
163
|
+
}
|
|
164
|
+
return classes
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Judge parsed entries against the rules loadToolSurface enforces. A tools
|
|
168
|
+
* entry must declare both allowlist flags as YAML booleans and a riskClass in
|
|
169
|
+
* the set; a hidden entry must declare a riskClass and carries no allowlist
|
|
170
|
+
* flags at all. Missing and invalid are both violations, because
|
|
171
|
+
* loadToolSurface throws on both. Returns every violation rather than the
|
|
172
|
+
* first: an author who mistyped one field has often mistyped its sibling. */
|
|
173
|
+
export function validateToolEntries(entries, riskClasses) {
|
|
174
|
+
const valid = new Set(riskClasses)
|
|
175
|
+
const list = riskClasses.join('|')
|
|
176
|
+
const violations = []
|
|
177
|
+
for (const e of entries) {
|
|
178
|
+
if (typeof e.name !== 'string' || e.name.length === 0) {
|
|
179
|
+
violations.push({ plugin: e.plugin, name: '<unnamed>', message: 'tool entry has no name' })
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
const add = (message) => violations.push({ plugin: e.plugin, name: e.name, message })
|
|
183
|
+
const subject = e.block === 'hidden' ? 'every hidden tool' : 'every tool'
|
|
184
|
+
if (e.block === 'tools') {
|
|
185
|
+
for (const field of ['publicAllowlist', 'adminAllowlist']) {
|
|
186
|
+
const v = e[field]
|
|
187
|
+
if (v === undefined) add(`${subject} must declare ${field}: <true|false>`)
|
|
188
|
+
else if (v !== 'true' && v !== 'false') add(`${field} must be true or false, got ${JSON.stringify(v)}`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (e.riskClass === undefined) add(`${subject} must declare riskClass: <${list}>`)
|
|
192
|
+
else if (!valid.has(e.riskClass)) add(`riskClass must be one of ${list}, got ${JSON.stringify(e.riskClass)}`)
|
|
193
|
+
}
|
|
194
|
+
return violations
|
|
195
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Single source of truth for what counts as an internal task citation in
|
|
2
|
+
// shipped markdown, and for how a markdown file is scanned for one.
|
|
3
|
+
//
|
|
4
|
+
// Two consumers, deliberately. `platform/scripts/check-no-task-id-leaks.mjs`
|
|
5
|
+
// scans the SOURCE trees at commit time; `packages/create-maxy-code/scripts/
|
|
6
|
+
// bundle.js` scans the ASSEMBLED payload at bundle time. They answer different
|
|
7
|
+
// questions, "could this ship?" and "did this ship?", and a divergence between
|
|
8
|
+
// two copies of the regex would let one pass what the other rejects, with the
|
|
9
|
+
// payload audit failing a publish for something the commit gate allowed. The
|
|
10
|
+
// regex and the scan live here so neither consumer carries its own copy.
|
|
11
|
+
//
|
|
12
|
+
// A shipped `.md` loads into a client-facing agent's context wholesale, so ANY
|
|
13
|
+
// citation form leaks. Catches `Task NNN`, `(Task NNN)`, `Tasks NNN`,
|
|
14
|
+
// `since Task NNN`. It deliberately does NOT match bare `#NNN`: every `#NNN` in
|
|
15
|
+
// the corpus is a hex colour and there is no signal to separate `#732`-issue
|
|
16
|
+
// from `#732`-hex.
|
|
17
|
+
// Two alternatives:
|
|
18
|
+
// `\bTasks?[\s-]+\d{2,4}` — capital-T, any separator: `Task 732`, `(Task 732)`,
|
|
19
|
+
// `Tasks 12`, the hyphenated `Pre-Task-748` / `post-Task-904` forms.
|
|
20
|
+
// `\btasks?\s+\d{2,4}` — lowercase but SPACE-separated only: catches prose
|
|
21
|
+
// citations like "before task 127", while a lowercase HYPHENATED `task-537`
|
|
22
|
+
// stays exempt because that shape is a branch slug or a
|
|
23
|
+
// `.tasks/<n>-…-task-NNN.md` file-path reference (file paths are out of
|
|
24
|
+
// scope). The 2-digit floor keeps single-digit domain counts ("task 1 of
|
|
25
|
+
// 3") out.
|
|
26
|
+
export const MD_TASK_ID_RE = /\bTasks?[\s-]+\d{2,4}|\btasks?\s+\d{2,4}/
|
|
27
|
+
|
|
28
|
+
// Scanned per paragraph, not per line: a markdown soft-wrap can split a single
|
|
29
|
+
// citation across two source lines (`... (Task\n659) ...`), which renders as
|
|
30
|
+
// `(Task 659)` but matches neither line in isolation. Consecutive non-blank
|
|
31
|
+
// lines are joined with a space (the soft-break a renderer would insert) and
|
|
32
|
+
// scanned as one string; a blank line ends the paragraph, so the join never
|
|
33
|
+
// crosses a paragraph break. Every match in the paragraph is reported, mapped
|
|
34
|
+
// back to the source line where the match starts, preserving exact-line and
|
|
35
|
+
// multi-citation reporting.
|
|
36
|
+
export function findCitations(content) {
|
|
37
|
+
const re = new RegExp(MD_TASK_ID_RE.source, 'g')
|
|
38
|
+
const found = []
|
|
39
|
+
const lines = content.split('\n')
|
|
40
|
+
let joined = ''
|
|
41
|
+
let lineStarts = []
|
|
42
|
+
|
|
43
|
+
function flush() {
|
|
44
|
+
if (joined === '') return
|
|
45
|
+
for (const m of joined.matchAll(re)) {
|
|
46
|
+
let lineNo = lineStarts[0].lineNo
|
|
47
|
+
for (const ls of lineStarts) {
|
|
48
|
+
if (ls.offset <= m.index) lineNo = ls.lineNo
|
|
49
|
+
}
|
|
50
|
+
found.push({ line: lineNo, match: m[0] })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < lines.length; i++) {
|
|
55
|
+
if (lines[i].trim() === '') {
|
|
56
|
+
flush()
|
|
57
|
+
joined = ''
|
|
58
|
+
lineStarts = []
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
if (joined !== '') joined += ' '
|
|
62
|
+
lineStarts.push({ offset: joined.length, lineNo: i + 1 })
|
|
63
|
+
joined += lines[i]
|
|
64
|
+
}
|
|
65
|
+
flush()
|
|
66
|
+
return found
|
|
67
|
+
}
|
|
@@ -126,6 +126,45 @@ case ":$(echo "$CHROMIUM_REALPATH" | tr '/' ':'):" in
|
|
|
126
126
|
;;
|
|
127
127
|
esac
|
|
128
128
|
CHROMIUM_CONFINEMENT="non-snap"
|
|
129
|
+
|
|
130
|
+
# resolve the websockify bind address from the install-time config file
|
|
131
|
+
# (packages/create-maxy-code/src/index.ts writeWebsockifyBindFile). The
|
|
132
|
+
# installer decides it once, from the same classifier that decided the edge
|
|
133
|
+
# bind, so this script never re-derives host state and can never disagree with
|
|
134
|
+
# the edge about whether the box is publicly reachable.
|
|
135
|
+
#
|
|
136
|
+
# Called from the `start` branch rather than at the top level, unlike the
|
|
137
|
+
# Chromium resolution above: `stop`, `status` and the per-cycle `cdp-liveness`
|
|
138
|
+
# probe bind nothing, so a partial install should not take them down too.
|
|
139
|
+
#
|
|
140
|
+
# Absent or unparseable is fatal, matching brand.json and chromium-binary.path.
|
|
141
|
+
# This script and the file ship in the same payload, so absence means a
|
|
142
|
+
# tampered or partial install — and both possible defaults are wrong for one
|
|
143
|
+
# host class: a wildcard re-opens a cloud box to the internet, a loopback kills
|
|
144
|
+
# the LAN noVNC page on a Pi with no signal beyond a page that will not load.
|
|
145
|
+
resolve_websockify_bind() {
|
|
146
|
+
local websockify_bind_file websockify_bind_line
|
|
147
|
+
websockify_bind_file="${PLATFORM_ROOT}/config/websockify-bind"
|
|
148
|
+
if [ ! -r "$websockify_bind_file" ]; then
|
|
149
|
+
log "[vnc.sh:websockify] FATAL: ${websockify_bind_file} missing or unreadable"
|
|
150
|
+
echo "ERROR: ${websockify_bind_file} missing or unreadable." >&2
|
|
151
|
+
echo " Re-run the installer to provision the websockify bind posture." >&2
|
|
152
|
+
exit 1
|
|
153
|
+
fi
|
|
154
|
+
websockify_bind_line="$(head -n1 "$websockify_bind_file")"
|
|
155
|
+
WEBSOCKIFY_BIND="$(printf '%s\n' "$websockify_bind_line" | sed -n 's/.*bind=\([^ ]*\).*/\1/p')"
|
|
156
|
+
WEBSOCKIFY_BIND_KIND="$(printf '%s\n' "$websockify_bind_line" | sed -n 's/.*kind=\([^ ]*\).*/\1/p')"
|
|
157
|
+
case "$WEBSOCKIFY_BIND" in
|
|
158
|
+
"[::]"|"127.0.0.1") ;;
|
|
159
|
+
*)
|
|
160
|
+
log "[vnc.sh:websockify] FATAL: ${websockify_bind_file} names no recognised bind address (line=${websockify_bind_line})"
|
|
161
|
+
echo "ERROR: ${websockify_bind_file} does not name a recognised bind address." >&2
|
|
162
|
+
echo " Re-run the installer to rewrite it." >&2
|
|
163
|
+
exit 1
|
|
164
|
+
;;
|
|
165
|
+
esac
|
|
166
|
+
[ -n "$WEBSOCKIFY_BIND_KIND" ] || WEBSOCKIFY_BIND_KIND="unknown"
|
|
167
|
+
}
|
|
129
168
|
# The standing cdp-liveness probe runs every rss-sampler cycle; skip this
|
|
130
169
|
# per-invocation resolution line on that path so vnc-boot.log is not spammed.
|
|
131
170
|
# The FATAL guards above still run (and hard-exit) on every invocation.
|
|
@@ -541,7 +580,12 @@ case "${1:-}" in
|
|
|
541
580
|
pkill -f "websockify.*${WEBSOCKIFY_PORT}" 2>/dev/null || true
|
|
542
581
|
sleep 0.2
|
|
543
582
|
|
|
544
|
-
|
|
583
|
+
# The bind address is the install-time host decision, not a constant: on a
|
|
584
|
+
# host with no private LAN interface the noVNC page and the RFB port behind
|
|
585
|
+
# it must not be reachable from off the box. Task 2089.
|
|
586
|
+
resolve_websockify_bind
|
|
587
|
+
log "websockify-bind address=${WEBSOCKIFY_BIND} kind=${WEBSOCKIFY_BIND_KIND} port=${WEBSOCKIFY_PORT}"
|
|
588
|
+
websockify --web /usr/share/novnc "${WEBSOCKIFY_BIND}:${WEBSOCKIFY_PORT}" "localhost:${RFB_PORT}" >> "$LOG_FILE" 2>&1 &
|
|
545
589
|
|
|
546
590
|
if wait_for_port "${WEBSOCKIFY_PORT}"; then
|
|
547
591
|
log "websockify ready on :${WEBSOCKIFY_PORT} → localhost:${RFB_PORT}"
|
|
@@ -3970,7 +3970,7 @@ async function startConnection(accountId) {
|
|
|
3970
3970
|
const hasAuth = await authExists(authDir);
|
|
3971
3971
|
if (!hasAuth) {
|
|
3972
3972
|
console.error(`${TAG17} no credentials for account=${accountId}`);
|
|
3973
|
-
|
|
3973
|
+
throw new Error(`no credentials for account=${accountId}`);
|
|
3974
3974
|
}
|
|
3975
3975
|
let platformAccountId;
|
|
3976
3976
|
let houseAccountId;
|
|
@@ -4006,7 +4006,9 @@ async function startConnection(accountId) {
|
|
|
4006
4006
|
lidMapping: null
|
|
4007
4007
|
};
|
|
4008
4008
|
connections.set(accountId, conn);
|
|
4009
|
-
|
|
4009
|
+
void connectWithReconnect(conn).catch((err) => {
|
|
4010
|
+
console.error(`${TAG17} connection loop ended in error account=${accountId}: ${formatError(err)}`);
|
|
4011
|
+
});
|
|
4010
4012
|
}
|
|
4011
4013
|
async function stopConnection(accountId) {
|
|
4012
4014
|
const conn = connections.get(accountId);
|
package/payload/server/server.js
CHANGED
|
@@ -180,7 +180,7 @@ import {
|
|
|
180
180
|
waitForConnection,
|
|
181
181
|
warnNonUuidAccount,
|
|
182
182
|
writeChatMetadataMerged
|
|
183
|
-
} from "./chunk-
|
|
183
|
+
} from "./chunk-7MGR5M6G.js";
|
|
184
184
|
import {
|
|
185
185
|
ACCOUNTS_DIR,
|
|
186
186
|
BIN_DIR,
|
|
@@ -5008,7 +5008,7 @@ async function defaultHttpGet(url) {
|
|
|
5008
5008
|
}
|
|
5009
5009
|
}
|
|
5010
5010
|
async function defaultResolve(accountId) {
|
|
5011
|
-
const { resolveSocket: resolveSocket2 } = await import("./manager-
|
|
5011
|
+
const { resolveSocket: resolveSocket2 } = await import("./manager-6XOWHREK.js");
|
|
5012
5012
|
return resolveSocket2(accountId);
|
|
5013
5013
|
}
|
|
5014
5014
|
async function fetchAvatar(accountId, jid, deps = {}) {
|
|
@@ -6620,15 +6620,21 @@ app3.post("/disconnect", async (c) => {
|
|
|
6620
6620
|
}
|
|
6621
6621
|
});
|
|
6622
6622
|
app3.post("/reconnect", async (c) => {
|
|
6623
|
+
let accountId = "(unresolved)";
|
|
6623
6624
|
try {
|
|
6624
6625
|
const body = await c.req.json().catch(() => ({}));
|
|
6625
|
-
|
|
6626
|
+
accountId = validateAccountId(body.accountId);
|
|
6626
6627
|
const denial = selfOrHouseScopeDenial(c, accountId, "reconnect");
|
|
6627
6628
|
if (denial) return denial;
|
|
6628
6629
|
await startConnection(accountId);
|
|
6630
|
+
console.error(
|
|
6631
|
+
`[whatsapp:route] op=reconnect account=${accountId} started=true outcome=accepted reason=none`
|
|
6632
|
+
);
|
|
6629
6633
|
return c.json({ reconnecting: true, accountId });
|
|
6630
6634
|
} catch (err) {
|
|
6631
|
-
console.error(
|
|
6635
|
+
console.error(
|
|
6636
|
+
`[whatsapp:route] op=reconnect account=${accountId} started=false outcome=refused reason=${String(err)}`
|
|
6637
|
+
);
|
|
6632
6638
|
return c.json({ error: String(err) }, 500);
|
|
6633
6639
|
}
|
|
6634
6640
|
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# Search-surface parity contract
|
|
2
|
-
|
|
3
|
-
Three operator/agent surfaces run the same `hybrid()` search engine
|
|
4
|
-
(`platform/lib/graph-search`). The contract: **the same query yields the same
|
|
5
|
-
node set on every surface** unless a surface declares an explicit, documented
|
|
6
|
-
divergence.
|
|
7
|
-
|
|
8
|
-
| Surface | Entry point | Labels default | Vector floor |
|
|
9
|
-
|---|---|---|---|
|
|
10
|
-
| Chat (agent recall) | `memory-search` MCP tool → `memorySearch()` | none set ⇒ all labels | `DEFAULT_VECTOR_THRESHOLD` (0.82) |
|
|
11
|
-
| `/graph` admin page | `GET /api/admin/graph-search` | no chips ⇒ all labels; chips narrow | `DEFAULT_VECTOR_THRESHOLD` (0.82), `?threshold` overrides |
|
|
12
|
-
| `/data` admin page | `GET /api/admin/graph-search?labels=FileArtifact` | `FileArtifact` only (files) | `DEFAULT_VECTOR_THRESHOLD` (0.82), `?threshold` overrides; show-all sends `?threshold=0` |
|
|
13
|
-
|
|
14
|
-
## The two parity rules (Task 635)
|
|
15
|
-
|
|
16
|
-
1. **All labels by default.** A caller that passes no labels (chat) or an empty
|
|
17
|
-
chip set (`/graph`) searches every label — the route forwards `undefined` to
|
|
18
|
-
`hybrid()`, the lib's no-label gate, which queries every vector index. The
|
|
19
|
-
`*` wildcard is a separate explicit opt-in to the same all-labels path (no
|
|
20
|
-
current caller sends it; `/data` sends a real `FileArtifact` label instead).
|
|
21
|
-
`/graph` no longer rejects an empty chip set with a 400; chips, when present,
|
|
22
|
-
narrow post-filter.
|
|
23
|
-
|
|
24
|
-
2. **One vector floor, defined once.** `DEFAULT_VECTOR_THRESHOLD = 0.82` lives
|
|
25
|
-
in `platform/lib/graph-search/src/index.ts` and is the single source of
|
|
26
|
-
truth. Each *surface* opts into it as its default; `hybrid()` itself keeps
|
|
27
|
-
the `vectorThreshold === undefined ⇒ no floor` contract. Two literals of
|
|
28
|
-
0.82 in two files would be a drift vector — there is one constant, imported
|
|
29
|
-
by both the route (`src`) and `memory-search` (`dist`, so the lib must be
|
|
30
|
-
rebuilt for the memory plugin to see a change).
|
|
31
|
-
|
|
32
|
-
## Declared divergences (allowed, by explicit caller or post-filter)
|
|
33
|
-
|
|
34
|
-
- **`/data` is files-only.** It sends `labels=FileArtifact` — a deliberate
|
|
35
|
-
server-side caller filter to file nodes, not a parity break. (It also
|
|
36
|
-
triggers the route's `FileArtifact` index-reconcile branch before searching.)
|
|
37
|
-
- **`graph-subgraph` pivot mask.** `GET /api/admin/graph-subgraph?q=…` runs
|
|
38
|
-
`hybrid()` with `expandHops: 0` and **no** vector floor. Its output is not a
|
|
39
|
-
result list — it is intersected with a clicked node's neighbourhood to
|
|
40
|
-
narrow a pivot. "Same query, same results" does not apply to an intersection
|
|
41
|
-
mask, so it intentionally stays unthresholded.
|
|
42
|
-
- **Post-search projections** — the chat public-twin property rewrite and the
|
|
43
|
-
`/graph` Message→Conversation parent merge run after the engine returns and
|
|
44
|
-
do not change which nodes the engine selected.
|
|
45
|
-
|
|
46
|
-
## Diagnostics
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
logs-read.sh --tail server 100 | grep -E '\[graph-search\]'
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
- `op=labels-default applied=all` — a no-chip `/graph` request took the
|
|
53
|
-
all-labels path. Its absence on a no-labels request is the regression signal.
|
|
54
|
-
- `labels=all` (query line) — no chips (all labels); `labels=*` — explicit
|
|
55
|
-
wildcard opt-in; `labels=FileArtifact` — `/data` files search;
|
|
56
|
-
`labels=Person,…` — narrowed chip search.
|
|
57
|
-
- `threshold=0.82` on a default request; `threshold=off` only when an explicit
|
|
58
|
-
`?threshold=0` was passed.
|