@rubytech/create-maxy-code 0.1.486 → 0.1.488
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/package.json +1 -1
- package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -3
- package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +30 -8
- package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +22 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +23 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/schema-exposed-dirs.test.ts +43 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +12 -2
- package/payload/platform/plugins/memory/PLUGIN.md +1 -0
- package/payload/platform/plugins/memory/references/schema-construction.md +13 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.js +12 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.d.ts +8 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.js +12 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js +12 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js.map +1 -1
- package/payload/platform/plugins/storage-broker/PLUGIN.md +1 -0
- package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +22 -2
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +23 -2
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
- package/payload/server/public/activity.html +6 -6
- package/payload/server/public/assets/{AdminLoginScreens-DF9S5Rca.js → AdminLoginScreens-Dic_fPt6.js} +1 -1
- package/payload/server/public/assets/{AdminShell-B0PNDCAb.js → AdminShell-JN7rZ8aG.js} +1 -1
- package/payload/server/public/assets/{Checkbox-s5X6dso4.js → Checkbox-CB74kINA.js} +1 -1
- package/payload/server/public/assets/{activity-BRBsaalV.js → activity-DbfpHg3i.js} +1 -1
- package/payload/server/public/assets/{admin-BoVCP4Mw.js → admin-CThbXaYH.js} +1 -1
- package/payload/server/public/assets/{bot-BVO9Uqr_.js → bot-DnuyRsLS.js} +1 -1
- package/payload/server/public/assets/{browser-BVW4FFii.js → browser-q4EFqKvR.js} +1 -1
- package/payload/server/public/assets/{calendar-CWsD3uLk.js → calendar-C-q9kWmA.js} +1 -1
- package/payload/server/public/assets/chat-DcefIV9q.js +1 -0
- package/payload/server/public/assets/chevron-down-Blma_CKl.js +1 -0
- package/payload/server/public/assets/chevron-left-xeYDxFiC.js +1 -0
- package/payload/server/public/assets/chevron-right-BonbtjqJ.js +1 -0
- package/payload/server/public/assets/data-D5dp6nj6.js +1 -0
- package/payload/server/public/assets/{graph-mJ9ky5dc.js → graph-CNgt7ygi.js} +1 -1
- package/payload/server/public/assets/{graph-labels-C8-sIKuv.js → graph-labels-DA48HrRR.js} +1 -1
- package/payload/server/public/assets/{maximize-2-yucCepu_.js → maximize-2-k3Fu5hdP.js} +1 -1
- package/payload/server/public/assets/{operator-911z_Fy_.js → operator-8NVpNLeD.js} +1 -1
- package/payload/server/public/assets/{page-Cv0qUwMG.js → page-Bvx0A2jB.js} +1 -1
- package/payload/server/public/assets/{page-CgSgj8Cy.js → page-DhwPG5Zf.js} +1 -1
- package/payload/server/public/assets/{public-BzwveFwz.js → public-BJ5Oo2Fw.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-BH51cf2Q.js → rotate-ccw-D-ov5jM4.js} +1 -1
- package/payload/server/public/assets/tasks-DHMxTZVD.js +1 -0
- package/payload/server/public/assets/{time-entry-format-DPdGkp1C.js → time-entry-format-EmV5lz6o.js} +1 -1
- package/payload/server/public/assets/{triangle-alert-DO2rjjfV.js → triangle-alert-CivyPfAL.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-HMSM7-Ir.js → useCopyFeedback-hbnPS-r9.js} +1 -1
- package/payload/server/public/assets/{useSubAccountSwitcher-2ojOOYNo.css → useSubAccountSwitcher-kwhG-aJj.css} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-BcukQIza.js → useVoiceRecorder-QN9_GqB_.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +6 -6
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +10 -10
- package/payload/server/public/graph.html +11 -11
- package/payload/server/public/index.html +16 -16
- package/payload/server/public/operator.html +15 -15
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +155 -103
- package/payload/server/public/assets/chat-DKGREuIO.js +0 -1
- package/payload/server/public/assets/chevron-down-m581tl1J.js +0 -1
- package/payload/server/public/assets/chevron-left-BAY0xfmR.js +0 -1
- package/payload/server/public/assets/chevron-right-Dy_REdYZ.js +0 -1
- package/payload/server/public/assets/data-CtOxa94P.js +0 -1
- package/payload/server/public/assets/tasks-21egY7A6.js +0 -1
- /package/payload/server/public/assets/{useSubAccountSwitcher-DLvqvfoc.js → useSubAccountSwitcher-DotTrvX3.js} +0 -0
package/package.json
CHANGED
|
@@ -71,7 +71,7 @@ The setup-done claim only fires when `curl -I https://<hostname>` issued from ou
|
|
|
71
71
|
|
|
72
72
|
## Data-portal folder index
|
|
73
73
|
|
|
74
|
-
The portal shows a sub-account's deliverable folders alongside what the client uploaded. Which folders those are is never a list in the code: it is derived per account from that account's own `SCHEMA.md` — the buckets in its `<!-- ontology-buckets -->` region plus `output`, intersected with its `allowed-top-level` block. A brand on a different vertical exposes different folders with no code change. `bin/schema-exposed-dirs.mjs` is the only place that decision is made.
|
|
74
|
+
The portal shows a sub-account's deliverable folders alongside what the client uploaded. Which folders those are is never a list in the code: it is derived per account from that account's own `SCHEMA.md` — the buckets in its `<!-- ontology-buckets -->` region plus `output`, intersected with its `allowed-top-level` block. A brand on a different vertical exposes different folders with no code change. `bin/schema-exposed-dirs.mjs` is the only place that decision is made. An operator can add non-ontology folders (a workflow folder such as `inbound-invoices`) by listing them in `exposeFolders` in the account's `data-portal.json`; each must match `[A-Za-z0-9_-]+` and is never a system folder (`agents`, `secrets`, `specialists`, `state`, `memory`, `cache`, `logs`, `tmp`) or dot-prefixed. Those extras union on top of the schema-derived set and, unlike ontology buckets, are not intersected with `allowed-top-level`; empty or absent behaves as before.
|
|
75
75
|
|
|
76
76
|
`bin/portal-index-push.mjs` walks those folders and full-replaces the account's rows in the portal's D1 `directory` table each cycle. Metadata only — names, sizes, times — so listing survives the device being offline, and a deleted or renamed file converges on the next cycle without a tombstone. The scheduling heartbeat runs it hourly for every account carrying a `data-portal.json`; it is also runnable standalone, which is how it stays operable while the heartbeat spawn path is unproven on a device.
|
|
77
77
|
|
|
@@ -82,7 +82,7 @@ Exposure is narrowed twice. The install re-derives the account's exposed set fro
|
|
|
82
82
|
What to watch, on the device:
|
|
83
83
|
|
|
84
84
|
```
|
|
85
|
-
[portal-index] op=resolve account=<id> schemaPresent=<bool> allowed=<n> domainBuckets=<dirs|none> exposed=<dirs|none>
|
|
85
|
+
[portal-index] op=resolve account=<id> schemaPresent=<bool> allowed=<n> domainBuckets=<dirs|none> extraFolders=<n> exposed=<dirs|none>
|
|
86
86
|
[portal-index] op=replace account=<id> deleted=all inserted=<n>
|
|
87
87
|
[portal-index] op=verify account=<id> rowsAfter=<n> expected=<n>
|
|
88
88
|
[portal-index] op=collision account=<id> dir=<dir> claimedBy=plugin-owned
|
|
@@ -90,7 +90,7 @@ What to watch, on the device:
|
|
|
90
90
|
[portal-index] op=audit accounts=<n> stale=<n> never-ran=<n>
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
`exposed=none` where deliverables exist means the schema was missing or carried no ontology region — not that the walk found nothing. `rowsAfter` disagreeing with `expected` is a half-applied replace. An `op=collision` line explains a folder that has silently vanished from the client's view, which nothing else would report. `op=exposed-changed` fires only on a delta, so it is rare and always worth reading; a `removed=` naming a populated bucket is the client-visible regression.
|
|
93
|
+
`exposed=none` where deliverables exist means the schema was missing or carried no ontology region — not that the walk found nothing. `rowsAfter` disagreeing with `expected` is a half-applied replace. An `op=collision` line explains a folder that has silently vanished from the client's view, which nothing else would report. `op=exposed-changed` fires only on a delta, so it is rare and always worth reading; a `removed=` naming a populated bucket is the client-visible regression. `extraFolders=<n>` counts the folders the operator requested in `exposeFolders`; a non-zero `extraFolders` with none of those names in `exposed=` means the guard dropped every one.
|
|
94
94
|
|
|
95
95
|
`op=audit` is the standing check, and it is the only one that speaks when the push does **not**. It runs in-process inside the heartbeat rather than as a spawn, and reads only local state, so it cannot be blinded by the same failure it is watching for. It fires hourly even when everything is healthy — an audit that goes quiet on success is indistinguishable from an audit that stopped running. `result=never-ran` on an account with a `data-portal.json` means no cycle has ever succeeded for it; `result=stale` with `ageMin` means cycles have stopped. Its input is written only after `op=verify`, so a stale entry means the push stopped succeeding rather than merely stopped logging.
|
|
96
96
|
|
|
@@ -42,6 +42,20 @@ function readEnvFile(path) {
|
|
|
42
42
|
return out
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** Operator-named extra folders for an account, from its data-portal.json.
|
|
46
|
+
* Absent file, absent key, or a non-array value all yield []. The resolver
|
|
47
|
+
* applies the charset-and-system guard; this reader does not pre-filter, so the
|
|
48
|
+
* `extraFolders=<n>` count reflects what the operator asked for.
|
|
49
|
+
* @param {string} accountDir @returns {Promise<string[]>} */
|
|
50
|
+
export async function readExposeFolders(accountDir) {
|
|
51
|
+
try {
|
|
52
|
+
const cfg = JSON.parse(await readFile(join(accountDir, 'data-portal.json'), 'utf8'))
|
|
53
|
+
return Array.isArray(cfg?.exposeFolders) ? cfg.exposeFolders.filter((f) => typeof f === 'string') : []
|
|
54
|
+
} catch {
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
/**
|
|
46
60
|
* Files under `exposed` dirs, at any depth. Dot-prefixed entries are skipped at
|
|
47
61
|
* every level: `.uploads-tmp/` holds in-flight upload partials (files.ts:25) and
|
|
@@ -85,7 +99,7 @@ export async function walkExposed(accountDir, exposed) {
|
|
|
85
99
|
* @param {{ accountDir: string, accountId: string, dbName: string,
|
|
86
100
|
* client: { query: (sql: string, params?: unknown[]) => Promise<Record<string, unknown>[]> },
|
|
87
101
|
* log: (line: string) => void, nowIso: string,
|
|
88
|
-
* previousExposed?: string[] }} opts
|
|
102
|
+
* exposeFolders?: string[], previousExposed?: string[] }} opts
|
|
89
103
|
* @returns {Promise<{ exposed: string[], rows: number, schemaPresent: boolean }>}
|
|
90
104
|
*/
|
|
91
105
|
export async function pushAccount(opts) {
|
|
@@ -98,10 +112,15 @@ export async function pushAccount(opts) {
|
|
|
98
112
|
schemaText = null
|
|
99
113
|
}
|
|
100
114
|
|
|
101
|
-
const
|
|
115
|
+
const exposeFolders = Array.isArray(opts.exposeFolders) ? opts.exposeFolders : []
|
|
116
|
+
const r = resolveExposedDirs(schemaText, exposeFolders)
|
|
117
|
+
// extraFolders is the REQUESTED count, not the accepted count: a non-zero
|
|
118
|
+
// extraFolders with none of those names in exposed= means the guard dropped
|
|
119
|
+
// every one, which is distinct from none configured (extraFolders=0).
|
|
102
120
|
log(
|
|
103
121
|
`${TAG} op=resolve account=${accountId} schemaPresent=${r.schemaPresent} allowed=${r.allowed.length} ` +
|
|
104
|
-
`domainBuckets=${r.domainBuckets.join(',') || 'none'}
|
|
122
|
+
`domainBuckets=${r.domainBuckets.join(',') || 'none'} extraFolders=${exposeFolders.length} ` +
|
|
123
|
+
`exposed=${r.exposed.join(',') || 'none'}`,
|
|
105
124
|
)
|
|
106
125
|
// The generator gives a plugin-claimed dir to the plugin-owned region only
|
|
107
126
|
// (account-schema-owned-dirs.py:287), so this is normally silent. When it is
|
|
@@ -212,10 +231,10 @@ function arg(flag) {
|
|
|
212
231
|
* Every account carrying a data-portal.json has a portal. The file's presence is
|
|
213
232
|
* the enrolment signal — there is no separate flag to keep in sync with it.
|
|
214
233
|
* @param {string} accountsRoot
|
|
215
|
-
* @returns {Promise<{accountDir: string, accountId: string, dbName: string}[]>}
|
|
234
|
+
* @returns {Promise<{accountDir: string, accountId: string, dbName: string, exposeFolders: string[]}[]>}
|
|
216
235
|
*/
|
|
217
236
|
async function portalAccounts(accountsRoot) {
|
|
218
|
-
/** @type {{accountDir: string, accountId: string, dbName: string}[]} */ const out = []
|
|
237
|
+
/** @type {{accountDir: string, accountId: string, dbName: string, exposeFolders: string[]}[]} */ const out = []
|
|
219
238
|
/** @type {string[]} */ let names
|
|
220
239
|
try {
|
|
221
240
|
names = await readdir(accountsRoot)
|
|
@@ -227,7 +246,10 @@ async function portalAccounts(accountsRoot) {
|
|
|
227
246
|
try {
|
|
228
247
|
const cfg = JSON.parse(await readFile(join(accountDir, 'data-portal.json'), 'utf8'))
|
|
229
248
|
if (typeof cfg?.portalDbName === 'string' && cfg.portalDbName) {
|
|
230
|
-
|
|
249
|
+
const exposeFolders = Array.isArray(cfg?.exposeFolders)
|
|
250
|
+
? cfg.exposeFolders.filter((f) => typeof f === 'string')
|
|
251
|
+
: []
|
|
252
|
+
out.push({ accountDir, accountId, dbName: cfg.portalDbName, exposeFolders })
|
|
231
253
|
} else {
|
|
232
254
|
console.error(`${TAG} op=skip account=${accountId} reason=no-portalDbName`)
|
|
233
255
|
}
|
|
@@ -292,7 +314,7 @@ async function main() {
|
|
|
292
314
|
process.exit(1)
|
|
293
315
|
}
|
|
294
316
|
|
|
295
|
-
/** @type {{accountDir: string, accountId: string, dbName: string}[]} */ let targets
|
|
317
|
+
/** @type {{accountDir: string, accountId: string, dbName: string, exposeFolders: string[]}[]} */ let targets
|
|
296
318
|
if (process.argv.includes('--all-accounts')) {
|
|
297
319
|
// Same derivation as the platform's own DATA_ROOT (data-path.ts:19), from
|
|
298
320
|
// the PLATFORM_ROOT the heartbeat already passes.
|
|
@@ -307,7 +329,7 @@ async function main() {
|
|
|
307
329
|
console.error(`${TAG} op=usage --account-dir <dir> --account <id> --db <name>`)
|
|
308
330
|
process.exit(1)
|
|
309
331
|
}
|
|
310
|
-
targets = [{ accountDir, accountId, dbName }]
|
|
332
|
+
targets = [{ accountDir, accountId, dbName, exposeFolders: await readExposeFolders(accountDir) }]
|
|
311
333
|
}
|
|
312
334
|
|
|
313
335
|
const state = readState(platformRoot)
|
|
@@ -21,6 +21,18 @@ const OWNED_END = '<!-- plugin-owned-dirs:end -->'
|
|
|
21
21
|
/** The delivery convention, always exposed when the account allows it. */
|
|
22
22
|
export const ALWAYS_EXPOSED = 'output'
|
|
23
23
|
|
|
24
|
+
/** Folders that must never be named into exposure, even by an operator. The
|
|
25
|
+
* dot-prefixed `.git`/`.claude` are already excluded by the charset guard
|
|
26
|
+
* below. This is the not-system half of the extra-folder bound. */
|
|
27
|
+
const SYSTEM_DIRS = new Set(['agents', 'secrets', 'specialists', 'state', 'memory', 'cache', 'logs', 'tmp'])
|
|
28
|
+
|
|
29
|
+
/** An operator-named extra folder is accepted only as a single safe segment
|
|
30
|
+
* (the same charset the per-person `--folders` grant enforces) and never a
|
|
31
|
+
* system dir. This drops dot-prefixed names, slashes, and traversal such as
|
|
32
|
+
* `jobs/../secrets`, which would otherwise let the push walk index files under
|
|
33
|
+
* a system dir that the fetch route would refuse to serve. */
|
|
34
|
+
const SAFE_FOLDER = /^[A-Za-z0-9_-]+$/
|
|
35
|
+
|
|
24
36
|
/**
|
|
25
37
|
* The generator writes the ontology region with an ASCII hyphen and the
|
|
26
38
|
* plugin-owned region with an em dash (account-schema-owned-dirs.py:294 vs
|
|
@@ -53,10 +65,11 @@ function allowedBlockOrNull(text) {
|
|
|
53
65
|
|
|
54
66
|
/**
|
|
55
67
|
* @param {string | null} schemaText Contents of accounts/<id>/SCHEMA.md, or null when absent.
|
|
68
|
+
* @param {string[]} [exposeFolders] Operator-named extra top-level folders. Empty or absent = today.
|
|
56
69
|
* @returns {{ schemaPresent: boolean, allowed: string[], domainBuckets: string[],
|
|
57
70
|
* pluginOwned: string[], collisions: string[], exposed: string[] }}
|
|
58
71
|
*/
|
|
59
|
-
export function resolveExposedDirs(schemaText) {
|
|
72
|
+
export function resolveExposedDirs(schemaText, exposeFolders = []) {
|
|
60
73
|
const empty = {
|
|
61
74
|
schemaPresent: false,
|
|
62
75
|
allowed: /** @type {string[]} */ ([]),
|
|
@@ -97,7 +110,14 @@ export function resolveExposedDirs(schemaText) {
|
|
|
97
110
|
const collisions = domainBuckets.filter((d) => pluginOwned.includes(d))
|
|
98
111
|
|
|
99
112
|
const candidates = [ALWAYS_EXPOSED, ...domainBuckets.filter((d) => !collisions.includes(d))]
|
|
100
|
-
const
|
|
113
|
+
const ontologyExposed = [...new Set(candidates)].filter((d) => allowed.includes(d))
|
|
114
|
+
// Extras are unioned AFTER the ontology step and are deliberately NOT
|
|
115
|
+
// intersected with `allowed`: a workflow folder is legitimately outside the
|
|
116
|
+
// ontology set. The charset-plus-deny guard is the whole bound.
|
|
117
|
+
const extras = (Array.isArray(exposeFolders) ? exposeFolders : []).filter(
|
|
118
|
+
(f) => typeof f === 'string' && SAFE_FOLDER.test(f) && !SYSTEM_DIRS.has(f),
|
|
119
|
+
)
|
|
120
|
+
const exposed = [...new Set([...ontologyExposed, ...extras])].sort()
|
|
101
121
|
|
|
102
122
|
return { schemaPresent: true, allowed, domainBuckets, pluginOwned, collisions, exposed }
|
|
103
123
|
}
|
|
@@ -173,4 +173,27 @@ describe('pushAccount', () => {
|
|
|
173
173
|
await push(fakeClient(), { log: (l: string) => lines.push(l) })
|
|
174
174
|
expect(lines.some((l) => l.includes('schemaPresent=false') && l.includes('exposed=none'))).toBe(true)
|
|
175
175
|
})
|
|
176
|
+
|
|
177
|
+
it('op=resolve carries extraFolders and the walk includes an operator folder', async () => {
|
|
178
|
+
// inbound-invoices is NOT in the fixture allowed block, so this also proves
|
|
179
|
+
// an extra bypasses the ontology/allowed intersection.
|
|
180
|
+
file('inbound-invoices/INV-42.pdf')
|
|
181
|
+
const lines: string[] = []
|
|
182
|
+
const c = fakeClient()
|
|
183
|
+
await push(c, { log: (l: string) => lines.push(l), exposeFolders: ['inbound-invoices'] })
|
|
184
|
+
expect(lines.some((l) => /op=resolve .*extraFolders=1/.test(l))).toBe(true)
|
|
185
|
+
expect(lines.some((l) => /exposed=.*inbound-invoices/.test(l))).toBe(true)
|
|
186
|
+
const inserted = c.calls.filter((x) => /INSERT INTO directory/i.test(x.sql)).map((x) => String(x.params[1]))
|
|
187
|
+
expect(inserted).toContain('inbound-invoices/INV-42.pdf')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('with no exposeFolders the operator folder is not walked and extraFolders=0', async () => {
|
|
191
|
+
file('inbound-invoices/INV-42.pdf')
|
|
192
|
+
const lines: string[] = []
|
|
193
|
+
const c = fakeClient()
|
|
194
|
+
await push(c, { log: (l: string) => lines.push(l) })
|
|
195
|
+
const inserted = c.calls.filter((x) => /INSERT INTO directory/i.test(x.sql)).map((x) => String(x.params[1]))
|
|
196
|
+
expect(inserted).not.toContain('inbound-invoices/INV-42.pdf')
|
|
197
|
+
expect(lines.some((l) => /op=resolve .*extraFolders=0/.test(l))).toBe(true)
|
|
198
|
+
})
|
|
176
199
|
})
|
|
@@ -85,4 +85,47 @@ describe('resolveExposedDirs', () => {
|
|
|
85
85
|
const r = resolveExposedDirs('# no fence here')
|
|
86
86
|
expect(r.exposed).toEqual([])
|
|
87
87
|
})
|
|
88
|
+
|
|
89
|
+
it('unions an operator-named folder into exposed', () => {
|
|
90
|
+
const r = resolveExposedDirs(schema({ ontology: ONTOLOGY }), ['inbound-invoices'])
|
|
91
|
+
expect(r.exposed).toEqual(['inbound-invoices', 'output', 'quotes'])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('exposes an extra folder that is absent from the allowed block', () => {
|
|
95
|
+
// inbound-invoices is deliberately NOT in ALLOWED — extras bypass that intersection.
|
|
96
|
+
const r = resolveExposedDirs(schema({ ontology: ONTOLOGY }), ['inbound-invoices'])
|
|
97
|
+
expect(r.exposed).toContain('inbound-invoices')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('drops a system folder named as an extra', () => {
|
|
101
|
+
const r = resolveExposedDirs(schema({ ontology: ONTOLOGY }), ['secrets'])
|
|
102
|
+
expect(r.exposed).not.toContain('secrets')
|
|
103
|
+
expect(r.exposed).toEqual(['output', 'quotes'])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('drops a traversal or dotted extra', () => {
|
|
107
|
+
const r = resolveExposedDirs(schema({ ontology: ONTOLOGY }), ['jobs/../secrets', '.git', '..'])
|
|
108
|
+
expect(r.exposed).toEqual(['output', 'quotes'])
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('an empty extras list equals today', () => {
|
|
112
|
+
const base = resolveExposedDirs(schema({ ontology: ONTOLOGY }))
|
|
113
|
+
expect(resolveExposedDirs(schema({ ontology: ONTOLOGY }), []).exposed).toEqual(base.exposed)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('never applies extras when the schema is missing', () => {
|
|
117
|
+
const r = resolveExposedDirs(null, ['inbound-invoices'])
|
|
118
|
+
expect(r.exposed).toEqual([])
|
|
119
|
+
expect(r.schemaPresent).toBe(false)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('never applies extras when the allowed block is absent', () => {
|
|
123
|
+
const r = resolveExposedDirs('# no fence here', ['inbound-invoices'])
|
|
124
|
+
expect(r.exposed).toEqual([])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('dedupes an extra that is already an ontology bucket', () => {
|
|
128
|
+
const r = resolveExposedDirs(schema({ ontology: ONTOLOGY }), ['quotes'])
|
|
129
|
+
expect(r.exposed).toEqual(['output', 'quotes'])
|
|
130
|
+
})
|
|
88
131
|
})
|
|
@@ -182,17 +182,27 @@ An account with no `SCHEMA.md` publishes nothing at all. That is deliberate and
|
|
|
182
182
|
what the write-path guard does — the guard fails open so an unseeded account is not write-blocked,
|
|
183
183
|
while a surface facing a client must fail closed.
|
|
184
184
|
|
|
185
|
+
An operator can publish extra folders that are not ontology buckets by listing them in
|
|
186
|
+
`exposeFolders` in the account's `data-portal.json`, for example a workflow folder like
|
|
187
|
+
`inbound-invoices`. Each name must be a single folder matching `[A-Za-z0-9_-]+` and is refused if it
|
|
188
|
+
is a system folder (`agents`, `secrets`, `specialists`, `state`, `memory`, `cache`, `logs`, `tmp`) or
|
|
189
|
+
dot-prefixed. These extras are added on top of the schema-derived set and, unlike ontology buckets,
|
|
190
|
+
are not intersected with the `allowed-top-level` block. An empty or absent list behaves exactly as
|
|
191
|
+
before.
|
|
192
|
+
|
|
185
193
|
The heartbeat runs this hourly for every account carrying a `data-portal.json`. Run it once by hand
|
|
186
194
|
after assembly so the client has something to see on their first sign-in, and check the output names
|
|
187
195
|
the folders you expect:
|
|
188
196
|
|
|
189
197
|
```
|
|
190
|
-
[portal-index] op=resolve account=<id> schemaPresent=true allowed=22 domainBuckets=quotes exposed=output,quotes
|
|
198
|
+
[portal-index] op=resolve account=<id> schemaPresent=true allowed=22 domainBuckets=quotes extraFolders=1 exposed=inbound-invoices,output,quotes
|
|
191
199
|
[portal-index] op=verify account=<id> rowsAfter=41 expected=41
|
|
192
200
|
```
|
|
193
201
|
|
|
194
202
|
`exposed=none` on an account that has deliverables means the schema was missing or its ontology
|
|
195
|
-
region was absent — not that the walk found nothing.
|
|
203
|
+
region was absent — not that the walk found nothing. `extraFolders=<n>` counts the folders the
|
|
204
|
+
operator requested in `exposeFolders`; a non-zero `extraFolders` with none of those names in
|
|
205
|
+
`exposed=` means the guard dropped every one (a system or dot name, or a bad spelling).
|
|
196
206
|
|
|
197
207
|
## How a client browses, downloads and uploads
|
|
198
208
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memory
|
|
3
3
|
description: "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-write, memory-update, memory-edge (create/delete typed directed edges between pre-existing nodes), memory-update-by-name (repair surface that resolves an elementId from a (label, name, accountId) tuple when search cannot return one), memory-lookup-by-name (deterministic read surface that returns nodes by case-insensitive exact `name` match — a Person, which carries no `name`, is matched on its composed `givenName` + `familyName` display name instead — optionally constrained by labels, bypassing the memory-search ranking stack so a present entity is never missed because a diluted query ranked it below the cut), and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion goes through memory-ingest; the dispatched specialist produces typed-section JSON in-turn from the loaded ontology. Operator-solicited reclassify of an already-stored thread-shaped KD (today: email-thread KDs) is exposed as `kd-classify <attachmentId>` — runs a body-growth gate (proceed iff body grew ≥25% since the last classify, or first-ever classify), stages the body to a temp file under `$ACCOUNT_DIR/tmp/`, and returns a dispatch envelope for the `librarian`; on success `memory-ingest` replaces the prior :Section children and stamps `lastClassifiedAt` + `lastClassifiedBodyLength` on the parent KD. Two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :ConversationArchive (keyed on `conversationIdentity`) + :Section chunks; two parent labels, two writer paths chosen by which identity property is set. Conversation-archive Phase 2 splits across two read-only tools: `conversation-archive-list-chunks` pages chunk bodies for the dispatched specialist to read in-turn; `conversation-archive-derive-insights` converts the specialist's per-chunk claims into operator-facing proposals with per-kind cypher. `conversation-archive-enrich-rejection` records (or undoes) durable per-row rejections so already-triaged claims do not re-surface on re-runs. Ships five skills: `conversational-memory`, `document-ingest`, `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack), `conversation-archive-mcp` (operator-initiated MCP-driven ingest for Granola, Otter, Circleback meetings — sibling of `conversation-archive` sharing the same writer surface and identity formula), and `conversation-archive-enrich` (per-row operator-gated insight derivation over a named archive's chunks). Ships three thinking-tool skills: `challenge` (adversarial retrieval — strongest counter-evidence from the graph for a given claim), `connect` (bridge-finding between two topics via shared graph neighbors), and `emerge` (clusters uncategorised KnowledgeDocument and Section nodes into operator-approved Concept proposals)."
|
|
4
|
+
account-owned-dirs: [{"dir": "archive", "description": "Archived obsidian imports and their embedded attachments."}]
|
|
4
5
|
tools:
|
|
5
6
|
- name: memory-search
|
|
6
7
|
publicAllowlist: false
|
|
@@ -29,6 +29,7 @@ field-service domain where the captured reference does not document the entity a
|
|
|
29
29
|
| Quote | `Quote` | `Job` | `schema:Order` (estimate) | `accountId`, `ref`, `status`, `totalPaymentDue`, `date` | authored |
|
|
30
30
|
| Part | `Part` | | `schema:Product` | `accountId`, `partId`, `name`, `sku`, `partCategory` | authored |
|
|
31
31
|
| Purchase Order | `PurchaseOrder` | `Job` | `schema:Order` | `accountId`, `poNumber`, `status`, `completionStatus`, `dateRaised` | JobLogic-exact |
|
|
32
|
+
| Supplier | `Supplier` | | `schema:Organization` | `accountId`, `supplierId`, `accountNumber` | authored |
|
|
32
33
|
| PPM Contract | `PpmContract` | | `schema:Service` (contract) | `accountId`, `contractId`, `name`, `startDate`, `endDate` | authored |
|
|
33
34
|
| WhatsApp Group | `WhatsAppGroup` | | `cdm:Channel` | `accountId`, `groupId`, `jobId`, `clientName` | n/a (transport) |
|
|
34
35
|
|
|
@@ -38,6 +39,11 @@ bucket. A named `Owner` (here `Job`) is a job-owned entity: its records live und
|
|
|
38
39
|
the owning bucket's folder (`jobs/<jobId>/…`), not a sibling root bucket. The
|
|
39
40
|
account SCHEMA.md projection derives a root bucket only for blank-`Owner` labels.
|
|
40
41
|
|
|
42
|
+
`Supplier` appears in this table so the projection derives its `suppliers/` bucket,
|
|
43
|
+
which is where a received supplier invoice is filed. It remains an additional label on
|
|
44
|
+
`:Organization` as described below; the row adds a filesystem bucket, not a standalone
|
|
45
|
+
node type.
|
|
46
|
+
|
|
41
47
|
`Job.status` is one of JobLogic's job statuses: `New Job`, `Allocated`, `Attended`,
|
|
42
48
|
`Parts To Fit`, `Awaiting Parts`, `Completed`, `Costed`, `Reqs. Invoice`, `Invoiced`, `Paid`,
|
|
43
49
|
`Cancelled`, `Recall`. `Job.jobType` is the highest categorisation (Call out, Installation,
|
|
@@ -184,6 +190,13 @@ one sheet per valuation cycle is a `:Valuation`, a variations sheet is the `:Var
|
|
|
184
190
|
register, a costs sheet is the `:JobCost` lines, and per-supplier sheets are the procurement
|
|
185
191
|
packages. Folder and workbook are two views of one job; both project into this same graph.
|
|
186
192
|
|
|
193
|
+
The `Suppliers/` row above is the per-job procurement package, held inside one job folder.
|
|
194
|
+
Separately, the account root carries a projected `suppliers/` bucket, one folder per
|
|
195
|
+
`Supplier` record, holding that supplier's artefacts across every job. A received supplier
|
|
196
|
+
invoice is filed at `suppliers/<supplier>/Invoices/`. It is never filed to a top-level
|
|
197
|
+
folder named after the `InboundInvoice` label; that label describes the node, not the
|
|
198
|
+
folder.
|
|
199
|
+
|
|
187
200
|
Operators vary this template — they rename folders, add trades, restructure their workbook. This
|
|
188
201
|
template is the shipped-generic default the agent projects against; the graph follows this one
|
|
189
202
|
canonical ontology, not a per-operator schema. Where a real folder does not fit the default, the
|
package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"free-busy-url.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/free-busy-url.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildFreeBusyUrl } from '../free-busy-url.js';
|
|
3
|
+
describe('buildFreeBusyUrl', () => {
|
|
4
|
+
it('carries the account id and the escaped from/to', () => {
|
|
5
|
+
const url = buildFreeBusyUrl('19500', 'dddb9148-7ba4-4de7-a0da-6eadc5743eda', '2026-07-21T00:00:00Z', '2026-08-05T00:00:00Z');
|
|
6
|
+
expect(url).toContain('http://127.0.0.1:19500/api/calendar/free-busy');
|
|
7
|
+
expect(url).toContain('account=dddb9148-7ba4-4de7-a0da-6eadc5743eda');
|
|
8
|
+
expect(url).toContain('from=2026-07-21T00%3A00%3A00Z');
|
|
9
|
+
expect(url).toContain('to=2026-08-05T00%3A00%3A00Z');
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
//# sourceMappingURL=free-busy-url.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"free-busy-url.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/free-busy-url.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEtD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,EAAE,sCAAsC,EAAE,sBAAsB,EAAE,sBAAsB,CAAC,CAAA;QAC7H,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,+CAA+C,CAAC,CAAA;QACtE,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,8CAA8C,CAAC,CAAA;QACrE,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,+BAA+B,CAAC,CAAA;QACtD,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,6BAA6B,CAAC,CAAA;IACtD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the device-local free/busy URL the publish dispatcher fetches. The
|
|
3
|
+
* `account` param names which booking-site account's free/busy to compute, so a
|
|
4
|
+
* multi-tenant install refreshes every site rather than only the one the server
|
|
5
|
+
* resolves by default (Task 1890). Values are URL-encoded for the query string.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildFreeBusyUrl(port: string, accountId: string, from: string, to: string): string;
|
|
8
|
+
//# sourceMappingURL=free-busy-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"free-busy-url.d.ts","sourceRoot":"","sources":["../../src/lib/free-busy-url.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAMlG"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the device-local free/busy URL the publish dispatcher fetches. The
|
|
3
|
+
* `account` param names which booking-site account's free/busy to compute, so a
|
|
4
|
+
* multi-tenant install refreshes every site rather than only the one the server
|
|
5
|
+
* resolves by default (Task 1890). Values are URL-encoded for the query string.
|
|
6
|
+
*/
|
|
7
|
+
export function buildFreeBusyUrl(port, accountId, from, to) {
|
|
8
|
+
return (`http://127.0.0.1:${port}/api/calendar/free-busy` +
|
|
9
|
+
`?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}` +
|
|
10
|
+
`&account=${encodeURIComponent(accountId)}`);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=free-busy-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"free-busy-url.js","sourceRoot":"","sources":["../../src/lib/free-busy-url.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,SAAiB,EAAE,IAAY,EAAE,EAAU;IACxF,OAAO,CACL,oBAAoB,IAAI,yBAAyB;QACjD,SAAS,kBAAkB,CAAC,IAAI,CAAC,OAAO,kBAAkB,CAAC,EAAE,CAAC,EAAE;QAChE,YAAY,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAC5C,CAAA;AACH,CAAC"}
|
|
@@ -28,6 +28,7 @@ import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync, mkdtemp
|
|
|
28
28
|
import { resolve, join, basename } from "node:path";
|
|
29
29
|
import { homedir, tmpdir } from "node:os";
|
|
30
30
|
import { resolveHouseScopedToken } from "../lib/house-scoped-token.js";
|
|
31
|
+
import { buildFreeBusyUrl } from "../lib/free-busy-url.js";
|
|
31
32
|
function resolveConfigDir() {
|
|
32
33
|
const platformRoot = process.env.PLATFORM_ROOT;
|
|
33
34
|
if (!platformRoot)
|
|
@@ -41,7 +42,14 @@ function resolveConfigDir() {
|
|
|
41
42
|
return brand.configDir;
|
|
42
43
|
}
|
|
43
44
|
const CONFIG_DIR = resolve(homedir(), resolveConfigDir());
|
|
44
|
-
|
|
45
|
+
// Per-account lock (Task 1890): the server spawns one dispatcher per booking-site
|
|
46
|
+
// account each tick, so a single shared lock would make all but one bail
|
|
47
|
+
// result=skipped reason=lock-held. Keyed on the account this run publishes,
|
|
48
|
+
// resolved the same way as `accountId` in main() (env, else the account dir's
|
|
49
|
+
// basename) so a run with only PUBLISH_ACCOUNT_DIR set still locks per account.
|
|
50
|
+
const LOCK_ACCOUNT = process.env.PUBLISH_ACCOUNT_ID ??
|
|
51
|
+
(process.env.PUBLISH_ACCOUNT_DIR ? basename(process.env.PUBLISH_ACCOUNT_DIR) : "default");
|
|
52
|
+
const LOCK_FILE = resolve(CONFIG_DIR, `publish-availability-${LOCK_ACCOUNT}.lock`);
|
|
45
53
|
function acquireLock() {
|
|
46
54
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
47
55
|
if (existsSync(LOCK_FILE)) {
|
|
@@ -182,7 +190,9 @@ async function main() {
|
|
|
182
190
|
const from = utcMidnight(now, 0);
|
|
183
191
|
const to = utcMidnight(now, daysAhead + 1);
|
|
184
192
|
const nowIso = now.toISOString();
|
|
185
|
-
|
|
193
|
+
// Name the account so a multi-tenant install computes this site's free/busy,
|
|
194
|
+
// not whatever the endpoint resolves by default (Task 1890).
|
|
195
|
+
const url = buildFreeBusyUrl(port, accountId, from, to);
|
|
186
196
|
let payload;
|
|
187
197
|
try {
|
|
188
198
|
// Bound the read so a wedged server cannot hold the PID lock.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-availability.js","sourceRoot":"","sources":["../../src/scripts/publish-availability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC9G,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"publish-availability.js","sourceRoot":"","sources":["../../src/scripts/publish-availability.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: storage-broker
|
|
3
3
|
description: Per-account Cloudflare D1/R2 storage broker. The only agent-reachable path to storage; the account-wide credential stays house-only and every operation is scoped to the caller's account.
|
|
4
|
+
account-owned-dirs: [{"dir": "pages", "description": "Cloudflare Pages deploy staging for the account's public sites."}]
|
|
4
5
|
tools:
|
|
5
6
|
- name: storage-d1-list
|
|
6
7
|
publicAllowlist: false
|
|
@@ -73,6 +73,20 @@ ALLOWED_MX="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_MX/SCH
|
|
|
73
73
|
assert_nogrep "quoting" "$ALLOWED_MX" "mx-no-quoting"
|
|
74
74
|
assert_nogrep "Plugin-owned top-level workspaces" "$(cat "$A_MX/SCHEMA.md")" "mx-no-section"
|
|
75
75
|
|
|
76
|
+
# --- platform plugins declare pages (storage-broker) + archive (memory) ---
|
|
77
|
+
# Task 1891: these two plugin-owned root dirs must land in the allowed block so
|
|
78
|
+
# the account-dir reconcile stops quarantining them. Symlink the real plugin
|
|
79
|
+
# dirs so the case exercises the shipped PLUGIN.md declaration.
|
|
80
|
+
T_PA="$(make_tree '[]')"
|
|
81
|
+
ln -s "$REAL_PROJECT_DIR/plugins/storage-broker" "$T_PA/platform/plugins/storage-broker"
|
|
82
|
+
ln -s "$REAL_PROJECT_DIR/plugins/memory" "$T_PA/platform/plugins/memory"
|
|
83
|
+
A_PA="$(make_account "$T_PA")"
|
|
84
|
+
export PROJECT_DIR="$T_PA/platform"
|
|
85
|
+
merge_owned_dirs_into_schema "$A_PA" >/dev/null
|
|
86
|
+
ALLOWED_PA="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_PA/SCHEMA.md")"
|
|
87
|
+
assert_grep "pages" "$ALLOWED_PA" "pa-allowlist-has-pages"
|
|
88
|
+
assert_grep "archive" "$ALLOWED_PA" "pa-allowlist-has-archive"
|
|
89
|
+
|
|
76
90
|
# --- reconcile: present=false on bare schema, present=true after merge ---
|
|
77
91
|
A_RC="$(make_account "$T_SD")"
|
|
78
92
|
export PROJECT_DIR="$T_SD/platform"
|
|
@@ -117,11 +131,11 @@ OUT_C="$(merge_owned_dirs_into_schema "$A_C")"
|
|
|
117
131
|
# Match a non-empty derived list, not the bare key (which prints even for zero).
|
|
118
132
|
assert_grep "domainBuckets=jobs" "$OUT_C" "con-domain-log"
|
|
119
133
|
# Summary reports the full root set and the job-owned entities with their parent.
|
|
120
|
-
assert_grep "roots=jobs,customers,customer-sites,assets,parts,ppm-contracts" "$OUT_C" "con-roots-log"
|
|
134
|
+
assert_grep "roots=jobs,customers,customer-sites,assets,parts,suppliers,ppm-contracts" "$OUT_C" "con-roots-log"
|
|
121
135
|
assert_grep "nested=Visit<-Job,Quote<-Job,PurchaseOrder<-Job" "$OUT_C" "con-nested-log"
|
|
122
136
|
ALLOWED_C="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_C/SCHEMA.md")"
|
|
123
137
|
# Root entities (blank Owner) get a top-level bucket.
|
|
124
|
-
for d in jobs customers customer-sites assets parts ppm-contracts; do
|
|
138
|
+
for d in jobs customers customer-sites assets parts ppm-contracts suppliers; do
|
|
125
139
|
assert_grep "$d" "$ALLOWED_C" "con-allow-$d"
|
|
126
140
|
done
|
|
127
141
|
# Job-owned entities (Owner=Job) do NOT get a root bucket.
|
|
@@ -137,6 +151,10 @@ assert_grep '`Quote` records live under `jobs/` (owned by `Job`), not a root buc
|
|
|
137
151
|
B_C="$(shasum "$A_C/SCHEMA.md" | awk '{print $1}')"
|
|
138
152
|
merge_owned_dirs_into_schema "$A_C" >/dev/null
|
|
139
153
|
assert_eq "$(shasum "$A_C/SCHEMA.md" | awk '{print $1}')" "$B_C" "con-idempotent"
|
|
154
|
+
# Supplier is a root bucket (Task 1892): received invoices file under
|
|
155
|
+
# suppliers/<supplier>/Invoices/, not a stray top-level inbound-invoices/.
|
|
156
|
+
RCS="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" reconcile "$A_C")"
|
|
157
|
+
assert_grep "ontologyEntity=Supplier bucket=suppliers present=true" "$RCS" "con-supplier-root-present"
|
|
140
158
|
|
|
141
159
|
# --- estate-agent vertical: root buckets only, listing-owned entities nested ---
|
|
142
160
|
T_E="$(make_vertical_tree '["real-agent"]' 'schema-estate-agent')"; A_E="$(make_account "$T_E")"
|
|
@@ -151,6 +169,8 @@ for d in viewings offers; do
|
|
|
151
169
|
assert_nogrep "$d" "$ALLOWED_E" "ea-noroot-$d"
|
|
152
170
|
done
|
|
153
171
|
assert_nogrep "jobs" "$ALLOWED_E" "ea-no-sitedesk-jobs"
|
|
172
|
+
# The suppliers/ bucket is construction-only; it must not leak into another vertical.
|
|
173
|
+
assert_nogrep "suppliers" "$ALLOWED_E" "ea-no-suppliers"
|
|
154
174
|
|
|
155
175
|
# --- no vertical declared: generic base only ---
|
|
156
176
|
T_N="$(make_tree '[]')"; A_N="$(make_account "$T_N")"
|
package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"AA2FA,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;+DAE+D;AAC/D,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAE9D;AAED;;8EAE8E;AAC9E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAcpE;AA0FD;;;gFAGgF;AAChF,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,MAAM,GAAG,eAAe,CA6DvF;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;oCAGoC;AACpC,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAwBtD;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;CACb;AAED;oDACoD;AACpD,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,aAAa,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3C,yBAAyB,CAiB3B"}
|
package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js
CHANGED
|
@@ -37,7 +37,17 @@ const QUARANTINE_DIR = '.quarantine';
|
|
|
37
37
|
* an account carrying a stale/older-template/hand-edited schema whose allowed
|
|
38
38
|
* block omits one of these must not have it moved. This is a fail-SAFE belt on
|
|
39
39
|
* the control plane, the mirror of the guard's fail-OPEN on a missing schema —
|
|
40
|
-
* operator strays are still quarantined; the account's own machinery is not.
|
|
40
|
+
* operator strays are still quarantined; the account's own machinery is not.
|
|
41
|
+
*
|
|
42
|
+
* The second group is platform-written control-plane FILES the platform reads
|
|
43
|
+
* and writes at the account root every run. Unlike the tool-owned buckets, no
|
|
44
|
+
* plugin declares them into the SCHEMA.md allowed-top-level block, so without
|
|
45
|
+
* this belt the sweep quarantines them and the feature that reads each one goes
|
|
46
|
+
* dark: calendar-availability.json is the booking page's availability config (a
|
|
47
|
+
* quarantine froze a live booking page, Task 1877 fix); wa/webchat/telegram
|
|
48
|
+
* -channel-bindings.json route each channel's inbound; canonical-webchat-session
|
|
49
|
+
* .json pins the webchat session; session-titles.json holds operator renames.
|
|
50
|
+
* Each is `join(accountDir, <name>)` in platform code, none is operator data. */
|
|
41
51
|
const PROTECTED_TOP_LEVEL = new Set([
|
|
42
52
|
'.git',
|
|
43
53
|
'.claude',
|
|
@@ -45,6 +55,12 @@ const PROTECTED_TOP_LEVEL = new Set([
|
|
|
45
55
|
'SCHEMA.md',
|
|
46
56
|
'account.json',
|
|
47
57
|
'AGENTS.md',
|
|
58
|
+
'calendar-availability.json',
|
|
59
|
+
'wa-channel-bindings.json',
|
|
60
|
+
'webchat-channel-bindings.json',
|
|
61
|
+
'telegram-channel-bindings.json',
|
|
62
|
+
'canonical-webchat-session.json',
|
|
63
|
+
'session-titles.json',
|
|
48
64
|
]);
|
|
49
65
|
/** Buckets whose depth fs-schema-guard.sh checks, with the same 3-segment cap.
|
|
50
66
|
* Every other top-level bucket is tool-owned and unbounded — not walked. This
|
|
@@ -208,7 +224,12 @@ export function reconcileAccount(accountDir, now) {
|
|
|
208
224
|
catch {
|
|
209
225
|
return empty;
|
|
210
226
|
}
|
|
211
|
-
|
|
227
|
+
// A dot-prefixed top-level entry is tool/system state, never operator data —
|
|
228
|
+
// the same judgement countBadNameEntries applies when it skips dotfiles. The
|
|
229
|
+
// stray check must agree, or it quarantines .remember/.superpowers/.wrangler
|
|
230
|
+
// and breaks the tool that owns each (Task 1891). .git/.claude/.quarantine
|
|
231
|
+
// stay covered by PROTECTED_TOP_LEVEL regardless.
|
|
232
|
+
const strays = rootEntries.filter((name) => !name.startsWith('.') && !PROTECTED_TOP_LEVEL.has(name) && !allowed.has(name));
|
|
212
233
|
let quarantined = 0;
|
|
213
234
|
if (strays.length > 0) {
|
|
214
235
|
const quarantineRoot = join(accountDir, QUARANTINE_DIR);
|