@rubytech/create-maxy-code 0.1.575 → 0.1.577
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__/preserve-house-cloudflare.test.js +2 -72
- package/dist/index.js +8 -59
- package/dist/preserve-house-cloudflare.js +2 -49
- package/package.json +1 -1
- package/payload/platform/lib/active-rules/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/active-rules/dist/index.js +32 -1
- package/payload/platform/lib/active-rules/dist/index.js.map +1 -1
- package/payload/platform/lib/active-rules/src/index.test.ts +52 -3
- package/payload/platform/lib/active-rules/src/index.ts +36 -2
- package/payload/platform/lib/dispatch-read/dist/allocate.d.ts +57 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.d.ts.map +1 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.js +33 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.js.map +1 -0
- package/payload/platform/lib/dispatch-read/dist/index.d.ts +2 -0
- package/payload/platform/lib/dispatch-read/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/dispatch-read/dist/index.js +10 -1
- package/payload/platform/lib/dispatch-read/dist/index.js.map +1 -1
- package/payload/platform/lib/dispatch-read/src/__tests__/allocate.test.ts +41 -0
- package/payload/platform/lib/dispatch-read/src/allocate.ts +89 -0
- package/payload/platform/lib/dispatch-read/src/index.ts +14 -0
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-compliance.test.sh +7 -2
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-compliance.sh +8 -2
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +333 -22
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visits-scope.test.ts +262 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +126 -21
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +142 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visits.ts +118 -6
- package/payload/platform/plugins/dispatch/PLUGIN.md +2 -0
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts +19 -50
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts.map +1 -1
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js +1 -28
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js.map +1 -1
- package/payload/platform/plugins/dispatch/skills/dispatch-page/SKILL.md +45 -6
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.css +60 -0
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.html +13 -0
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.js +203 -5
- package/payload/platform/plugins/docs/references/internals.md +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js +19 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js +110 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js +22 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.js +19 -163
- package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts +9 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js +11 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts +30 -3
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js +29 -4
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts +33 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js +145 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js.map +1 -0
- package/payload/platform/plugins/telegram/skills/configure/SKILL.md +4 -2
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +5 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +29 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/notification.d.ts +35 -3
- package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/notification.js +38 -5
- package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/server.js +1 -1
- package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +2 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +2 -2
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/server/server.js +670 -155
- package/dist/__tests__/converge-client-admins.test.js +0 -50
|
@@ -114,20 +114,133 @@
|
|
|
114
114
|
column order above. */
|
|
115
115
|
var HOUR_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
|
|
116
116
|
|
|
117
|
-
/*
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
/* Whether the account declared this move (Task 2511).
|
|
118
|
+
|
|
119
|
+
Absent from the published list means refused, and the list travels with the
|
|
120
|
+
week, so a drag is refused with the install offline. `visit-status.ts`
|
|
121
|
+
refuses it a second time from the same list; that refusal is the binding
|
|
122
|
+
one, and this one exists so the operator is told now rather than in sixty
|
|
123
|
+
seconds. An unchanged status is NOT a move, which is the same rule the door
|
|
124
|
+
applies: `[from, to]` declares a move and no account declares a
|
|
125
|
+
self-transition, so without this every pure re-timing would be refused. */
|
|
126
|
+
function declared(meta, from, to) {
|
|
127
|
+
if (from === to) return true;
|
|
128
|
+
var list = (meta && meta.transitions) || [];
|
|
129
|
+
for (var i = 0; i < list.length; i++) {
|
|
130
|
+
if (list[i][0] === from && list[i][1] === to) return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* The same wall-clock time on a different day, IN THE ACCOUNT'S ZONE.
|
|
136
|
+
|
|
137
|
+
Built by asking the account's own formatter what instant renders as the
|
|
138
|
+
wanted wall time, never from the browser's offset: a page that guesses the
|
|
139
|
+
zone re-times a visit to a plausible wrong hour, which is the Task 2280
|
|
140
|
+
defect. Two passes, because the offset either side of a daylight-saving
|
|
141
|
+
transition differs and the first correction can land on the far side of it.
|
|
142
|
+
|
|
143
|
+
Returns the shape the door admits, which is what `toISOString` produces. */
|
|
144
|
+
function retimed(iso, dayKey, zone) {
|
|
145
|
+
var wall = new Intl.DateTimeFormat('en-GB', {
|
|
146
|
+
timeZone: zone,
|
|
147
|
+
hour: '2-digit',
|
|
148
|
+
minute: '2-digit',
|
|
149
|
+
hour12: false,
|
|
150
|
+
});
|
|
151
|
+
var held = wall.format(new Date(iso));
|
|
152
|
+
var guess = new Date(dayKey + 'T' + held + ':00.000Z');
|
|
153
|
+
for (var i = 0; i < 2; i++) {
|
|
154
|
+
var seen = wall.format(guess);
|
|
155
|
+
if (seen === held) break;
|
|
156
|
+
var drift =
|
|
157
|
+
(Number(held.slice(0, 2)) - Number(seen.slice(0, 2))) * 60 +
|
|
158
|
+
(Number(held.slice(3)) - Number(seen.slice(3)));
|
|
159
|
+
guess = new Date(guess.getTime() + drift * 60000);
|
|
160
|
+
}
|
|
161
|
+
return guess.toISOString();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* One visit. Clicking moves it to the next declared status; dragging re-times
|
|
165
|
+
or re-assigns it. Both post through the portal's existing write door, and
|
|
166
|
+
both are refused here first against the published transition list. */
|
|
167
|
+
function visitButton(v, data, meta, zone) {
|
|
121
168
|
var b = document.createElement('button');
|
|
122
169
|
b.type = 'button';
|
|
123
170
|
b.className = 'pl-visit';
|
|
171
|
+
b.draggable = true;
|
|
124
172
|
b.textContent = [timeOf(v.startDate, zone), v.status, v.purpose].filter(Boolean).join(' · ');
|
|
125
173
|
b.addEventListener('click', function () {
|
|
126
174
|
advance(v, meta);
|
|
127
175
|
});
|
|
176
|
+
/* The panel is a second gesture on the same card, so the click keeps
|
|
177
|
+
meaning what it meant before this task. */
|
|
178
|
+
b.addEventListener('contextmenu', function (e) {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
openVisit(v, data, meta, zone);
|
|
181
|
+
});
|
|
182
|
+
b.addEventListener('dragstart', function (e) {
|
|
183
|
+
dragging = v;
|
|
184
|
+
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
|
|
185
|
+
});
|
|
186
|
+
b.addEventListener('dragend', function () {
|
|
187
|
+
dragging = null;
|
|
188
|
+
});
|
|
128
189
|
return b;
|
|
129
190
|
}
|
|
130
191
|
|
|
192
|
+
/* The visit under the pointer during a drag. Held here rather than in
|
|
193
|
+
dataTransfer, which only carries strings and would mean looking the visit
|
|
194
|
+
back up by id on every drop. */
|
|
195
|
+
var dragging = null;
|
|
196
|
+
|
|
197
|
+
function move(v, meta, zone, toOwnerId, toDayKey) {
|
|
198
|
+
var body = { visitId: v.visitId, status: v.status };
|
|
199
|
+
if (toOwnerId !== v.ownerId) body.assignedOwnerId = toOwnerId;
|
|
200
|
+
if (toDayKey !== dayOf(v.startDate, zone)) {
|
|
201
|
+
body.startDate = retimed(v.startDate, toDayKey, zone);
|
|
202
|
+
}
|
|
203
|
+
/* A drop that changes neither is not a write. */
|
|
204
|
+
if (body.assignedOwnerId === undefined && body.startDate === undefined) return;
|
|
205
|
+
say('Saving…');
|
|
206
|
+
api('/api/visit-status', {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
headers: { 'content-type': 'application/json' },
|
|
209
|
+
body: JSON.stringify(body),
|
|
210
|
+
})
|
|
211
|
+
.then(function (r) {
|
|
212
|
+
return r.json().then(function (d) {
|
|
213
|
+
return { s: r.status, d: d };
|
|
214
|
+
});
|
|
215
|
+
})
|
|
216
|
+
.then(function (res) {
|
|
217
|
+
if (!res.d.ok) {
|
|
218
|
+
console.error('[planner] move refused', { status: res.s, error: res.d.error });
|
|
219
|
+
say(
|
|
220
|
+
res.s === 404
|
|
221
|
+
? 'That is not yours to change.'
|
|
222
|
+
: res.d.error === 'illegal-transition'
|
|
223
|
+
? 'That move is not one this account allows.'
|
|
224
|
+
: 'That change was refused.',
|
|
225
|
+
);
|
|
226
|
+
/* The card returns to origin by redrawing what the device last
|
|
227
|
+
published. Leaving it where it fell would be the page telling the
|
|
228
|
+
operator something happened that did not. */
|
|
229
|
+
if (latest) render(latest);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (body.assignedOwnerId !== undefined) v.ownerId = body.assignedOwnerId;
|
|
233
|
+
if (body.startDate !== undefined) v.startDate = body.startDate;
|
|
234
|
+
say('Saved. It reaches the office within a minute.');
|
|
235
|
+
if (latest) render(latest);
|
|
236
|
+
})
|
|
237
|
+
.catch(function (err) {
|
|
238
|
+
console.error('[planner] move failed', { status: 'network', error: err });
|
|
239
|
+
say('Could not reach the server.');
|
|
240
|
+
if (latest) render(latest);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
131
244
|
function advance(v, meta) {
|
|
132
245
|
var names = ((meta && meta.statuses) || []).map(function (s) {
|
|
133
246
|
return s.name;
|
|
@@ -137,6 +250,10 @@
|
|
|
137
250
|
return;
|
|
138
251
|
}
|
|
139
252
|
var next = names[(names.indexOf(v.status) + 1) % names.length];
|
|
253
|
+
if (!declared(meta, v.status, next)) {
|
|
254
|
+
say('That move is not one this account allows.');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
140
257
|
say('Saving…');
|
|
141
258
|
api('/api/visit-status', {
|
|
142
259
|
method: 'POST',
|
|
@@ -169,6 +286,60 @@
|
|
|
169
286
|
});
|
|
170
287
|
}
|
|
171
288
|
|
|
289
|
+
/* Who may be sent to this record, why the others may not, and what has been
|
|
290
|
+
recorded against it.
|
|
291
|
+
|
|
292
|
+
Rendered from what /api/visits returned and nothing else. The install
|
|
293
|
+
computed the offers and the exclusions with the one implementation of
|
|
294
|
+
qualification and clash; re-deciding any of it here would be a second rule
|
|
295
|
+
to get wrong, which is what `functions/api/visits.ts` rules out. A
|
|
296
|
+
candidate carries no travel estimate today and is rendered without one
|
|
297
|
+
rather than dropped. */
|
|
298
|
+
function openVisit(v, data, meta, zone) {
|
|
299
|
+
var mine = (data.candidates || []).filter(function (c) {
|
|
300
|
+
return c.visitId === v.visitId;
|
|
301
|
+
});
|
|
302
|
+
document.getElementById('pl-allocate-head').textContent =
|
|
303
|
+
[timeOf(v.startDate, zone), v.status, v.purpose].filter(Boolean).join(' · ');
|
|
304
|
+
|
|
305
|
+
var list = document.getElementById('pl-allocate-list');
|
|
306
|
+
list.replaceChildren();
|
|
307
|
+
if (!mine.length) {
|
|
308
|
+
/* Never an empty box. Nobody offered and nobody excluded is a different
|
|
309
|
+
fact from an unstaffable record, and the operator cannot tell them
|
|
310
|
+
apart from a blank panel. */
|
|
311
|
+
var none = cell('pl-candidate', 'Nobody has been offered for this, and nobody was excluded.');
|
|
312
|
+
none.setAttribute('data-kind', 'excluded');
|
|
313
|
+
list.appendChild(none);
|
|
314
|
+
}
|
|
315
|
+
mine.forEach(function (c) {
|
|
316
|
+
var who = c.name || c.workerId;
|
|
317
|
+
var note =
|
|
318
|
+
c.kind === 'excluded'
|
|
319
|
+
? c.reason
|
|
320
|
+
: (c.conflicts || []).length
|
|
321
|
+
? (c.conflicts || []).length + ' already booked in this window'
|
|
322
|
+
: 'free';
|
|
323
|
+
var li = document.createElement('li');
|
|
324
|
+
li.className = 'pl-candidate';
|
|
325
|
+
li.setAttribute('data-kind', c.kind);
|
|
326
|
+
li.textContent = who + ' · ' + note;
|
|
327
|
+
list.appendChild(li);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
/* What the worker recorded, as counts. The contents themselves are files,
|
|
331
|
+
and the portal's own file exchange already carries them. */
|
|
332
|
+
document.getElementById('pl-record').textContent = [
|
|
333
|
+
'notes ' + (v.notes || 0),
|
|
334
|
+
'media ' + (v.media || 0),
|
|
335
|
+
'parts ' + (v.parts || 0),
|
|
336
|
+
'checklist ' + (v.checklistDone || 0) + '/' + (v.checklistTotal || 0),
|
|
337
|
+
'signatures ' + (v.signatures || 0),
|
|
338
|
+
].join(' · ');
|
|
339
|
+
|
|
340
|
+
document.getElementById('pl-allocate').hidden = false;
|
|
341
|
+
}
|
|
342
|
+
|
|
172
343
|
function render(data) {
|
|
173
344
|
var meta = data.meta;
|
|
174
345
|
/* UTC when the account published no config. Never the browser's: a page
|
|
@@ -225,12 +396,35 @@
|
|
|
225
396
|
'data-off',
|
|
226
397
|
w.workingHours && w.workingHours[HOUR_KEYS[i]] ? 'false' : 'true',
|
|
227
398
|
);
|
|
399
|
+
/* Every cell is a drop target, including a day the worker does not
|
|
400
|
+
work: the office books into one deliberately, and a cell that
|
|
401
|
+
refused the drop would make that decision for them. A row with no
|
|
402
|
+
configured worker is not a target, because there is no ownerId to
|
|
403
|
+
re-assign to. */
|
|
404
|
+
if (w.workerId) {
|
|
405
|
+
c.addEventListener('dragover', function (e) {
|
|
406
|
+
if (!dragging) return;
|
|
407
|
+
e.preventDefault();
|
|
408
|
+
c.setAttribute('data-over', 'true');
|
|
409
|
+
});
|
|
410
|
+
c.addEventListener('dragleave', function () {
|
|
411
|
+
c.removeAttribute('data-over');
|
|
412
|
+
});
|
|
413
|
+
c.addEventListener('drop', function (e) {
|
|
414
|
+
e.preventDefault();
|
|
415
|
+
c.removeAttribute('data-over');
|
|
416
|
+
if (!dragging) return;
|
|
417
|
+
var v = dragging;
|
|
418
|
+
dragging = null;
|
|
419
|
+
move(v, meta, zone, w.ownerId, k);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
228
422
|
(byOwner[w.ownerId] || [])
|
|
229
423
|
.filter(function (v) {
|
|
230
424
|
return dayOf(v.startDate, zone) === k;
|
|
231
425
|
})
|
|
232
426
|
.forEach(function (v) {
|
|
233
|
-
c.appendChild(visitButton(v, meta, zone));
|
|
427
|
+
c.appendChild(visitButton(v, data, meta, zone));
|
|
234
428
|
});
|
|
235
429
|
gridEl.appendChild(c);
|
|
236
430
|
});
|
|
@@ -316,6 +510,10 @@
|
|
|
316
510
|
});
|
|
317
511
|
});
|
|
318
512
|
|
|
513
|
+
document.getElementById('pl-allocate-close').addEventListener('click', function () {
|
|
514
|
+
document.getElementById('pl-allocate').hidden = true;
|
|
515
|
+
});
|
|
516
|
+
|
|
319
517
|
document.getElementById('pl-prev').addEventListener('click', function () {
|
|
320
518
|
anchor.setUTCDate(anchor.getUTCDate() - 7);
|
|
321
519
|
if (latest) render(latest);
|
|
@@ -406,7 +406,7 @@ Standing rules the operator gives over a channel are `Preference` nodes. Two mec
|
|
|
406
406
|
|
|
407
407
|
**Reconcile by meaning on write (`profile-update`).** The existence check is two-tier. First an exact `(category, key)` match — the classic fast path, unchanged. When that misses, a semantic-neighbour lookup runs over the `preference_embedding` vector index, scoped to the same `accountId` and `scope:'admin'`, non-trashed. If the nearest neighbour is at or above `SAME_SUBJECT_COSINE` (0.85), the write lands on that node through the existing reinforce/update/contradict curve instead of minting a duplicate; the matched neighbours are returned in the tool result so the agent can classify future writes against real prior state. The confidence constants are unchanged — only the join key widens, so a rephrasing under a new `key` no longer forks the rule and a correction demotes the prior node rather than adding a contradictory one.
|
|
408
408
|
|
|
409
|
-
**Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: the account owner's admin-scope preferences (resolve the seeded `AdminUser {role:'owner'}`, then select through that owner's `UserProfile` via `HAS_PREFERENCE`), a floor-filtered (`confidence >= 0.4`), confidence-ordered query that returns every such preference verbatim — no cap, no de-confliction, no candidate limit. Concision is enforced at write time by the `conversational-memory` skill (each stored value is a single minimal imperative) and by the write-time semantic reconcile (a same-subject write at or above `SAME_SUBJECT_COSINE` folds into the existing node rather than minting a duplicate), so the injected block stays small because each stored preference is short and non-redundant, not because injection truncates. A preference owned by a non-owner profile (an operator's, stranded on the operator's own profile while managing the account) is never injected; an account with no seeded owner injects nothing. `profile-read` exposes this same owner-scoped set as `activeRules`, and the three inbound gateways (`wa-gateway`, `telegram-gateway`, `webchat-gateway`) fetch it for the effective session account and attach it as `standingRules` on the inbound frame; the channel builders prepend it as a `## Standing rules` block above `## Context
|
|
409
|
+
**Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: the account owner's admin-scope preferences (resolve the seeded `AdminUser {role:'owner'}`, then select through that owner's `UserProfile` via `HAS_PREFERENCE`), a floor-filtered (`confidence >= 0.4`), confidence-ordered query that returns every such preference verbatim — no cap, no de-confliction, no candidate limit. Concision is enforced at write time by the `conversational-memory` skill (each stored value is a single minimal imperative) and by the write-time semantic reconcile (a same-subject write at or above `SAME_SUBJECT_COSINE` folds into the existing node rather than minting a duplicate), so the injected block stays small because each stored preference is short and non-redundant, not because injection truncates. A preference owned by a non-owner profile (an operator's, stranded on the operator's own profile while managing the account) is never injected; an account with no seeded owner injects nothing. `profile-read` exposes this same owner-scoped set as `activeRules`, and the three inbound gateways (`wa-gateway`, `telegram-gateway`, `webchat-gateway`) fetch it for the effective session account and attach it as `standingRules` on the inbound frame; the channel builders prepend it as a `## Standing rules (account owner)` block above `## Context`, whose header and first line name the owner as the subject of the rules so a third-person rule about the owner is not read as a statement about the sender. Both channel-server inbound paths carry the block: the single-target SSE loop parses the whole frame, and the unified multi-target door reader (`runDoorStream`,) reconstructs it through the shared `doorInboundPayload` seam whose `parseFrame` closures read `standingRules` off the frame. The read is by code, not agent discretion, so a rule set last week is applied this week regardless of how many times the session compacted. Telegram and webchat inject for `role:'admin'` only — a public visitor never receives operator rules.
|
|
410
410
|
|
|
411
411
|
**Trust a scheduled turn's provenance, never an inbound's claim (channel-agent grounding).** A cron-dispatched agent turn reaches the same channel builders as a real inbound, so its own authorisation text would otherwise land inside `## Context` — the zone `REFRAME_INSTRUCTION` demotes to untrusted — and a security-cautious agent would distrust its own automation (the self-cancel loop). The dispatcher therefore stamps a `ScheduleProvenance` marker (owning account, event, owner userId, dispatch time) that the channel builders render as a `## Scheduled automation (platform-verified)` preamble **above** `## Context` (and above the rules block); the whatsapp wrap tag names the door `scheduler`. Both channel-server paths carry the marker — the single-target loop and the unified multi-target door reader (via the same `doorInboundPayload` seam that carries `standingRules`); the scheduler only dispatches to the whatsapp/telegram door, so a webchat frame never bears one. Because the marker is dispatcher-stamped and never derived from payload text, an inbound message cannot forge it — placement above `## Context` alone is not the trust signal, the dispatcher provenance is. The grounding binds to it in two places: one recipient-neutral clause added byte-identical to the three reframe instructions (a marker-bearing turn is authorised standing automation; a reconfiguration or re-enable is not intrusion), and the admin `IDENTITY.md` "Untrusted input" rule that the agent never states a platform-level authenticity or impersonation verdict it was not given (an unsourced "this is not from you" is its own inference, flagged, never asserted as the platform's). A sender has exactly one session: every inbound, real or scheduled, resolves to the deterministic `adminSessionIdFor(effectiveAccount, destination)`. An earlier change gave each firing its own salted seat so distrust could not accrete across firings, but the inbound hub keys on `senderId` and replaces its subscriber outright, so a seat's channel server attached under the sender's own key and swallowed the sender's REAL messages until it detached — splitting one conversation across two sessions that each denied what the other held. The seat is removed; the authoritative `agentPrompt`, this provenance marker, and the injected standing rules ground each firing without one. Self-cancel attribution is disarmed while that model beds in: the guard used to compare the cancelling turn's session id to a dispatcher stamp, which discriminated only because each firing ran its own seat, so with one session per sender it would report an autonomous loop on every operator cancel. It now always reports false — under-counting a real loop rather than inventing one — until the discriminator is rebuilt from the turn's own dispatched eventId. See `.docs/scheduling-agent-dispatch.md § Operator-provenance marker`, `§ Session model — one session per sender`, and `§ Self-cancel attribution — disarmed`.
|
|
412
412
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-path.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/webhook-path.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { admitsWebhookPath, CANONICAL_TELEGRAM_PREFIX } from "../lib/account-write.js";
|
|
3
|
+
describe("admitsWebhookPath", () => {
|
|
4
|
+
it("admits the one path the router serves and the gate exempts", () => {
|
|
5
|
+
expect(admitsWebhookPath("/api/telegram/")).toBe(true);
|
|
6
|
+
expect(CANONICAL_TELEGRAM_PREFIX).toBe("/api/telegram/");
|
|
7
|
+
});
|
|
8
|
+
it("refuses a subpath — telegram-mount.ts leaves every one of them on 404", () => {
|
|
9
|
+
expect(admitsWebhookPath("/api/telegram/webhook")).toBe(false);
|
|
10
|
+
expect(admitsWebhookPath("/api/telegram/8861756902")).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
it("refuses the no-slash shape — the router serves it but isRemoteAuthBypassPath 401s it", () => {
|
|
13
|
+
expect(admitsWebhookPath("/api/telegram")).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
it("refuses the official plugin's path", () => {
|
|
16
|
+
expect(admitsWebhookPath("/channels/telegram")).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
//# sourceMappingURL=webhook-path.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-path.test.js","sourceRoot":"","sources":["../../src/__tests__/webhook-path.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAEvF,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvD,MAAM,CAAC,yBAAyB,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,CAAC,iBAAiB,CAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/D,MAAM,CAAC,iBAAiB,CAAC,0BAA0B,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sFAAsF,EAAE,GAAG,EAAE;QAC9F,MAAM,CAAC,iBAAiB,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,iBAAiB,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-register.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/webhook-register.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { webhookRegister } from "../tools/webhook-register.js";
|
|
6
|
+
const STAMP = 1_754_600_000;
|
|
7
|
+
const TOKEN = "8861756902:AAAsecret";
|
|
8
|
+
const URL_OK = "https://sitedesk.example/api/telegram/";
|
|
9
|
+
/** account.json lives at <platformRoot>/../data/accounts/<id>/account.json. */
|
|
10
|
+
function stubAccount() {
|
|
11
|
+
const root = mkdtempSync(join(tmpdir(), "tg-reg-"));
|
|
12
|
+
const platformRoot = join(root, "platform");
|
|
13
|
+
mkdirSync(platformRoot, { recursive: true });
|
|
14
|
+
const dir = resolve(root, "data", "accounts", "acct-1");
|
|
15
|
+
mkdirSync(dir, { recursive: true });
|
|
16
|
+
const configPath = join(dir, "account.json");
|
|
17
|
+
writeFileSync(configPath, JSON.stringify({ telegram: { bots: [] } }));
|
|
18
|
+
return { platformRoot, configPath };
|
|
19
|
+
}
|
|
20
|
+
function botsIn(configPath) {
|
|
21
|
+
return JSON.parse(readFileSync(configPath, "utf-8")).telegram.bots;
|
|
22
|
+
}
|
|
23
|
+
/** getMe ok, setWebhook ok, getWebhookInfo returns whatever `info` says. */
|
|
24
|
+
function stubFetch(info) {
|
|
25
|
+
return (async (url) => ({
|
|
26
|
+
json: async () => {
|
|
27
|
+
if (String(url).endsWith("/getMe"))
|
|
28
|
+
return { ok: true, result: { username: "sitedeskbot" } };
|
|
29
|
+
if (String(url).endsWith("/setWebhook"))
|
|
30
|
+
return { ok: true };
|
|
31
|
+
return { result: { url: URL_OK, pending_update_count: 0, ...info } };
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
function deps(platformRoot, lines, fetchImpl) {
|
|
36
|
+
const secretDir = mkdtempSync(join(tmpdir(), "tg-sec-"));
|
|
37
|
+
return {
|
|
38
|
+
platformRoot,
|
|
39
|
+
accountId: "acct-1",
|
|
40
|
+
log: (l) => lines.push(l),
|
|
41
|
+
fetchImpl,
|
|
42
|
+
nowSec: () => STAMP,
|
|
43
|
+
secretPathFor: (botId) => join(secretDir, botId),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
describe("webhookRegister", () => {
|
|
47
|
+
it("refuses a subpath, names the one path that works, and writes nothing", async () => {
|
|
48
|
+
const { platformRoot, configPath } = stubAccount();
|
|
49
|
+
const lines = [];
|
|
50
|
+
const out = await webhookRegister({ botToken: TOKEN, webhookUrl: "https://sitedesk.example/api/telegram/webhook", role: "admin" }, deps(platformRoot, lines, stubFetch({})));
|
|
51
|
+
expect(out.isError).toBe(true);
|
|
52
|
+
expect(out.text).toContain("/api/telegram/");
|
|
53
|
+
expect(botsIn(configPath)).toEqual([]);
|
|
54
|
+
expect(lines.join("\n")).toContain("op=webhook-register path=/api/telegram/webhook");
|
|
55
|
+
expect(lines.join("\n")).toContain("admitted=false");
|
|
56
|
+
});
|
|
57
|
+
it("fails the registration on a fresh delivery error and persists no bot entry", async () => {
|
|
58
|
+
const { platformRoot, configPath } = stubAccount();
|
|
59
|
+
const lines = [];
|
|
60
|
+
const out = await webhookRegister({ botToken: TOKEN, webhookUrl: URL_OK, role: "admin" }, deps(platformRoot, lines, stubFetch({
|
|
61
|
+
last_error_date: STAMP + 3,
|
|
62
|
+
last_error_message: "Wrong response from the webhook: 404 Not Found",
|
|
63
|
+
})));
|
|
64
|
+
expect(out.isError).toBe(true);
|
|
65
|
+
expect(out.text).toContain("Wrong response from the webhook: 404 Not Found");
|
|
66
|
+
expect(botsIn(configPath)).toEqual([]);
|
|
67
|
+
expect(lines.join("\n")).toContain("op=webhook-verify");
|
|
68
|
+
expect(lines.join("\n")).toContain("reachable=false");
|
|
69
|
+
});
|
|
70
|
+
it("passes an error Telegram kept from the previous URL", async () => {
|
|
71
|
+
const { platformRoot, configPath } = stubAccount();
|
|
72
|
+
const lines = [];
|
|
73
|
+
const out = await webhookRegister({ botToken: TOKEN, webhookUrl: URL_OK, role: "admin" }, deps(platformRoot, lines, stubFetch({
|
|
74
|
+
last_error_date: STAMP - 60,
|
|
75
|
+
last_error_message: "Wrong response from the webhook: 404 Not Found",
|
|
76
|
+
pending_update_count: 83,
|
|
77
|
+
})));
|
|
78
|
+
expect(out.isError).toBe(false);
|
|
79
|
+
expect(botsIn(configPath)).toHaveLength(1);
|
|
80
|
+
expect(lines.join("\n")).toContain("pending=83");
|
|
81
|
+
});
|
|
82
|
+
it("persists the bot and logs the verify line on a clean probe", async () => {
|
|
83
|
+
const { platformRoot, configPath } = stubAccount();
|
|
84
|
+
const lines = [];
|
|
85
|
+
const out = await webhookRegister({ botToken: TOKEN, webhookUrl: URL_OK, role: "admin", adminUsers: [42] }, deps(platformRoot, lines, stubFetch({})));
|
|
86
|
+
expect(out.isError).toBe(false);
|
|
87
|
+
expect(botsIn(configPath)).toEqual([
|
|
88
|
+
{ id: "8861756902", token: TOKEN, role: "admin", adminUsers: [42] },
|
|
89
|
+
]);
|
|
90
|
+
expect(lines.join("\n")).toContain("op=webhook-verify path=/api/telegram/ botId=8861756902");
|
|
91
|
+
expect(lines.join("\n")).toContain("pending=0");
|
|
92
|
+
});
|
|
93
|
+
it("keeps the registration when the probe itself throws", async () => {
|
|
94
|
+
const { platformRoot, configPath } = stubAccount();
|
|
95
|
+
const lines = [];
|
|
96
|
+
const throwingProbe = (async (url) => {
|
|
97
|
+
if (String(url).endsWith("/getWebhookInfo"))
|
|
98
|
+
throw new Error("socket hang up");
|
|
99
|
+
return {
|
|
100
|
+
json: async () => String(url).endsWith("/getMe") ? { ok: true, result: { username: "b" } } : { ok: true },
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
const out = await webhookRegister({ botToken: TOKEN, webhookUrl: URL_OK, role: "admin" }, deps(platformRoot, lines, throwingProbe));
|
|
104
|
+
expect(out.isError).toBe(false);
|
|
105
|
+
expect(botsIn(configPath)).toHaveLength(1);
|
|
106
|
+
expect(lines.join("\n")).toContain("op=webhook-verify path=/api/telegram/ botId=8861756902 probe=failed");
|
|
107
|
+
expect(lines.join("\n")).toContain("reason=socket hang up");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
//# sourceMappingURL=webhook-register.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-register.test.js","sourceRoot":"","sources":["../../src/__tests__/webhook-register.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-verdict.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/webhook-verdict.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { webhookVerdict } from "../lib/telegram.js";
|
|
3
|
+
const STAMP = 1_754_600_000;
|
|
4
|
+
const clean = { url: "https://x/api/telegram/?bot=1", pendingUpdateCount: 0, lastErrorDate: 0 };
|
|
5
|
+
describe("webhookVerdict", () => {
|
|
6
|
+
it("fails on an error Telegram saw at or after the registration stamp", () => {
|
|
7
|
+
expect(webhookVerdict({ ...clean, lastErrorDate: STAMP, lastErrorMessage: "Wrong response from the webhook: 404 Not Found" }, STAMP)).toEqual({ reachable: false, reason: "Wrong response from the webhook: 404 Not Found" });
|
|
8
|
+
});
|
|
9
|
+
it("passes an error dated before the stamp — it belongs to the previous URL", () => {
|
|
10
|
+
expect(webhookVerdict({ ...clean, lastErrorDate: STAMP - 1, lastErrorMessage: "Wrong response from the webhook: 404 Not Found" }, STAMP)).toEqual({ reachable: true, reason: "" });
|
|
11
|
+
});
|
|
12
|
+
it("passes a backlog with no error — pendingUpdateCount never gates", () => {
|
|
13
|
+
expect(webhookVerdict({ ...clean, pendingUpdateCount: 83 }, STAMP)).toEqual({
|
|
14
|
+
reachable: true,
|
|
15
|
+
reason: "",
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
it("passes a clean probe", () => {
|
|
19
|
+
expect(webhookVerdict(clean, STAMP)).toEqual({ reachable: true, reason: "" });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=webhook-verdict.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-verdict.test.js","sourceRoot":"","sources":["../../src/__tests__/webhook-verdict.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,KAAK,GAAG,aAAa,CAAC;AAE5B,MAAM,KAAK,GAAG,EAAE,GAAG,EAAE,+BAA+B,EAAE,kBAAkB,EAAE,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC;AAEhG,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,mEAAmE,EAAE,GAAG,EAAE;QAC3E,MAAM,CACJ,cAAc,CACZ,EAAE,GAAG,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,gBAAgB,EAAE,gDAAgD,EAAE,EACtG,KAAK,CACN,CACF,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,gDAAgD,EAAE,CAAC,CAAC;IAC5F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yEAAyE,EAAE,GAAG,EAAE;QACjF,MAAM,CACJ,cAAc,CACZ,EAAE,GAAG,KAAK,EAAE,aAAa,EAAE,KAAK,GAAG,CAAC,EAAE,gBAAgB,EAAE,gDAAgD,EAAE,EAC1G,KAAK,CACN,CACF,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,CAAC,cAAc,CAAC,EAAE,GAAG,KAAK,EAAE,kBAAkB,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC;YAC1E,SAAS,EAAE,IAAI;YACf,MAAM,EAAE,EAAE;SACX,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|