@rubytech/create-maxy 1.0.884 → 1.0.886
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/neo4j/schema.cypher +7 -0
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js +21 -50
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +1 -1
- package/payload/platform/scripts/__tests__/logs-read-prefix.sh +85 -240
- package/payload/platform/scripts/check-no-conversation-id-leaks.mjs +165 -0
- package/payload/platform/scripts/conversation-id-allowlist.txt +151 -0
- package/payload/platform/scripts/log-adherence-check.sh +100 -0
- package/payload/platform/scripts/logs-read.sh +71 -141
- package/payload/platform/scripts/logs-read.test.sh +47 -104
- package/payload/platform/scripts/seed-neo4j.sh +46 -0
- package/payload/premium-plugins/real-agency/BUNDLE.md +1 -1
- package/payload/server/chunk-IFMZ5I3E.js +1460 -0
- package/payload/server/chunk-MOAY7KG2.js +11667 -0
- package/payload/server/chunk-NPKQWE3S.js +1431 -0
- package/payload/server/chunk-ZVO5ASQA.js +11660 -0
- package/payload/server/client-pool-M6NS5G2U.js +34 -0
- package/payload/server/client-pool-QUMX7OUT.js +34 -0
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/server.js +181 -137
|
@@ -83,7 +83,7 @@ Premium plugins are one-off purchases that grant permanent ownership. They are n
|
|
|
83
83
|
|
|
84
84
|
Some premium plugins are **bundles** — a single purchase that delivers multiple sub-plugins, each independently activatable. For example, Real Agency delivers 11 sub-plugins covering different aspects of estate agency work. You can enable all of them or just the ones you need (e.g., "Enable estate-sales" for just the sales skills). Enabling or disabling individual sub-plugins does not affect the others.
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
Every admin session start reconciles your `enabledPlugins` against what is actually on disk: any purchased sub-plugin present in `platform/plugins/` is enabled, so you don't need to enable each one by hand. This holds whether the bundle was delivered just now (fresh purchase via `premium-deliver`) or has been on disk from a prior install. Sub-plugins you don't want active can be turned off individually with "disable <name>". Standalone premium plugins behave the same way: presence on disk implies enablement. If you ask {{productName}} about a tool from a plugin you haven't purchased, {{productName}} responds with a structured `<tool-surface-error>` envelope naming the missing plugin and the remedy, rather than improvising with a generic alternative.
|
|
87
87
|
|
|
88
88
|
**Public agent embedding:** Premium plugins marked as public-eligible have their full content (skills and reference knowledge) embedded in public agent prompts. This means a public agent for a Real Agency member can handle buyer enquiries, book viewings, deliver coaching content, and onboard new applicants — all powered by the premium plugin's domain knowledge. Plugins marked admin-only (listings, vendors, leads, business) are only available to the account owner's admin agent.
|
|
89
89
|
|
|
@@ -73,7 +73,7 @@ tail -200 ~/.maxy/logs/maxy-ui.log | rg '\[remote-auth\].*resolvedKind='
|
|
|
73
73
|
**Agent searches the filesystem after uploading a zip.** If you uploaded a zip and the agent burns several turns running `find` / `Glob` instead of unzipping, that is the symptom of the recovery-retry attachment-context regression (now closed by the recovery context preservation contract in `.docs/agents.md`). Greppable confirmation is the `[context-overflow-recovery] retry … attachmentsCarried=<n>` line in the conversation stream log. If you see `[context-overflow-recovery] WARN attachment-context-lost`, the regression has returned — surface to support.
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
**A turn rendered in chat is missing on next page-refresh.** Pre-the 2026-05-07 mandate this was a class of silent failure — Neo4j persists were wrapped in a no-op error catch and a write that threw left the artefact "rendered then disappeared on resume". The 2026-05-07 mandate makes JSONL canonical: the resume route reads the SDK transcript file at `~/.claude/projects/<project-key>/<sessionId>.jsonl` first, supplements from Neo4j, and triggers async heal-on-resume writes for any turn the JSONL has but Neo4j does not. So a refreshed conversation always renders what the SDK saw, regardless of write outcome. If a heal write itself fails, the chat shows a top-of-conversation banner naming the count; if every heal succeeds the resume is silent and the missing rows are quietly restored to Neo4j. Greppable post-deploy invariants in the per-
|
|
76
|
+
**A turn rendered in chat is missing on next page-refresh.** Pre-the 2026-05-07 mandate this was a class of silent failure — Neo4j persists were wrapped in a no-op error catch and a write that threw left the artefact "rendered then disappeared on resume". The 2026-05-07 mandate makes JSONL canonical: the resume route reads the SDK transcript file at `~/.claude/projects/<project-key>/<sessionId>.jsonl` first, supplements from Neo4j, and triggers async heal-on-resume writes for any turn the JSONL has but Neo4j does not. So a refreshed conversation always renders what the SDK saw, regardless of write outcome. If a heal write itself fails, the chat shows a top-of-conversation banner naming the count; if every heal succeeds the resume is silent and the missing rows are quietly restored to Neo4j. Greppable post-deploy invariants in the per-session stream log (`logs/claude-agent-stream-<sessionKey>.log`): `[admin-resume] reason=<…> source=<jsonl|jsonl-missing|neo4j-only>` (one per resume), `[admin-persist] convId=<8> writer=<…> outcome=<ok|fail|skip>` (per persist site), `[admin-persist-heal] convId=<8> turnIndex=<n> outcome=<ok|fail>` (per heal write). To force-audit a specific conversation against its Neo4j projection without re-executing it, run `tsx platform/scripts/admin-persist-audit.ts --conversation-id=<uuid> --account-id=<uuid> --session-id=<uuid>` — non-zero exit + per-divergence `[admin-persist-audit] expected=<message|component> missing reason=neo4j-row-absent` lines name what would have been silently lost pre-mandate.
|
|
77
77
|
**Wrong Claude account answering on a multi-brand device.** On a host running both Maxy and Real Agent, each brand's admin agent reads its own `~/${brand.configDir}/.claude/.credentials.json`; there is no longer a shared `~/.claude/` thrashing them against one another. If a brand reports auth failures or appears to be operating against the wrong subscription, check three things:
|
|
78
78
|
1. `grep "\[claude-auth\] init" ~/.${brand}/logs/server.log | tail -1` — the resolved path must end with `~/.${brand}/.claude/.credentials.json`. If a `[claude-auth] WARN cross-brand-path-detected` line is present, the runtime is still pointing at `~/.claude/`; the brand main service did not pick up the `Environment=CLAUDE_CONFIG_DIR=` setting (re-run the brand installer to refresh the unit file).
|
|
79
79
|
2. `diff <(jq .claudeAiOauth.accessToken ~/.maxy/.claude/.credentials.json) <(jq .claudeAiOauth.accessToken ~/.realagent/.claude/.credentials.json)` — must be non-empty after each brand's operator has run `claude /login` against distinct Anthropic accounts; if it's empty, both brands are still logged in to the same account (operator action, not a code bug).
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Task
|
|
2
|
+
# Task 1006 — prefix-match harness for logs-read.sh.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# 3. More-specific prefix disambiguates
|
|
8
|
-
# 4. Zero matches → reason=file-not-found-in-either-shape with glob patterns
|
|
9
|
-
# 5. Full 36-char UUID still resolves (regression boundary)
|
|
10
|
-
# 6. Preflush prefix match
|
|
11
|
-
# 7. Preflush ambiguity (Pass 1 zero, Pass 2 multiple)
|
|
12
|
-
# 8. Every per-conversation type (agent-stream, session, error, public)
|
|
13
|
-
# resolves under prefix-match
|
|
4
|
+
# Task 998 introduced 8-char-prefix resolution; Task 1006 collapses the
|
|
5
|
+
# writer's two-shape contract to one (`claude-agent-stream-<sessionKey>.log`).
|
|
6
|
+
# The prefix match still applies, now against a single basename shape.
|
|
14
7
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
8
|
+
# Cases:
|
|
9
|
+
# 1. 8-char prefix resolves a full sessionKey-named file.
|
|
10
|
+
# 2. Multiple matches → ambiguous-prefix, exit 1, never picks silently.
|
|
11
|
+
# 3. More-specific prefix disambiguates.
|
|
12
|
+
# 4. Zero matches → exit 1 with file-not-found trailer.
|
|
13
|
+
# 5. Full 36-char sessionKey still resolves (regression boundary).
|
|
14
|
+
# 6. Every per-session type (agent-stream, session, error, public)
|
|
15
|
+
# resolves under prefix-match.
|
|
17
16
|
set -euo pipefail
|
|
18
17
|
|
|
19
18
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -26,9 +25,6 @@ fi
|
|
|
26
25
|
PASS=0
|
|
27
26
|
FAIL=0
|
|
28
27
|
|
|
29
|
-
# logs-read.sh resolves PLATFORM_ROOT from its own location; mirror the
|
|
30
|
-
# logs-read.test.sh convention by symlinking the script into an ephemeral
|
|
31
|
-
# install tree per case.
|
|
32
28
|
setup_install_tree() {
|
|
33
29
|
local root="$1"
|
|
34
30
|
mkdir -p "$root/platform/scripts"
|
|
@@ -58,281 +54,130 @@ run_case() {
|
|
|
58
54
|
fi
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
local root="/tmp/maxy-logs-read-prefix-1-$$"
|
|
57
|
+
case_8char_prefix_resolves_full() {
|
|
58
|
+
local root="/tmp/maxy-prefix-test-1-$$"
|
|
64
59
|
local script
|
|
65
60
|
script=$(setup_install_tree "$root")
|
|
66
61
|
local logdir="$root/data/accounts/acct-test/logs"
|
|
67
|
-
local
|
|
68
|
-
|
|
69
|
-
echo "prefix-match-sentinel" > "$logdir/$filename"
|
|
70
|
-
|
|
71
|
-
local stdout stderr rc=0
|
|
72
|
-
stdout=$("$script" "3483269d" system 2>/tmp/logs-read-prefix-stderr-1-$$) || rc=$?
|
|
73
|
-
stderr=$(cat /tmp/logs-read-prefix-stderr-1-$$)
|
|
74
|
-
rm -f /tmp/logs-read-prefix-stderr-1-$$
|
|
62
|
+
local sk="abcd1234-5678-90ab-cdef-1234567890ab"
|
|
63
|
+
echo "sentinel-1" > "$logdir/claude-agent-stream-${sk}.log"
|
|
75
64
|
|
|
65
|
+
local rc=0
|
|
66
|
+
local stdout
|
|
67
|
+
stdout=$("$script" "abcd1234" agent-stream 2>/dev/null) || rc=$?
|
|
76
68
|
cleanup_install_tree "$root"
|
|
77
69
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
echo " stdout missing sentinel: $stdout"; return 1
|
|
81
|
-
fi
|
|
82
|
-
if [[ "$stderr" != *"matched_shape=full"* ]]; then
|
|
83
|
-
echo " stderr missing matched_shape=full: $stderr"; return 1
|
|
84
|
-
fi
|
|
85
|
-
if [[ "$stdout" != *"$filename"* ]]; then
|
|
86
|
-
echo " header missing matched filename: $stdout"; return 1
|
|
87
|
-
fi
|
|
70
|
+
[[ $rc -eq 0 ]] || { echo " expected exit 0, got $rc"; return 1; }
|
|
71
|
+
[[ "$stdout" == *"sentinel-1"* ]] || { echo " missing sentinel"; return 1; }
|
|
88
72
|
return 0
|
|
89
73
|
}
|
|
90
74
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
local root="/tmp/maxy-logs-read-prefix-2-$$"
|
|
75
|
+
case_ambiguous_prefix_refuses() {
|
|
76
|
+
local root="/tmp/maxy-prefix-test-2-$$"
|
|
94
77
|
local script
|
|
95
78
|
script=$(setup_install_tree "$root")
|
|
96
79
|
local logdir="$root/data/accounts/acct-test/logs"
|
|
97
|
-
echo "
|
|
98
|
-
echo "
|
|
99
|
-
|
|
100
|
-
local
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
80
|
+
echo "a" > "$logdir/claude-agent-stream-abcd1111.log"
|
|
81
|
+
echo "b" > "$logdir/claude-agent-stream-abcd2222.log"
|
|
82
|
+
|
|
83
|
+
local rc=0
|
|
84
|
+
"$script" "abcd" agent-stream >/dev/null 2>/tmp/prefix-stderr-2-$$ || rc=$?
|
|
85
|
+
local stderr
|
|
86
|
+
stderr=$(cat /tmp/prefix-stderr-2-$$)
|
|
87
|
+
rm -f /tmp/prefix-stderr-2-$$
|
|
105
88
|
cleanup_install_tree "$root"
|
|
106
89
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
echo " stderr missing reason=ambiguous-prefix: $stderr"; return 1
|
|
110
|
-
fi
|
|
111
|
-
if [[ "$stderr" != *"matches=2"* ]]; then
|
|
112
|
-
echo " stderr missing matches=2: $stderr"; return 1
|
|
113
|
-
fi
|
|
114
|
-
if [[ "$stderr" != *"claude-agent-stream-aaaaaaaa-1111-A.log"* ]]; then
|
|
115
|
-
echo " stderr missing candidate A: $stderr"; return 1
|
|
116
|
-
fi
|
|
117
|
-
if [[ "$stderr" != *"claude-agent-stream-aaaaaaaa-2222-B.log"* ]]; then
|
|
118
|
-
echo " stderr missing candidate B: $stderr"; return 1
|
|
119
|
-
fi
|
|
120
|
-
# Body must NOT contain either sentinel — refusal means no body emitted.
|
|
121
|
-
if [[ -n "$stdout" ]]; then
|
|
122
|
-
echo " stdout non-empty on ambiguous refusal: $stdout"; return 1
|
|
123
|
-
fi
|
|
90
|
+
[[ $rc -eq 1 ]] || { echo " expected exit 1, got $rc"; return 1; }
|
|
91
|
+
[[ "$stderr" == *"ambiguous-prefix"* ]] || { echo " stderr missing ambiguous-prefix: $stderr"; return 1; }
|
|
124
92
|
return 0
|
|
125
93
|
}
|
|
126
94
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
local root="/tmp/maxy-logs-read-prefix-3-$$"
|
|
95
|
+
case_more_specific_prefix_resolves() {
|
|
96
|
+
local root="/tmp/maxy-prefix-test-3-$$"
|
|
130
97
|
local script
|
|
131
98
|
script=$(setup_install_tree "$root")
|
|
132
99
|
local logdir="$root/data/accounts/acct-test/logs"
|
|
133
|
-
echo "
|
|
134
|
-
echo "
|
|
135
|
-
|
|
136
|
-
local stdout stderr rc=0
|
|
137
|
-
stdout=$("$script" "aaaaaaaa-1111" system 2>/tmp/logs-read-prefix-stderr-3-$$) || rc=$?
|
|
138
|
-
stderr=$(cat /tmp/logs-read-prefix-stderr-3-$$)
|
|
139
|
-
rm -f /tmp/logs-read-prefix-stderr-3-$$
|
|
100
|
+
echo "a" > "$logdir/claude-agent-stream-abcd1111.log"
|
|
101
|
+
echo "b" > "$logdir/claude-agent-stream-abcd2222.log"
|
|
140
102
|
|
|
103
|
+
local rc=0
|
|
104
|
+
local stdout
|
|
105
|
+
stdout=$("$script" "abcd1111" agent-stream 2>/dev/null) || rc=$?
|
|
141
106
|
cleanup_install_tree "$root"
|
|
142
107
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if [[ "$stdout" == *"TWO"* ]]; then echo " stdout contained TWO: $stdout"; return 1; fi
|
|
146
|
-
if [[ "$stderr" != *"matched_shape=full"* ]]; then
|
|
147
|
-
echo " stderr missing matched_shape=full: $stderr"; return 1
|
|
148
|
-
fi
|
|
108
|
+
[[ $rc -eq 0 ]] || { echo " expected exit 0, got $rc"; return 1; }
|
|
109
|
+
[[ "$stdout" == *"a"* ]] || { echo " missing sentinel"; return 1; }
|
|
149
110
|
return 0
|
|
150
111
|
}
|
|
151
112
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
local root="/tmp/maxy-logs-read-prefix-4-$$"
|
|
113
|
+
case_zero_match_miss() {
|
|
114
|
+
local root="/tmp/maxy-prefix-test-4-$$"
|
|
155
115
|
local script
|
|
156
116
|
script=$(setup_install_tree "$root")
|
|
157
117
|
|
|
158
|
-
local
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
118
|
+
local rc=0
|
|
119
|
+
"$script" "nomatch1" agent-stream >/dev/null 2>/tmp/prefix-stderr-4-$$ || rc=$?
|
|
120
|
+
local stderr
|
|
121
|
+
stderr=$(cat /tmp/prefix-stderr-4-$$)
|
|
122
|
+
rm -f /tmp/prefix-stderr-4-$$
|
|
163
123
|
cleanup_install_tree "$root"
|
|
164
124
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
echo " stderr missing reason=file-not-found-in-either-shape: $stderr"; return 1
|
|
168
|
-
fi
|
|
169
|
-
if [[ "$stderr" != *"tried=[claude-agent-stream-xxxxxxxx*.log, claude-agent-stream-preflush-xxxxxxxx*.log]"* ]]; then
|
|
170
|
-
echo " stderr missing tried=[<glob>, <glob>]: $stderr"; return 1
|
|
171
|
-
fi
|
|
125
|
+
[[ $rc -eq 1 ]] || { echo " expected exit 1, got $rc"; return 1; }
|
|
126
|
+
[[ "$stderr" == *"file-not-found"* ]] || { echo " stderr missing file-not-found"; return 1; }
|
|
172
127
|
return 0
|
|
173
128
|
}
|
|
174
129
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
local root="/tmp/maxy-logs-read-prefix-5-$$"
|
|
130
|
+
case_full_sessionkey_resolves() {
|
|
131
|
+
local root="/tmp/maxy-prefix-test-5-$$"
|
|
178
132
|
local script
|
|
179
133
|
script=$(setup_install_tree "$root")
|
|
180
134
|
local logdir="$root/data/accounts/acct-test/logs"
|
|
181
|
-
local
|
|
182
|
-
|
|
183
|
-
echo "regression-sentinel" > "$logdir/$filename"
|
|
184
|
-
|
|
185
|
-
local stdout stderr rc=0
|
|
186
|
-
stdout=$("$script" "$full_uuid" system 2>/tmp/logs-read-prefix-stderr-5-$$) || rc=$?
|
|
187
|
-
stderr=$(cat /tmp/logs-read-prefix-stderr-5-$$)
|
|
188
|
-
rm -f /tmp/logs-read-prefix-stderr-5-$$
|
|
135
|
+
local sk="11111111-2222-3333-4444-555555555555"
|
|
136
|
+
echo "sentinel-5" > "$logdir/claude-agent-stream-${sk}.log"
|
|
189
137
|
|
|
138
|
+
local rc=0
|
|
139
|
+
local stdout
|
|
140
|
+
stdout=$("$script" "$sk" agent-stream 2>/dev/null) || rc=$?
|
|
190
141
|
cleanup_install_tree "$root"
|
|
191
142
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
echo " stdout missing sentinel: $stdout"; return 1
|
|
195
|
-
fi
|
|
196
|
-
if [[ "$stderr" != *"matched_shape=full"* ]]; then
|
|
197
|
-
echo " stderr missing matched_shape=full: $stderr"; return 1
|
|
198
|
-
fi
|
|
143
|
+
[[ $rc -eq 0 ]] || { echo " expected exit 0, got $rc"; return 1; }
|
|
144
|
+
[[ "$stdout" == *"sentinel-5"* ]] || { echo " missing sentinel"; return 1; }
|
|
199
145
|
return 0
|
|
200
146
|
}
|
|
201
147
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
local root="/tmp/maxy-logs-read-prefix-6-$$"
|
|
148
|
+
case_every_per_session_type() {
|
|
149
|
+
local root="/tmp/maxy-prefix-test-6-$$"
|
|
205
150
|
local script
|
|
206
151
|
script=$(setup_install_tree "$root")
|
|
207
152
|
local logdir="$root/data/accounts/acct-test/logs"
|
|
208
|
-
|
|
209
|
-
echo "
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
fi
|
|
225
|
-
return 0
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
# --- Case 7: preflush ambiguity (Pass 1 zero, Pass 2 multiple) ---
|
|
229
|
-
case_preflush_ambiguity() {
|
|
230
|
-
local root="/tmp/maxy-logs-read-prefix-7-$$"
|
|
231
|
-
local script
|
|
232
|
-
script=$(setup_install_tree "$root")
|
|
233
|
-
local logdir="$root/data/accounts/acct-test/logs"
|
|
234
|
-
echo "P1" > "$logdir/claude-agent-stream-preflush-cccccccc-111.log"
|
|
235
|
-
echo "P2" > "$logdir/claude-agent-stream-preflush-cccccccc-222.log"
|
|
236
|
-
|
|
237
|
-
local stdout stderr rc=0
|
|
238
|
-
stdout=$("$script" "cccccccc" system 2>/tmp/logs-read-prefix-stderr-7-$$) || rc=$?
|
|
239
|
-
stderr=$(cat /tmp/logs-read-prefix-stderr-7-$$)
|
|
240
|
-
rm -f /tmp/logs-read-prefix-stderr-7-$$
|
|
241
|
-
|
|
242
|
-
cleanup_install_tree "$root"
|
|
243
|
-
|
|
244
|
-
if [[ $rc -ne 1 ]]; then echo " expected exit 1, got $rc"; return 1; fi
|
|
245
|
-
if [[ "$stderr" != *"reason=ambiguous-prefix"* ]]; then
|
|
246
|
-
echo " stderr missing reason=ambiguous-prefix: $stderr"; return 1
|
|
247
|
-
fi
|
|
248
|
-
if [[ "$stderr" != *"matches=2"* ]]; then
|
|
249
|
-
echo " stderr missing matches=2: $stderr"; return 1
|
|
250
|
-
fi
|
|
251
|
-
if [[ "$stderr" != *"claude-agent-stream-preflush-cccccccc-111.log"* ]]; then
|
|
252
|
-
echo " stderr missing preflush candidate 1: $stderr"; return 1
|
|
253
|
-
fi
|
|
254
|
-
if [[ "$stderr" != *"claude-agent-stream-preflush-cccccccc-222.log"* ]]; then
|
|
255
|
-
echo " stderr missing preflush candidate 2: $stderr"; return 1
|
|
256
|
-
fi
|
|
257
|
-
return 0
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
# --- Case 8: every per-conversation type resolves under prefix-match ---
|
|
261
|
-
# Parameterised over the four prefix_for_type values:
|
|
262
|
-
# agent-stream → claude-agent-stream-
|
|
263
|
-
# session → sse-events-
|
|
264
|
-
# error → claude-agent-stderr-
|
|
265
|
-
# public → public-agent-stream-
|
|
266
|
-
case_all_types_prefix_match() {
|
|
267
|
-
local root="/tmp/maxy-logs-read-prefix-8-$$"
|
|
268
|
-
local script
|
|
269
|
-
script=$(setup_install_tree "$root")
|
|
270
|
-
local logdir="$root/data/accounts/acct-test/logs"
|
|
271
|
-
local full_uuid="7d49ef21-1234-4567-89ab-cdef01234567"
|
|
272
|
-
|
|
273
|
-
echo "sentinel-agent-stream" > "$logdir/claude-agent-stream-${full_uuid}.log"
|
|
274
|
-
echo "sentinel-session" > "$logdir/sse-events-${full_uuid}.log"
|
|
275
|
-
echo "sentinel-error" > "$logdir/claude-agent-stderr-${full_uuid}.log"
|
|
276
|
-
echo "sentinel-public" > "$logdir/public-agent-stream-${full_uuid}.log"
|
|
277
|
-
|
|
278
|
-
local pairs=(
|
|
279
|
-
"agent-stream|sentinel-agent-stream"
|
|
280
|
-
"session|sentinel-session"
|
|
281
|
-
"error|sentinel-error"
|
|
282
|
-
"public|sentinel-public"
|
|
283
|
-
)
|
|
284
|
-
local fail=0
|
|
285
|
-
local pair t expected_sentinel
|
|
286
|
-
for pair in "${pairs[@]}"; do
|
|
287
|
-
t="${pair%%|*}"
|
|
288
|
-
expected_sentinel="${pair##*|}"
|
|
289
|
-
local stdout stderr rc=0
|
|
290
|
-
stdout=$("$script" "7d49ef21" "$t" 2>/tmp/logs-read-prefix-stderr-8-$$) || rc=$?
|
|
291
|
-
stderr=$(cat /tmp/logs-read-prefix-stderr-8-$$)
|
|
292
|
-
rm -f /tmp/logs-read-prefix-stderr-8-$$
|
|
293
|
-
if [[ $rc -ne 0 ]]; then echo " [$t] expected exit 0, got $rc — stderr: $stderr"; fail=1; continue; fi
|
|
294
|
-
if [[ "$stdout" != *"$expected_sentinel"* ]]; then
|
|
295
|
-
echo " [$t] missing sentinel '$expected_sentinel': $stdout"; fail=1
|
|
296
|
-
fi
|
|
297
|
-
if [[ "$stderr" != *"matched_shape=full"* ]]; then
|
|
298
|
-
echo " [$t] stderr missing matched_shape=full: $stderr"; fail=1
|
|
299
|
-
fi
|
|
153
|
+
local sk="abcdef00-0000-0000-0000-000000000000"
|
|
154
|
+
echo "stream" > "$logdir/claude-agent-stream-${sk}.log"
|
|
155
|
+
echo "err" > "$logdir/claude-agent-stderr-${sk}.log"
|
|
156
|
+
echo "sse" > "$logdir/sse-events-${sk}.log"
|
|
157
|
+
echo "pub" > "$logdir/public-agent-stream-${sk}.log"
|
|
158
|
+
|
|
159
|
+
local ok=1
|
|
160
|
+
for type in agent-stream error session public; do
|
|
161
|
+
local out
|
|
162
|
+
out=$("$script" "abcdef00" "$type" 2>/dev/null) || ok=0
|
|
163
|
+
case "$type" in
|
|
164
|
+
agent-stream) [[ "$out" == *"stream"* ]] || ok=0 ;;
|
|
165
|
+
error) [[ "$out" == *"err"* ]] || ok=0 ;;
|
|
166
|
+
session) [[ "$out" == *"sse"* ]] || ok=0 ;;
|
|
167
|
+
public) [[ "$out" == *"pub"* ]] || ok=0 ;;
|
|
168
|
+
esac
|
|
300
169
|
done
|
|
301
|
-
|
|
302
|
-
cleanup_install_tree "$root"
|
|
303
|
-
[[ $fail -eq 0 ]]
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
# --- Case 9: conv_id with shell metacharacters is rejected ---
|
|
307
|
-
case_metacharacter_rejected() {
|
|
308
|
-
local root="/tmp/maxy-logs-read-prefix-9-$$"
|
|
309
|
-
local script
|
|
310
|
-
script=$(setup_install_tree "$root")
|
|
311
|
-
|
|
312
|
-
local stdout stderr rc=0
|
|
313
|
-
# Use a backslash-escaped wildcard to feed it as a literal argument.
|
|
314
|
-
stdout=$("$script" "ab*cd" system 2>/tmp/logs-read-prefix-stderr-9-$$) || rc=$?
|
|
315
|
-
stderr=$(cat /tmp/logs-read-prefix-stderr-9-$$)
|
|
316
|
-
rm -f /tmp/logs-read-prefix-stderr-9-$$
|
|
317
|
-
|
|
318
170
|
cleanup_install_tree "$root"
|
|
319
|
-
|
|
320
|
-
if [[ $rc -ne 2 ]]; then echo " expected exit 2 (usage), got $rc"; return 1; fi
|
|
321
|
-
if [[ "$stderr" != *"invalid characters"* ]]; then
|
|
322
|
-
echo " stderr missing invalid-characters guard: $stderr"; return 1
|
|
323
|
-
fi
|
|
171
|
+
[[ $ok -eq 1 ]] || { echo " one or more per-session types failed to resolve"; return 1; }
|
|
324
172
|
return 0
|
|
325
173
|
}
|
|
326
174
|
|
|
327
|
-
run_case "8-char prefix resolves full
|
|
328
|
-
run_case "ambiguous prefix
|
|
329
|
-
run_case "more-specific prefix disambiguates"
|
|
330
|
-
run_case "zero matches → file-not-found
|
|
331
|
-
run_case "full 36-char
|
|
332
|
-
run_case "
|
|
333
|
-
run_case "preflush ambiguity refused" case_preflush_ambiguity
|
|
334
|
-
run_case "every per-conversation type prefix-matches" case_all_types_prefix_match
|
|
335
|
-
run_case "shell metacharacters rejected" case_metacharacter_rejected
|
|
175
|
+
run_case "8-char prefix resolves full sessionKey file" case_8char_prefix_resolves_full
|
|
176
|
+
run_case "ambiguous prefix refuses to pick" case_ambiguous_prefix_refuses
|
|
177
|
+
run_case "more-specific prefix disambiguates" case_more_specific_prefix_resolves
|
|
178
|
+
run_case "zero matches → file-not-found" case_zero_match_miss
|
|
179
|
+
run_case "full 36-char sessionKey resolves" case_full_sessionkey_resolves
|
|
180
|
+
run_case "every per-session type resolves" case_every_per_session_type
|
|
336
181
|
|
|
337
182
|
echo ""
|
|
338
183
|
echo "================================================"
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Task 1007 build-time gate: prevents NEW files from gaining a `conversationId` /
|
|
3
|
+
// `sessionId` reference while the 146-file rename slice is in progress.
|
|
4
|
+
//
|
|
5
|
+
// The legacy identifiers are being collapsed into the single canonical
|
|
6
|
+
// `sessionKey`. This sprint shipped the schema-side load-bearing pieces
|
|
7
|
+
// (backfill, new index, boot-time adherence probe) plus the core lib rename.
|
|
8
|
+
// The mechanical tail (routes, MCP plugin parameters, UI props, WhatsApp,
|
|
9
|
+
// tests, specialist templates) is split across follow-up tasks 1009 and 1010.
|
|
10
|
+
//
|
|
11
|
+
// During the transition window, every file currently containing the legacy
|
|
12
|
+
// identifiers is named in `conversation-id-allowlist.txt`. Each follow-up
|
|
13
|
+
// rename PR removes file paths from the allowlist as those files are migrated.
|
|
14
|
+
// This gate fails if:
|
|
15
|
+
// (a) a file NOT in the allowlist contains any of the four legacy tokens, or
|
|
16
|
+
// (b) the allowlist references a path that no longer exists (stale entry).
|
|
17
|
+
//
|
|
18
|
+
// Once tasks 1009 and 1010 land, the allowlist is empty; the gate then enforces
|
|
19
|
+
// the task's outcome contract (zero hits anywhere outside .tasks/archive/).
|
|
20
|
+
|
|
21
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
|
|
22
|
+
import { dirname, join, relative, basename } from 'node:path'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
|
|
25
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
|
|
26
|
+
const REPO_ROOT = join(SCRIPT_DIR, '..', '..')
|
|
27
|
+
const ALLOWLIST_PATH = join(SCRIPT_DIR, 'conversation-id-allowlist.txt')
|
|
28
|
+
|
|
29
|
+
const SCAN_ROOTS = [
|
|
30
|
+
join(REPO_ROOT, 'platform'),
|
|
31
|
+
join(REPO_ROOT, 'packages'),
|
|
32
|
+
join(REPO_ROOT, 'brands'),
|
|
33
|
+
join(REPO_ROOT, '.docs'),
|
|
34
|
+
join(REPO_ROOT, '.claude', 'skills'),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const ALLOWED_EXTS = new Set([
|
|
38
|
+
'.sh', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
39
|
+
'.md', '.cypher', '.json',
|
|
40
|
+
])
|
|
41
|
+
const SKIP_DIR_NAMES = new Set([
|
|
42
|
+
'node_modules', 'dist', 'build', '.next', 'payload',
|
|
43
|
+
])
|
|
44
|
+
const SKIP_PATH_FRAGMENTS = ['.tasks/archive/']
|
|
45
|
+
|
|
46
|
+
// Whole-word matches only — avoids hits inside longer identifiers like
|
|
47
|
+
// `agentSessionId` (Claude Agent SDK per-process session, intentionally kept)
|
|
48
|
+
// and `conversationIdentity` (ConversationArchive hash key, unrelated).
|
|
49
|
+
const LEGACY_TOKEN_RE = /\b(?:conversationId|sessionId|CONVERSATION_ID|SESSION_ID)\b/
|
|
50
|
+
|
|
51
|
+
// Comment-line skip — matches the convention in check-no-task-id-leaks.mjs.
|
|
52
|
+
// Comments that *describe* the legacy field (inside the migration code itself,
|
|
53
|
+
// or inline docstrings explaining the rename) are not new executing code
|
|
54
|
+
// introducing the token, so they don't trip the gate.
|
|
55
|
+
function isCommentLine(line) {
|
|
56
|
+
const trimmed = line.replace(/^\s+/, '')
|
|
57
|
+
if (trimmed.startsWith('#')) return true
|
|
58
|
+
if (trimmed.startsWith('//')) return true
|
|
59
|
+
if (trimmed.startsWith('/*')) return true
|
|
60
|
+
if (trimmed.startsWith('*')) return true
|
|
61
|
+
if (trimmed.startsWith('{/*')) return true
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function loadAllowlist() {
|
|
66
|
+
if (!existsSync(ALLOWLIST_PATH)) return new Set()
|
|
67
|
+
const text = readFileSync(ALLOWLIST_PATH, 'utf-8')
|
|
68
|
+
return new Set(
|
|
69
|
+
text.split('\n')
|
|
70
|
+
.map(l => l.trim())
|
|
71
|
+
.filter(l => l && !l.startsWith('#'))
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function shouldScanFile(filePath) {
|
|
76
|
+
const name = basename(filePath)
|
|
77
|
+
const ext = name.includes('.') ? '.' + name.split('.').pop() : ''
|
|
78
|
+
if (!ALLOWED_EXTS.has(ext)) return false
|
|
79
|
+
const rel = relative(REPO_ROOT, filePath)
|
|
80
|
+
for (const frag of SKIP_PATH_FRAGMENTS) {
|
|
81
|
+
if (rel.includes(frag)) return false
|
|
82
|
+
}
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function* walk(root) {
|
|
87
|
+
let entries
|
|
88
|
+
try { entries = readdirSync(root) } catch { return }
|
|
89
|
+
for (const name of entries) {
|
|
90
|
+
if (SKIP_DIR_NAMES.has(name)) continue
|
|
91
|
+
const full = join(root, name)
|
|
92
|
+
let st
|
|
93
|
+
try { st = statSync(full) } catch { continue }
|
|
94
|
+
if (st.isDirectory()) {
|
|
95
|
+
yield* walk(full)
|
|
96
|
+
} else if (st.isFile()) {
|
|
97
|
+
yield full
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const allowlist = loadAllowlist()
|
|
103
|
+
const newLeaks = []
|
|
104
|
+
const hitFiles = new Set()
|
|
105
|
+
|
|
106
|
+
for (const root of SCAN_ROOTS) {
|
|
107
|
+
if (!existsSync(root)) continue
|
|
108
|
+
for (const file of walk(root)) {
|
|
109
|
+
if (!shouldScanFile(file)) continue
|
|
110
|
+
let content
|
|
111
|
+
try { content = readFileSync(file, 'utf-8') } catch { continue }
|
|
112
|
+
if (!LEGACY_TOKEN_RE.test(content)) continue
|
|
113
|
+
const rel = relative(REPO_ROOT, file)
|
|
114
|
+
const lines = content.split('\n')
|
|
115
|
+
// For non-allowlisted files: only flag hits in non-comment lines. For
|
|
116
|
+
// allowlisted files: any hit counts (so the allowlist accurately tracks
|
|
117
|
+
// the surface still using the legacy name, comments and code alike).
|
|
118
|
+
if (allowlist.has(rel)) {
|
|
119
|
+
hitFiles.add(rel)
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
let firstLine = 0, firstText = ''
|
|
123
|
+
for (let i = 0; i < lines.length; i++) {
|
|
124
|
+
if (!LEGACY_TOKEN_RE.test(lines[i])) continue
|
|
125
|
+
if (isCommentLine(lines[i])) continue
|
|
126
|
+
firstLine = i + 1
|
|
127
|
+
firstText = lines[i].trim().slice(0, 120)
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
if (firstLine > 0) {
|
|
131
|
+
newLeaks.push({ file: rel, line: firstLine, content: firstText })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Stale allowlist entries: files in the allowlist that no longer have a hit
|
|
137
|
+
// (renamed already, or no longer exist) — remove them.
|
|
138
|
+
const staleAllowlistEntries = [...allowlist].filter(p => !hitFiles.has(p))
|
|
139
|
+
|
|
140
|
+
let failed = false
|
|
141
|
+
|
|
142
|
+
if (newLeaks.length > 0) {
|
|
143
|
+
failed = true
|
|
144
|
+
console.error(`check-no-conversation-id-leaks: ${newLeaks.length} NEW file(s) introduced legacy identifier(s):`)
|
|
145
|
+
for (const leak of newLeaks) {
|
|
146
|
+
console.error(` ${leak.file}:${leak.line}: ${leak.content}`)
|
|
147
|
+
}
|
|
148
|
+
console.error('')
|
|
149
|
+
console.error('The single canonical identifier is `sessionKey`. New code must use it.')
|
|
150
|
+
console.error('Legacy tokens: conversationId, sessionId, CONVERSATION_ID, SESSION_ID.')
|
|
151
|
+
console.error('Follow-up tasks 1009 + 1010 are migrating existing files off the allowlist.')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (staleAllowlistEntries.length > 0) {
|
|
155
|
+
failed = true
|
|
156
|
+
console.error('')
|
|
157
|
+
console.error(`check-no-conversation-id-leaks: ${staleAllowlistEntries.length} stale allowlist entry(ies) — remove from platform/scripts/conversation-id-allowlist.txt:`)
|
|
158
|
+
for (const entry of staleAllowlistEntries) {
|
|
159
|
+
console.error(` ${entry}`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (failed) process.exit(1)
|
|
164
|
+
|
|
165
|
+
console.error(`check-no-conversation-id-leaks: ok (${allowlist.size} files in transition allowlist; 0 new leaks)`)
|