@rubytech/create-sitedesk-code 0.1.512 → 0.1.513
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/docs/superpowers/plans/2026-07-27-task-2028-declared-file-write-deny.md +303 -0
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md +126 -0
- package/payload/platform/lib/account-schema-regions/dist/index.d.ts +5 -0
- package/payload/platform/lib/account-schema-regions/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/account-schema-regions/dist/index.js +5 -0
- package/payload/platform/lib/account-schema-regions/dist/index.js.map +1 -1
- package/payload/platform/lib/account-schema-regions/src/index.ts +5 -0
- package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh +14 -0
- package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh +38 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh +11 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard.sh +25 -1
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
- package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
- package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +1 -1
- package/payload/platform/plugins/scheduling/PLUGIN.md +1 -1
- package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +26 -0
- package/payload/platform/scripts/lib/account-schema-owned-dirs.py +40 -4
- package/payload/platform/scripts/logs-read.sh +47 -24
- package/payload/platform/scripts/logs-read.test.sh +73 -31
- package/payload/platform/scripts/provision-worktree.sh +80 -0
- package/payload/server/{chunk-2WAXM5N2.js → chunk-S6HYTIU3.js} +2 -3
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/server.js +89 -41
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cloudflare
|
|
3
3
|
description: Cloudflare operations — tunnel setup/reset, DNS, Pages hosting, D1 data capture, and dashboard guidance. Zero agent-facing MCP tools; every operation is the agent invoking `cloudflared` or `wrangler` directly via Bash, calling the Cloudflare API with a reused per-scope narrow token, or quoting a dashboard click-path the operator performs themselves.
|
|
4
|
-
account-owned-files: [{"file": "data-portal.json", "description": "The account's
|
|
4
|
+
account-owned-files: [{"file": "data-portal.json", "writtenBy": "agent", "description": "The account's portal config and folder index. Authored by the agent under the data-portal skill, which is also the only path that changes it; bin/portal-index-push.mjs reads it on every push."}]
|
|
5
5
|
tools: []
|
|
6
6
|
mcp-manifest: skip
|
|
7
7
|
---
|
|
@@ -78,7 +78,7 @@ export function resolveExposedDirs(schemaText, exposeFolders = []) {
|
|
|
78
78
|
collisions: /** @type {string[]} */ ([]),
|
|
79
79
|
exposed: /** @type {string[]} */ ([]),
|
|
80
80
|
}
|
|
81
|
-
// Fail CLOSED. fs-schema-guard.sh:
|
|
81
|
+
// Fail CLOSED. fs-schema-guard.sh:77-80 fails open on a missing schema so an
|
|
82
82
|
// unseeded legacy account is not write-blocked; a read surface facing a
|
|
83
83
|
// client must do the opposite. Do not "fix" this to match the guard.
|
|
84
84
|
if (typeof schemaText !== 'string' || schemaText.length === 0) return empty
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: scheduling
|
|
3
3
|
description: "Calendar and scheduling — create events, manage appointments, set up recurring triggers."
|
|
4
|
-
account-owned-files: [{"file": "calendar-availability.json", "description": "The booking page's availability config
|
|
4
|
+
account-owned-files: [{"file": "calendar-availability.json", "writtenBy": "agent", "description": "The booking page's availability config. Authored by the agent under the calendar-site skill, which is also the only path that changes it; the publish-availability script reads it to refresh the live booking page."}]
|
|
5
5
|
tools:
|
|
6
6
|
- name: schedule-event
|
|
7
7
|
publicAllowlist: false
|
|
@@ -395,5 +395,31 @@ for _n in e-sign data-portal.json calendar-availability.json wa-channel-bindings
|
|
|
395
395
|
assert_grep "$_n" "$ALLOWED_SHIP" "ship-allow-$_n"
|
|
396
396
|
done
|
|
397
397
|
|
|
398
|
+
# --- declared-files fence: one name<TAB>plugin line per declared FILE -------
|
|
399
|
+
# The allowed-top-level fence is a layout list: it says a name may exist at the
|
|
400
|
+
# account root, not that an agent may author it. The write guard needs the file
|
|
401
|
+
# set apart from the bucket set, plus the owning plugin to name in its block
|
|
402
|
+
# message, so the merge emits it as its own fence.
|
|
403
|
+
DECL_SHIP="$(awk '/^```declared-files$/{f=1;next} /^```$/{f=0} f' "$A_SHIP/SCHEMA.md")"
|
|
404
|
+
for _n in wa-channel-bindings.json telegram-channel-bindings.json webchat-channel-bindings.json \
|
|
405
|
+
canonical-webchat-session.json session-titles.json agents-disabled.json; do
|
|
406
|
+
assert_grep "$(printf '%s\tadmin' "$_n")" "$DECL_SHIP" "ship-decl-$_n"
|
|
407
|
+
done
|
|
408
|
+
# An agent-written declaration (writtenBy: agent) stays OUT of the fence: the
|
|
409
|
+
# calendar-site and data-portal skills author these two files with Write, so
|
|
410
|
+
# denying them would break the only path that creates them. They remain in the
|
|
411
|
+
# allowed set (asserted above), so the reconcile still never calls them strays.
|
|
412
|
+
assert_nogrep "data-portal.json" "$DECL_SHIP" "ship-decl-excludes-agent-written-portal"
|
|
413
|
+
assert_nogrep "calendar-availability.json" "$DECL_SHIP" "ship-decl-excludes-agent-written-availability"
|
|
414
|
+
# A declared DIR never enters the file fence.
|
|
415
|
+
assert_nogrep "e-sign" "$DECL_SHIP" "ship-decl-excludes-dirs"
|
|
416
|
+
assert_grep "declared-files:start" "$(cat "$A_SHIP/SCHEMA.md")" "ship-decl-marker"
|
|
417
|
+
# Idempotent: a second merge leaves the file byte-identical.
|
|
418
|
+
B_SHIP="$(shasum "$A_SHIP/SCHEMA.md" | awk '{print $1}')"
|
|
419
|
+
merge_owned_dirs_into_schema "$A_SHIP" >/dev/null
|
|
420
|
+
assert_eq "$(shasum "$A_SHIP/SCHEMA.md" | awk '{print $1}')" "$B_SHIP" "ship-decl-idempotent"
|
|
421
|
+
# A brand whose payload declares no owned file emits no region at all.
|
|
422
|
+
assert_nogrep "declared-files:start" "$(cat "$A_MX/SCHEMA.md")" "mx-no-decl-region"
|
|
423
|
+
|
|
398
424
|
echo "PASS=$PASS FAIL=$FAIL"
|
|
399
425
|
if [ "$FAIL" -ne 0 ]; then printf '%s\n' "${FAILED[@]}"; exit 1; fi
|
|
@@ -8,6 +8,13 @@ by kind:
|
|
|
8
8
|
account-owned-dirs: [{"dir": "quoting", "description": "..."}]
|
|
9
9
|
account-owned-files: [{"file": "data-portal.json", "description": "..."}]
|
|
10
10
|
|
|
11
|
+
A file entry may add "writtenBy": "agent" to say the agent authors it under a
|
|
12
|
+
skill's instruction rather than platform code writing it whole. Such a file is
|
|
13
|
+
still declared — the reconcile must not call it a stray — but it is left out of
|
|
14
|
+
the declared-files fence the write guard denies on, because denying it would
|
|
15
|
+
block the only path that creates it. Absent or any other value means platform
|
|
16
|
+
code is the writer.
|
|
17
|
+
|
|
11
18
|
The owned-entry set for a brand is the union declared by every platform plugin
|
|
12
19
|
($PROJECT_DIR/plugins/*/PLUGIN.md) and every premium sub-plugin of a bundle the
|
|
13
20
|
brand ships ($PREMIUM_ROOT/<bundle>/plugins/*/PLUGIN.md, gated by brand.json
|
|
@@ -30,6 +37,8 @@ MARK_START = "<!-- plugin-owned-dirs:start -->"
|
|
|
30
37
|
MARK_END = "<!-- plugin-owned-dirs:end -->"
|
|
31
38
|
ONT_START = "<!-- ontology-buckets:start -->"
|
|
32
39
|
ONT_END = "<!-- ontology-buckets:end -->"
|
|
40
|
+
DECL_START = "<!-- declared-files:start -->"
|
|
41
|
+
DECL_END = "<!-- declared-files:end -->"
|
|
33
42
|
|
|
34
43
|
# Base top-level names shipped in the account template plus `.quarantine`. This
|
|
35
44
|
# set is prune-protection only: a reconcile never *adds* a missing seed entry,
|
|
@@ -109,7 +118,9 @@ def _read_vertical(project_dir):
|
|
|
109
118
|
|
|
110
119
|
|
|
111
120
|
def _parse_plugin_md(path):
|
|
112
|
-
"""Return (name, [ {name, kind, description} ]) from a PLUGIN.md,
|
|
121
|
+
"""Return (name, [ {name, kind, description, writtenBy} ]) from a PLUGIN.md,
|
|
122
|
+
or (None, []). `writtenBy` is '' unless the entry states one; 'agent' keeps
|
|
123
|
+
the name out of the declared-files fence (see the module docstring).
|
|
113
124
|
Two frontmatter keys, one list: `account-owned-dirs` yields kind='dir',
|
|
114
125
|
`account-owned-files` yields kind='file'. A key whose value is not a JSON
|
|
115
126
|
array of objects is skipped WHOLE: entries are staged and committed only once
|
|
@@ -145,7 +156,8 @@ def _parse_plugin_md(path):
|
|
|
145
156
|
n = (e.get(field) or "").strip()
|
|
146
157
|
if n and "/" not in n:
|
|
147
158
|
staged.append({"name": n, "kind": kind,
|
|
148
|
-
"description": (e.get("description") or "").strip()
|
|
159
|
+
"description": (e.get("description") or "").strip(),
|
|
160
|
+
"writtenBy": (e.get("writtenBy") or "").strip()})
|
|
149
161
|
except Exception:
|
|
150
162
|
continue
|
|
151
163
|
owned.extend(staged)
|
|
@@ -190,7 +202,8 @@ def resolve(project_dir):
|
|
|
190
202
|
if e["name"] in seen:
|
|
191
203
|
continue
|
|
192
204
|
seen[e["name"]] = {"name": e["name"], "kind": e["kind"],
|
|
193
|
-
"description": e["description"], "plugin": name or "unknown"
|
|
205
|
+
"description": e["description"], "plugin": name or "unknown",
|
|
206
|
+
"writtenBy": e["writtenBy"]}
|
|
194
207
|
order.append(e["name"])
|
|
195
208
|
return [seen[n] for n in order]
|
|
196
209
|
|
|
@@ -355,9 +368,10 @@ def merge(project_dir, account_dir):
|
|
|
355
368
|
lines = text.split("\n")
|
|
356
369
|
lines[start + 1:end] = new_entries
|
|
357
370
|
text = "\n".join(lines)
|
|
358
|
-
# Regenerate
|
|
371
|
+
# Regenerate every generated region wholesale (idempotent).
|
|
359
372
|
text = _strip_region(text, MARK_START, MARK_END)
|
|
360
373
|
text = _strip_region(text, ONT_START, ONT_END)
|
|
374
|
+
text = _strip_region(text, DECL_START, DECL_END)
|
|
361
375
|
if owned:
|
|
362
376
|
region = [MARK_START, "## Plugin-owned top-level entries", "",
|
|
363
377
|
"A plugin declares the account-root entries its feature owns. A",
|
|
@@ -393,6 +407,28 @@ def merge(project_dir, account_dir):
|
|
|
393
407
|
f"(owned by `{b['owner']}`), not a root bucket.")
|
|
394
408
|
region.append(ONT_END)
|
|
395
409
|
text = text.rstrip("\n") + "\n\n" + "\n".join(region) + "\n"
|
|
410
|
+
# The declared-file set, machine-readable and separate from the
|
|
411
|
+
# allowed-top-level fence. That fence is a layout list — it says a name may
|
|
412
|
+
# exist at the account root — and the write guard reads it as its allow-list,
|
|
413
|
+
# so a name unioned into it becomes agent-writable. This fence carries the
|
|
414
|
+
# other half the guard needs: which of those names is written whole by
|
|
415
|
+
# platform code, and which plugin to name when it blocks the write. An
|
|
416
|
+
# agent-written declaration is excluded: its skill authors it with Write, so
|
|
417
|
+
# denying it would block the only path that creates it.
|
|
418
|
+
declared = [e for e in owned if e["kind"] == "file" and e["writtenBy"] != "agent"]
|
|
419
|
+
if declared:
|
|
420
|
+
region = [DECL_START, "## Declared files (written by platform code, owned by a plugin)", "",
|
|
421
|
+
"Each file below is written whole by platform code and owned by the",
|
|
422
|
+
"plugin named beside it. It is control-plane state, not operator data:",
|
|
423
|
+
"never hand-edit one, because the next platform write overwrites the",
|
|
424
|
+
"edit. The write guard reads this block and blocks a write whose first",
|
|
425
|
+
"path segment is one of these names.", "",
|
|
426
|
+
"```declared-files"]
|
|
427
|
+
for e in declared:
|
|
428
|
+
region.append(f"{e['name']}\t{e['plugin']}")
|
|
429
|
+
region.append("```")
|
|
430
|
+
region.append(DECL_END)
|
|
431
|
+
text = text.rstrip("\n") + "\n\n" + "\n".join(region) + "\n"
|
|
396
432
|
with open(schema_path, "w") as f:
|
|
397
433
|
f.write(text)
|
|
398
434
|
root_dirs = [b["dir"] for b in domain if _is_root(b["owner"])]
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
# For per-session types (agent-stream/system/session/error/public): sessionKey
|
|
27
27
|
# is required in the first mode. For platform-scoped types (server/vnc/
|
|
28
28
|
# heartbeat/mem): the id is ignored. chat-attempts is --tail-only:
|
|
29
|
-
# cross-grep of edge.log + server.log for the chat-ingress
|
|
29
|
+
# cross-grep of edge.log + server.log for the chat-ingress checkpoints within
|
|
30
30
|
# the most recent N minutes.
|
|
31
31
|
#
|
|
32
32
|
# Every invocation writes a one-line trailer describing files searched,
|
|
@@ -154,14 +154,17 @@ is_per_conversation_type() {
|
|
|
154
154
|
SEARCH_TYPES="agent-stream error session public server mcp vnc"
|
|
155
155
|
VALID_TYPES="agent-stream, system, session, error, heartbeat, public, server, mcp, vnc, mem, chat-attempts"
|
|
156
156
|
|
|
157
|
-
# chat-attempts cross-grep prefixes.
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
157
|
+
# chat-attempts cross-grep prefixes. Four checkpoint patterns across
|
|
158
|
+
# edge.log + server.log: edge-side inbound/outcome, admin-auth gate verdict,
|
|
159
|
+
# chat-route handler entry. Any one chat emits THREE of the four, and which
|
|
160
|
+
# three depends on the surface: the admin webchat (POST /api/webchat/send)
|
|
161
|
+
# carries an admin-auth row and no chat-route row, public chat
|
|
162
|
+
# (POST /api/chat) carries a chat-route row and no admin-auth row. Absence of
|
|
163
|
+
# a row the surface does emit localises the failure to one ingress stage.
|
|
161
164
|
#
|
|
162
165
|
# The `ts=` group is optional and sits between the tag and the keyword because
|
|
163
166
|
# that is where Task 2024 put it: the shipped line is
|
|
164
|
-
# `[edge-admin] ts=<ISO> inbound …` (edge.ts:
|
|
167
|
+
# `[edge-admin] ts=<ISO> inbound …` (edge.ts:188, 199; edge-admin.ts:48, 52).
|
|
165
168
|
# Requiring tag and keyword adjacent matched zero lines and reported the
|
|
166
169
|
# absence as an edge-side silence (Task 2032). Optional rather than required,
|
|
167
170
|
# so the pattern is not pinned to one format version a second time.
|
|
@@ -186,7 +189,7 @@ Usage:
|
|
|
186
189
|
is_error result (silent-failure audit)
|
|
187
190
|
logs-read.sh --tail [type] [lines] Tail most recent log of type
|
|
188
191
|
logs-read.sh --tail chat-attempts [min] Cross-grep edge.log + server.log
|
|
189
|
-
for the chat-ingress
|
|
192
|
+
for the chat-ingress checkpoints
|
|
190
193
|
logs-read.sh --grep <sessionKey> [type] Legacy grep across log files
|
|
191
194
|
|
|
192
195
|
Types: $VALID_TYPES
|
|
@@ -200,9 +203,12 @@ transcript, flagging subagent is_error results inline. An explicit <type>
|
|
|
200
203
|
keeps the legacy per-account stream-log read.
|
|
201
204
|
Per-session stream-log types (agent-stream, system, session, error, public)
|
|
202
205
|
require sessionKey; platform-scoped types (server, vnc, heartbeat, mem) ignore the id.
|
|
203
|
-
chat-attempts is --tail-only — it aggregates
|
|
206
|
+
chat-attempts is --tail-only — it aggregates four ingress checkpoint patterns
|
|
204
207
|
([edge-admin] ts=<ISO> inbound, [admin-auth] outcome, [chat-route] entered,
|
|
205
|
-
[edge-admin] ts=<ISO> outcome) into one timeline.
|
|
208
|
+
[edge-admin] ts=<ISO> outcome) into one timeline. Any one chat emits three of
|
|
209
|
+
them: the admin webchat (POST /api/webchat/send) emits the two edge rows plus
|
|
210
|
+
[admin-auth] outcome and no [chat-route] row; public chat (POST /api/chat)
|
|
211
|
+
emits the two edge rows plus [chat-route] entered and no [admin-auth] row.
|
|
206
212
|
EOF
|
|
207
213
|
exit 2
|
|
208
214
|
}
|
|
@@ -216,25 +222,42 @@ validate_type() {
|
|
|
216
222
|
}
|
|
217
223
|
|
|
218
224
|
# chat-attempts cross-grep mode. Combines edge.log + server.log
|
|
219
|
-
# lines matching the chat-ingress
|
|
225
|
+
# lines matching the chat-ingress checkpoints, filtered to the last N minutes,
|
|
220
226
|
# sorted chronologically. Default window: 5 minutes (matches the typical
|
|
221
227
|
# "agent doesn't respond" report window — operator sees the silence,
|
|
222
228
|
# screenshots the chat, then runs this within a few minutes).
|
|
223
229
|
#
|
|
224
230
|
# Output preserves source-file prefix so operators can see whether the line
|
|
225
231
|
# came from the edge process (edge.log) or maxy-ui (server.log). On a
|
|
226
|
-
# successful chat POST the timeline reads
|
|
232
|
+
# successful chat POST the timeline reads THREE rows, and which three depends
|
|
233
|
+
# on the surface.
|
|
227
234
|
#
|
|
228
|
-
#
|
|
229
|
-
# [admin
|
|
230
|
-
# [
|
|
231
|
-
# [edge-admin] ts=… outcome method=POST path=… status=200 …
|
|
235
|
+
# admin webchat, POST /api/webchat/send:
|
|
236
|
+
# [edge-admin] ts=… inbound method=POST path=/api/webchat/send … (edge.log)
|
|
237
|
+
# [admin-auth] outcome=accept path=/api/webchat/send cacheKey=… (server.log)
|
|
238
|
+
# [edge-admin] ts=… outcome method=POST path=… status=200 … (edge.log)
|
|
232
239
|
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
#
|
|
237
|
-
#
|
|
240
|
+
# public chat, POST /api/chat:
|
|
241
|
+
# [edge-admin] ts=… inbound method=POST path=/api/chat … (edge.log)
|
|
242
|
+
# [chat-route] entered route=public method=POST (server.log)
|
|
243
|
+
# [edge-admin] ts=… outcome method=POST path=… status=200 … (edge.log)
|
|
244
|
+
#
|
|
245
|
+
# Nothing emits [chat-route] on the admin surface and nothing emits
|
|
246
|
+
# [admin-auth] on the public one, so the row the OTHER surface would have
|
|
247
|
+
# shown is not evidence of anything. Reading admin against a four-row
|
|
248
|
+
# sequence is what made every healthy admin chat look like a routing
|
|
249
|
+
# regression (Task 2042).
|
|
250
|
+
#
|
|
251
|
+
# A row missing from the surface's OWN sequence is diagnostic.
|
|
252
|
+
# Both surfaces: zero edge inbound = browser/network problem; edge outcome
|
|
253
|
+
# missing = upstream proxy crash.
|
|
254
|
+
# Admin: [admin-auth] outcome is the handler-side checkpoint. Missing against
|
|
255
|
+
# a present inbound = handler unreachable; outcome=reject = auth failure;
|
|
256
|
+
# outcome=accept already proves the route matched, because requireAdminSession
|
|
257
|
+
# is middleware mounted on /send (webchat.ts), so no further handler-side row
|
|
258
|
+
# is expected.
|
|
259
|
+
# Public: [chat-route] entered is the handler-side checkpoint. Missing against
|
|
260
|
+
# a present inbound for /api/chat = public-route routing regression.
|
|
238
261
|
#
|
|
239
262
|
# The two log families are stamped differently and neither is negotiable here:
|
|
240
263
|
# edge.log self-stamps `ts=` inside the line (Task 2024), server.log is stamped
|
|
@@ -263,10 +286,10 @@ chat_attempts_mode() {
|
|
|
263
286
|
# matched line, which is what the previous form spawned on both dialects.
|
|
264
287
|
#
|
|
265
288
|
# Milliseconds are load-bearing, not decoration. Both families emit
|
|
266
|
-
# `toISOString()`, and
|
|
267
|
-
# one wall-clock second, so a key truncated to seconds collides on
|
|
268
|
-
#
|
|
269
|
-
#
|
|
289
|
+
# `toISOString()`, and a chat's three checkpoints normally complete inside
|
|
290
|
+
# one wall-clock second, so a key truncated to seconds collides on every row
|
|
291
|
+
# and the sort falls back to file-append order — which puts the edge rows
|
|
292
|
+
# first and reproduces the clustering this mode was fixed to stop.
|
|
270
293
|
#
|
|
271
294
|
# An undated line reaches $tmp with `-` as its key. It cannot be placed in
|
|
272
295
|
# the window or in the sequence, so it is listed separately below rather
|
|
@@ -197,28 +197,31 @@ iso_minutes_ago() {
|
|
|
197
197
|
|| date -u -d "${m} minutes ago" +%Y-%m-%dT%H:%M:%S 2>/dev/null
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
# --- Case 6 (Task 2032): all four
|
|
201
|
-
|
|
200
|
+
# --- Case 6 (Task 2032): all four patterns match, interleaved by time ---
|
|
201
|
+
case_chat_attempts_both_surfaces() {
|
|
202
202
|
local root="/tmp/maxy-logs-read-test-6-$$"
|
|
203
203
|
local script
|
|
204
204
|
script=$(setup_install_tree "$root")
|
|
205
205
|
local logs="$HOME/.$(basename "$root")/logs"
|
|
206
|
-
# Four
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
# Shipped shapes: edge.log self-stamps `ts=` after the tag (edge.ts:
|
|
206
|
+
# Four checkpoint patterns in one window takes TWO chats, not one: no single
|
|
207
|
+
# surface emits four (Task 2042). A public chat then an admin chat, three
|
|
208
|
+
# rows each. Distinct milliseconds inside two minute-slots, so the ordering
|
|
209
|
+
# assertion below is strict and exercises the millisecond sort key.
|
|
210
|
+
local tp ta
|
|
211
|
+
tp=$(iso_minutes_ago 4); ta=$(iso_minutes_ago 2)
|
|
212
|
+
|
|
213
|
+
# Shipped shapes: edge.log self-stamps `ts=` after the tag (edge.ts:188,199);
|
|
214
214
|
# server.log is stamped at the line start by server-init-line-stamper.cjs.
|
|
215
|
+
# Both edge rows fire for any /api/ path (lib/proxy-log-path.ts:28-30).
|
|
215
216
|
{
|
|
216
|
-
echo "[edge-admin] ts=${
|
|
217
|
-
echo "[edge-admin] ts=${
|
|
217
|
+
echo "[edge-admin] ts=${tp}.100Z inbound method=POST path=/api/chat token=absent allowlist-branch=passthrough"
|
|
218
|
+
echo "[edge-admin] ts=${tp}.300Z outcome method=POST path=/api/chat status=200 duration_ms=200"
|
|
219
|
+
echo "[edge-admin] ts=${ta}.100Z inbound method=POST path=/api/webchat/send token=present allowlist-branch=passthrough"
|
|
220
|
+
echo "[edge-admin] ts=${ta}.300Z outcome method=POST path=/api/webchat/send status=200 duration_ms=200"
|
|
218
221
|
} > "$logs/edge.log"
|
|
219
222
|
{
|
|
220
|
-
echo "${
|
|
221
|
-
echo "${
|
|
223
|
+
echo "${tp}.200Z [chat-route] entered route=public method=POST"
|
|
224
|
+
echo "${ta}.200Z [admin-auth] outcome=accept path=/api/webchat/send cacheKey=deadbeef…"
|
|
222
225
|
} > "$logs/server.log"
|
|
223
226
|
|
|
224
227
|
local stdout stderr rc=0
|
|
@@ -232,14 +235,15 @@ case_chat_attempts_quartet() {
|
|
|
232
235
|
if [[ "$stdout" != *"inbound"* ]]; then echo " timeline missing edge inbound"; return 1; fi
|
|
233
236
|
if [[ "$stdout" != *"[edge-admin]"*"outcome"* ]]; then echo " timeline missing edge outcome"; return 1; fi
|
|
234
237
|
if [[ "$stdout" != *"[admin-auth] outcome=accept"* ]]; then echo " timeline missing admin-auth"; return 1; fi
|
|
235
|
-
if [[ "$stdout" != *"[chat-route] entered"* ]]; then echo " timeline missing chat-route"; return 1; fi
|
|
236
|
-
if [[ "$stderr" != *"edge=
|
|
238
|
+
if [[ "$stdout" != *"[chat-route] entered route=public"* ]]; then echo " timeline missing chat-route"; return 1; fi
|
|
239
|
+
if [[ "$stderr" != *"edge=4"* ]]; then echo " trailer missing edge=4: $stderr"; return 1; fi
|
|
237
240
|
if [[ "$stderr" != *"server=2"* ]]; then echo " trailer missing server=2: $stderr"; return 1; fi
|
|
238
241
|
if [[ "$stderr" != *"undated=0"* ]]; then echo " trailer missing undated=0: $stderr"; return 1; fi
|
|
239
242
|
|
|
240
|
-
# Chronological
|
|
241
|
-
# chat-route row
|
|
242
|
-
# rows adjacent instead, which is the ordering defect
|
|
243
|
+
# Chronological across the two log families: the public chat's edge inbound
|
|
244
|
+
# precedes its chat-route row, which precedes its edge outcome. A fixed-column
|
|
245
|
+
# sort put the two edge rows adjacent instead, which is the ordering defect
|
|
246
|
+
# this pins.
|
|
243
247
|
local first mid last
|
|
244
248
|
first=$(echo "$stdout" | grep -n 'inbound' | head -1 | cut -d: -f1)
|
|
245
249
|
mid=$(echo "$stdout" | grep -n '\[chat-route\] entered' | head -1 | cut -d: -f1)
|
|
@@ -293,7 +297,7 @@ case_chat_attempts_window() {
|
|
|
293
297
|
recent=$(iso_minutes_ago 1)
|
|
294
298
|
old=$(iso_minutes_ago 90)
|
|
295
299
|
{
|
|
296
|
-
echo "[edge-admin] ts=${old}.000Z inbound method=POST path=/api/
|
|
300
|
+
echo "[edge-admin] ts=${old}.000Z inbound method=POST path=/api/webchat/send token=present allowlist-branch=passthrough"
|
|
297
301
|
echo "[edge-admin] ts=${recent}.000Z inbound method=GET path=/api/admin/version token=absent allowlist-branch=passthrough"
|
|
298
302
|
} > "$logs/edge.log"
|
|
299
303
|
: > "$logs/server.log"
|
|
@@ -312,11 +316,13 @@ case_chat_attempts_window() {
|
|
|
312
316
|
return 0
|
|
313
317
|
}
|
|
314
318
|
|
|
315
|
-
# --- Case 9 (Task 2032):
|
|
316
|
-
#
|
|
317
|
-
#
|
|
318
|
-
#
|
|
319
|
-
#
|
|
319
|
+
# --- Case 9 (Task 2032): all four patterns inside one second still order ---
|
|
320
|
+
# A chat POST completes in a few hundred milliseconds, so its checkpoints share
|
|
321
|
+
# a wall-clock second; an admin chat and a public chat overlapping in that same
|
|
322
|
+
# second put all four patterns there at once. A sort key truncated to seconds
|
|
323
|
+
# collides on every row and falls back to file-append order, which puts the edge
|
|
324
|
+
# rows first — the clustering this mode was fixed to stop. Six rows, because no
|
|
325
|
+
# single surface emits four checkpoints (Task 2042).
|
|
320
326
|
case_chat_attempts_subsecond_order() {
|
|
321
327
|
local root="/tmp/maxy-logs-read-test-9-$$"
|
|
322
328
|
local script
|
|
@@ -325,11 +331,13 @@ case_chat_attempts_subsecond_order() {
|
|
|
325
331
|
local s
|
|
326
332
|
s=$(iso_minutes_ago 1)
|
|
327
333
|
{
|
|
328
|
-
echo "[edge-admin] ts=${s}.010Z inbound method=POST path=/api/
|
|
329
|
-
echo "[edge-admin] ts=${s}.
|
|
334
|
+
echo "[edge-admin] ts=${s}.010Z inbound method=POST path=/api/webchat/send token=present allowlist-branch=passthrough"
|
|
335
|
+
echo "[edge-admin] ts=${s}.060Z inbound method=POST path=/api/chat token=absent allowlist-branch=passthrough"
|
|
336
|
+
echo "[edge-admin] ts=${s}.090Z outcome method=POST path=/api/chat status=200 duration_ms=30"
|
|
337
|
+
echo "[edge-admin] ts=${s}.230Z outcome method=POST path=/api/webchat/send status=200 duration_ms=220"
|
|
330
338
|
} > "$logs/edge.log"
|
|
331
339
|
{
|
|
332
|
-
echo "${s}.050Z [admin-auth] outcome=accept path=/api/
|
|
340
|
+
echo "${s}.050Z [admin-auth] outcome=accept path=/api/webchat/send cacheKey=deadbeef"
|
|
333
341
|
echo "${s}.070Z [chat-route] entered route=public method=POST"
|
|
334
342
|
} > "$logs/server.log"
|
|
335
343
|
|
|
@@ -365,7 +373,7 @@ case_chat_attempts_undated_not_silence() {
|
|
|
365
373
|
local script
|
|
366
374
|
script=$(setup_install_tree "$root")
|
|
367
375
|
local logs="$HOME/.$(basename "$root")/logs"
|
|
368
|
-
echo "[edge-admin] inbound method=POST path=/api/
|
|
376
|
+
echo "[edge-admin] inbound method=POST path=/api/webchat/send token=present allowlist-branch=passthrough" > "$logs/edge.log"
|
|
369
377
|
: > "$logs/server.log"
|
|
370
378
|
|
|
371
379
|
local stdout stderr rc=0
|
|
@@ -416,17 +424,51 @@ case_chat_attempts_edge_log_absent() {
|
|
|
416
424
|
return 0
|
|
417
425
|
}
|
|
418
426
|
|
|
427
|
+
# --- Case 12 (Task 2042): usage names each surface, not four rows per chat ---
|
|
428
|
+
# The mode greps four checkpoint patterns, but no single chat emits four: the
|
|
429
|
+
# admin webchat has no [chat-route] row and public chat has no [admin-auth]
|
|
430
|
+
# row. Usage that promised four made every healthy admin chat read as a
|
|
431
|
+
# routing regression, so the count and both surface paths are pinned here.
|
|
432
|
+
case_usage_checkpoints_per_surface() {
|
|
433
|
+
local root="/tmp/maxy-logs-read-test-12-$$"
|
|
434
|
+
local script
|
|
435
|
+
script=$(setup_install_tree "$root")
|
|
436
|
+
|
|
437
|
+
local stderr rc=0
|
|
438
|
+
"$script" >/dev/null 2>/tmp/maxy-logs-read-stderr-12-$$ || rc=$?
|
|
439
|
+
stderr=$(cat /tmp/maxy-logs-read-stderr-12-$$)
|
|
440
|
+
rm -f /tmp/maxy-logs-read-stderr-12-$$
|
|
441
|
+
|
|
442
|
+
cleanup_install_tree "$root"
|
|
443
|
+
|
|
444
|
+
if [[ $rc -ne 2 ]]; then echo " expected usage exit 2, got $rc"; return 1; fi
|
|
445
|
+
if [[ "$stderr" == *"four ingress checkpoints"* ]]; then
|
|
446
|
+
echo " usage still claims four checkpoints per chat"; return 1
|
|
447
|
+
fi
|
|
448
|
+
if [[ "$stderr" == *"quartet"* ]]; then
|
|
449
|
+
echo " usage still calls the checkpoints a quartet"; return 1
|
|
450
|
+
fi
|
|
451
|
+
if [[ "$stderr" != *"/api/webchat/send"* ]]; then
|
|
452
|
+
echo " usage does not name the admin surface path"; return 1
|
|
453
|
+
fi
|
|
454
|
+
if [[ "$stderr" != *"/api/chat"* ]]; then
|
|
455
|
+
echo " usage does not name the public surface path"; return 1
|
|
456
|
+
fi
|
|
457
|
+
return 0
|
|
458
|
+
}
|
|
459
|
+
|
|
419
460
|
run_case "sessionKey file present → exit 0" case_file_present
|
|
420
461
|
run_case "no file → exit 1 + missing-on-resolve" case_missing
|
|
421
462
|
run_case "ambiguous prefix → exit 1, refuses to pick" case_ambiguous
|
|
422
463
|
run_case "--tail mem present → exit 0 + sampler lines" case_mem_present
|
|
423
464
|
run_case "--tail mem absent → exit 1 + not-found message" case_mem_absent
|
|
424
|
-
run_case "chat-attempts
|
|
465
|
+
run_case "chat-attempts matches both surfaces and interleaves" case_chat_attempts_both_surfaces
|
|
425
466
|
run_case "chat-attempts genuine zero states what it means" case_chat_attempts_genuine_zero
|
|
426
467
|
run_case "chat-attempts window excludes an older line" case_chat_attempts_window
|
|
427
|
-
run_case "chat-attempts orders four
|
|
468
|
+
run_case "chat-attempts orders all four patterns inside one second" case_chat_attempts_subsecond_order
|
|
428
469
|
run_case "chat-attempts undated match is not edge silence" case_chat_attempts_undated_not_silence
|
|
429
470
|
run_case "chat-attempts absent edge.log is reader blindness" case_chat_attempts_edge_log_absent
|
|
471
|
+
run_case "usage names each surface's checkpoints" case_usage_checkpoints_per_surface
|
|
430
472
|
|
|
431
473
|
echo ""
|
|
432
474
|
echo "================================================"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# The one documented action that makes a fresh worktree able to run the
|
|
3
|
+
# platform/ui suite. Task 2034.
|
|
4
|
+
#
|
|
5
|
+
# A git worktree carries tracked files only. platform/node_modules,
|
|
6
|
+
# platform/ui/node_modules and platform/lib/*/dist are gitignored build output, so
|
|
7
|
+
# a fresh worktree has none of them, and `npm install` inside platform/ui creates
|
|
8
|
+
# neither the hoisted platform/node_modules nor any lib dist. Without this, the
|
|
9
|
+
# suite fails on absent artifacts in a way that reads as flaky tests.
|
|
10
|
+
#
|
|
11
|
+
# Two things here are not obvious and are the reason this is a script:
|
|
12
|
+
#
|
|
13
|
+
# 1. tsc is called as platform/node_modules/.bin/tsc, never bare. platform's
|
|
14
|
+
# own package.json declares no typescript; it hoists one from 28 workspace
|
|
15
|
+
# members. Inside an npm script npm prepends node_modules/.bin to PATH and a
|
|
16
|
+
# bare `tsc` resolves; from a bare shell it does not, and the tsc on PATH is
|
|
17
|
+
# an unrelated program that refuses to run.
|
|
18
|
+
#
|
|
19
|
+
# 2. The libs are built in a loop, not through `npm run build:lib`. build:lib
|
|
20
|
+
# chains its 27 compiler runs with &&, and one lib emits its output while
|
|
21
|
+
# still reporting type errors, which would abort every run after it. So each
|
|
22
|
+
# lib is built independently and the final gate is artifact presence, not
|
|
23
|
+
# compiler exit status — the only check that separates "emitted with type
|
|
24
|
+
# errors" from "emitted nothing".
|
|
25
|
+
|
|
26
|
+
set -uo pipefail
|
|
27
|
+
|
|
28
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
29
|
+
PLATFORM="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
|
30
|
+
TSC="$PLATFORM/node_modules/.bin/tsc"
|
|
31
|
+
GATE="$PLATFORM/ui/scripts/check-worktree-provisioned.mjs"
|
|
32
|
+
|
|
33
|
+
echo "[provision] platform: $PLATFORM"
|
|
34
|
+
|
|
35
|
+
echo "[provision] stage 1/3: npm install in platform/"
|
|
36
|
+
if ! (cd "$PLATFORM" && npm install); then
|
|
37
|
+
echo "[provision] FAILED at stage 1: npm install in platform/" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
echo "[provision] stage 2/3: npm install in platform/ui"
|
|
42
|
+
if ! (cd "$PLATFORM/ui" && npm install); then
|
|
43
|
+
echo "[provision] FAILED at stage 2: npm install in platform/ui" >&2
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
if [ ! -x "$TSC" ]; then
|
|
48
|
+
echo "[provision] FAILED: $TSC absent after stage 1." >&2
|
|
49
|
+
echo "[provision] typescript hoists to platform/node_modules from the workspace" >&2
|
|
50
|
+
echo "[provision] members; if it is missing, stage 1 did not complete." >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
echo "[provision] stage 3/3: building platform/lib/*/dist"
|
|
55
|
+
emitted_with_errors=()
|
|
56
|
+
for tsconfig in "$PLATFORM"/lib/*/tsconfig.json; do
|
|
57
|
+
lib="$(basename "$(dirname "$tsconfig")")"
|
|
58
|
+
if "$TSC" -p "$tsconfig" >/dev/null 2>&1; then
|
|
59
|
+
printf ' %-32s ok\n' "$lib"
|
|
60
|
+
elif [ -f "$PLATFORM/lib/$lib/dist/index.js" ]; then
|
|
61
|
+
printf ' %-32s emitted, with type errors\n' "$lib"
|
|
62
|
+
emitted_with_errors+=("$lib")
|
|
63
|
+
else
|
|
64
|
+
printf ' %-32s FAILED, emitted nothing\n' "$lib"
|
|
65
|
+
fi
|
|
66
|
+
done
|
|
67
|
+
|
|
68
|
+
if [ ${#emitted_with_errors[@]} -gt 0 ]; then
|
|
69
|
+
echo "[provision] ${#emitted_with_errors[@]} lib(s) emitted output while reporting type"
|
|
70
|
+
echo "[provision] errors: ${emitted_with_errors[*]}. Their dist is present, so the suite"
|
|
71
|
+
echo "[provision] can run. The type errors themselves are out of scope for provisioning."
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
echo "[provision] verifying against the gate"
|
|
75
|
+
if ! node "$GATE"; then
|
|
76
|
+
echo "[provision] FAILED: the tree is still unprovisioned after all three stages." >&2
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
echo "[provision] done. platform/ui is ready: cd platform/ui && npm test"
|
|
@@ -6161,9 +6161,8 @@ async function tryCookieBridgeForConversation(c, cacheKey2, sessionId) {
|
|
|
6161
6161
|
console.error(`[session] cookie-bridge-rejected reason=conversation-not-found path=${path} cacheKey=${sk8}\u2026 sessionId=${cid8}\u2026`);
|
|
6162
6162
|
return { ok: false, reason: "conversation-not-found" };
|
|
6163
6163
|
}
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
console.error(`[session] cookie-bridge-rejected reason=account-mismatch path=${path} cacheKey=${sk8}\u2026 sessionId=${cid8}\u2026 conversationAccountId=${owner.accountId.slice(0, 8)}\u2026 deviceAccountId=${(deviceAccount?.accountId ?? "none").slice(0, 8)}\u2026`);
|
|
6164
|
+
if (!isValidAccountId(owner.accountId)) {
|
|
6165
|
+
console.error(`[session] cookie-bridge-rejected reason=account-mismatch path=${path} cacheKey=${sk8}\u2026 sessionId=${cid8}\u2026 conversationAccountId=${owner.accountId.slice(0, 8)}\u2026`);
|
|
6167
6166
|
return { ok: false, reason: "account-mismatch" };
|
|
6168
6167
|
}
|
|
6169
6168
|
registerSession(cacheKey2, "admin", owner.accountId, void 0, owner.userId ?? void 0);
|