@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
package/package.json
CHANGED
package/payload/platform/docs/superpowers/plans/2026-07-27-task-2028-declared-file-write-deny.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Task 2028 — declared-file write deny: implementation plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** The write guard blocks an agent `Write`/`Edit`/`NotebookEdit` to a plugin-declared account-root file, naming the owning plugin, while an operator bucket in the same fence still passes.
|
|
6
|
+
|
|
7
|
+
**Architecture:** The merge emits a third generated region carrying `name<TAB>owning-plugin` inside a ```` ```declared-files ```` fence. The guard parses that fence with the same one-line awk it already uses for `allowed-top-level`, and denies after the top-level check passes. `allowed-top-level` itself is untouched, so the standing reconcile is unaffected.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** bash (PreToolUse/PostToolUse hooks), python3 (schema merge), bash test suites.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- No task numbers or internal refs in any operator-visible string (existing hook rule, stated in both hook headers).
|
|
14
|
+
- The guard never hard-codes a file list; the account's own `SCHEMA.md` is the only source.
|
|
15
|
+
- Fail-open posture is preserved: no `declared-files` fence means nothing to deny.
|
|
16
|
+
- The block log keeps the existing shape: `[fs-guard] blocked path=<rel> reason=<...>`.
|
|
17
|
+
- Spec: `platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
### Task 1: The merge emits the declared-files region
|
|
22
|
+
|
|
23
|
+
**Files:**
|
|
24
|
+
- Modify: `platform/scripts/lib/account-schema-owned-dirs.py` (module constants; `merge()`)
|
|
25
|
+
- Test: `platform/scripts/__tests__/account-schema-owned-dirs.test.sh`
|
|
26
|
+
|
|
27
|
+
**Interfaces:**
|
|
28
|
+
- Produces: a region delimited by `<!-- declared-files:start -->` / `<!-- declared-files:end -->` containing a ```` ```declared-files ```` fence, one `name<TAB>plugin` line per declared file in `resolve()` order. Task 2's guard consumes exactly that fence.
|
|
29
|
+
|
|
30
|
+
- [ ] **Step 1: Write the failing test**
|
|
31
|
+
|
|
32
|
+
Append to `platform/scripts/__tests__/account-schema-owned-dirs.test.sh`, after the existing platform-plugin declarations block:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# --- declared-files fence: one name<TAB>plugin line per declared file -------
|
|
36
|
+
DECL="$(awk '/^```declared-files$/{f=1;next} /^```$/{f=0} f' "$A_MX/SCHEMA.md")"
|
|
37
|
+
assert_grep "$(printf 'wa-channel-bindings.json\tadmin')" "$DECL" "mx-decl-wa"
|
|
38
|
+
assert_grep "$(printf 'agents-disabled.json\tadmin')" "$DECL" "mx-decl-agents-disabled"
|
|
39
|
+
assert_grep "$(printf 'data-portal.json\tcloudflare')" "$DECL" "mx-decl-portal"
|
|
40
|
+
assert_grep "$(printf 'calendar-availability.json\tscheduling')" "$DECL" "mx-decl-availability"
|
|
41
|
+
assert_nogrep "e-sign" "$DECL" "mx-decl-excludes-dirs"
|
|
42
|
+
assert_grep "declared-files:start" "$(cat "$A_MX/SCHEMA.md")" "mx-decl-marker"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- [ ] **Step 2: Run the test to verify it fails**
|
|
46
|
+
|
|
47
|
+
Run: `bash platform/scripts/__tests__/account-schema-owned-dirs.test.sh`
|
|
48
|
+
Expected: FAIL on `mx-decl-wa` and the other new assertions (no such fence yet).
|
|
49
|
+
|
|
50
|
+
- [ ] **Step 3: Write the implementation**
|
|
51
|
+
|
|
52
|
+
In `platform/scripts/lib/account-schema-owned-dirs.py`, beside the existing marker constants:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
DECL_START = "<!-- declared-files:start -->"
|
|
56
|
+
DECL_END = "<!-- declared-files:end -->"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
In `merge()`, add the strip beside the two existing strips:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
text = _strip_region(text, DECL_START, DECL_END)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
and append the region after the ontology region is appended:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
declared = [e for e in owned if e["kind"] == "file"]
|
|
69
|
+
if declared:
|
|
70
|
+
region = [DECL_START, "## Declared files (written by their owning plugin)", "",
|
|
71
|
+
"Each file below is written whole by the plugin named beside it. It is",
|
|
72
|
+
"control-plane state, not operator data, and is not hand-edited: change",
|
|
73
|
+
"it through the owning plugin's own tool. The write guard reads this",
|
|
74
|
+
"block and blocks a write whose first path segment is one of these",
|
|
75
|
+
"names, which the allowed-top-level set above cannot express — that set",
|
|
76
|
+
"lists what may exist at the root, not what may be authored by hand.", "",
|
|
77
|
+
"```declared-files"]
|
|
78
|
+
for e in declared:
|
|
79
|
+
region.append(f"{e['name']}\t{e['plugin']}")
|
|
80
|
+
region.append("```")
|
|
81
|
+
region.append(DECL_END)
|
|
82
|
+
text = text.rstrip("\n") + "\n\n" + "\n".join(region) + "\n"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- [ ] **Step 4: Run the tests to verify they pass**
|
|
86
|
+
|
|
87
|
+
Run: `bash platform/scripts/__tests__/account-schema-owned-dirs.test.sh`
|
|
88
|
+
Expected: PASS, including the pre-existing `idempotent-second-merge` assertion.
|
|
89
|
+
|
|
90
|
+
- [ ] **Step 5: Commit**
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
git add platform/scripts/lib/account-schema-owned-dirs.py platform/scripts/__tests__/account-schema-owned-dirs.test.sh
|
|
94
|
+
git commit -m "feat(2028): merge emits a declared-files fence carrying each file's owning plugin"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### Task 2: The guard denies a write to a declared file
|
|
100
|
+
|
|
101
|
+
**Files:**
|
|
102
|
+
- Modify: `platform/plugins/admin/hooks/fs-schema-guard.sh` (header comment; new check after the top-level check)
|
|
103
|
+
- Test: `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
|
|
104
|
+
|
|
105
|
+
**Interfaces:**
|
|
106
|
+
- Consumes: the ```` ```declared-files ```` fence from Task 1, lines `name<TAB>plugin`.
|
|
107
|
+
- Produces: `[fs-guard] blocked path=<rel> reason=declared-file` on stderr, exit 2.
|
|
108
|
+
|
|
109
|
+
- [ ] **Step 1: Write the failing test**
|
|
110
|
+
|
|
111
|
+
Append to `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`, after the `ACCT_D` setup:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# A third account modelling a merged schema: the eight declared files are in the
|
|
115
|
+
# allowed set (as the merge unions them) AND in the declared-files fence.
|
|
116
|
+
ACCT_F=$(mktemp -d)
|
|
117
|
+
trap 'rm -rf "$ACCT" "$ACCT_D" "$ACCT_F" "$ACCT_L"' EXIT
|
|
118
|
+
DECLARED_FILES="webchat-channel-bindings.json wa-channel-bindings.json telegram-channel-bindings.json canonical-webchat-session.json session-titles.json agents-disabled.json data-portal.json calendar-availability.json"
|
|
119
|
+
seed_declared() { # $1=acct $2=with-fence(yes|no)
|
|
120
|
+
cp "$TEMPLATE" "$1/SCHEMA.md"
|
|
121
|
+
for f in $DECLARED_FILES; do
|
|
122
|
+
awk -v n="$f" '1; /^```allowed-top-level$/{print n}' "$1/SCHEMA.md" > "$1/SCHEMA.md.tmp" && mv "$1/SCHEMA.md.tmp" "$1/SCHEMA.md"
|
|
123
|
+
done
|
|
124
|
+
[ "$2" = "yes" ] || return 0
|
|
125
|
+
{ echo; echo '<!-- declared-files:start -->'; echo '```declared-files'
|
|
126
|
+
for f in $DECLARED_FILES; do
|
|
127
|
+
case "$f" in
|
|
128
|
+
data-portal.json) printf '%s\tcloudflare\n' "$f" ;;
|
|
129
|
+
calendar-availability.json) printf '%s\tscheduling\n' "$f" ;;
|
|
130
|
+
*) printf '%s\tadmin\n' "$f" ;;
|
|
131
|
+
esac
|
|
132
|
+
done
|
|
133
|
+
echo '```'; echo '<!-- declared-files:end -->'; } >> "$1/SCHEMA.md"
|
|
134
|
+
}
|
|
135
|
+
seed_declared "$ACCT_F" yes
|
|
136
|
+
mkdir -p "$ACCT_F/projects/acme"
|
|
137
|
+
|
|
138
|
+
# A fourth account in the pre-2028 state: the names are in the allowed set, but
|
|
139
|
+
# no declared-files fence exists. Pins that the deny comes from the fence and
|
|
140
|
+
# not from a list baked into the hook.
|
|
141
|
+
ACCT_L=$(mktemp -d)
|
|
142
|
+
seed_declared "$ACCT_L" no
|
|
143
|
+
|
|
144
|
+
for f in $DECLARED_FILES; do
|
|
145
|
+
case "$f" in
|
|
146
|
+
data-portal.json) owner=cloudflare ;;
|
|
147
|
+
calendar-availability.json) owner=scheduling ;;
|
|
148
|
+
*) owner=admin ;;
|
|
149
|
+
esac
|
|
150
|
+
run_case "declared block $f" "$(mkenv Write file_path "$f")" 2 "fs-guard. blocked path=$f reason=declared-file" "$ACCT_F"
|
|
151
|
+
run_case "declared owner $f" "$(mkenv Write file_path "$f")" 2 "$owner plugin" "$ACCT_F"
|
|
152
|
+
done
|
|
153
|
+
run_case "operator bucket allow" "$(mkenv Write file_path 'projects/acme/a.txt')" 0 "" "$ACCT_F"
|
|
154
|
+
run_case "declared edit block" "$(mkenv Edit file_path 'wa-channel-bindings.json')" 2 "reason=declared-file" "$ACCT_F"
|
|
155
|
+
run_case "no fence no deny" "$(mkenv Write file_path 'wa-channel-bindings.json')" 0 "" "$ACCT_L"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- [ ] **Step 2: Run the test to verify it fails**
|
|
159
|
+
|
|
160
|
+
Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
|
|
161
|
+
Expected: FAIL on every `declared block`/`declared owner` case with `exit=0, want 2`; `no fence no deny` already passes.
|
|
162
|
+
|
|
163
|
+
- [ ] **Step 3: Write the implementation**
|
|
164
|
+
|
|
165
|
+
In `platform/plugins/admin/hooks/fs-schema-guard.sh`, immediately after the top-level check block:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Declared-file check. A file a plugin declares in its PLUGIN.md is written
|
|
169
|
+
# whole by that plugin, so an agent write to it is blocked even though its name
|
|
170
|
+
# is in the allowed set: that set lists what may exist at the account root, not
|
|
171
|
+
# what may be authored by hand. The set and its owner come from the account's
|
|
172
|
+
# own SCHEMA.md — the hook hard-codes no list. No fence (a brand declaring no
|
|
173
|
+
# files, or a schema predating the block) means nothing to deny, the same
|
|
174
|
+
# fail-open posture as the missing-schema case above.
|
|
175
|
+
DECLARED=""
|
|
176
|
+
if [ -f "$ACCOUNT_DIR/SCHEMA.md" ]; then
|
|
177
|
+
DECLARED=$(awk '/^```declared-files$/{f=1;next} /^```$/{f=0} f' "$ACCOUNT_DIR/SCHEMA.md")
|
|
178
|
+
fi
|
|
179
|
+
if [ -n "$DECLARED" ] && printf '%s\n' "$DECLARED" | awk -F'\t' -v n="$SEG0" '$1==n{found=1} END{exit !found}'; then
|
|
180
|
+
OWNER=$(printf '%s\n' "$DECLARED" | awk -F'\t' -v n="$SEG0" '$1==n{print $2; exit}')
|
|
181
|
+
[ -z "$OWNER" ] && OWNER="owning"
|
|
182
|
+
echo "[fs-guard] blocked path=$REL reason=declared-file" >&2
|
|
183
|
+
echo "Blocked: '$SEG0' is written whole by the $OWNER plugin. It is control-plane state, not operator data, and a hand-written copy is overwritten by its owner's next write. Change it through the $OWNER plugin's own tool." >&2
|
|
184
|
+
exit 2
|
|
185
|
+
fi
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Update the header comment's reason list to `reason=<top-level|declared-file|over-deep|bad-name>` and add one line to the enforcement summary: a target whose first segment is a declared file is blocked.
|
|
189
|
+
|
|
190
|
+
- [ ] **Step 4: Run the tests to verify they pass**
|
|
191
|
+
|
|
192
|
+
Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
|
|
193
|
+
Expected: PASS on all cases, including the pre-existing ten.
|
|
194
|
+
|
|
195
|
+
- [ ] **Step 5: Mutation check**
|
|
196
|
+
|
|
197
|
+
Comment out the `exit 2` inside the new block, re-run the suite, confirm the 16 declared cases fail, then restore.
|
|
198
|
+
|
|
199
|
+
- [ ] **Step 6: Commit**
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
git add platform/plugins/admin/hooks/fs-schema-guard.sh platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh
|
|
203
|
+
git commit -m "feat(2028): the write guard denies a write to a plugin-declared file"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
### Task 3: The Bash post-hook decision is stated and pinned
|
|
209
|
+
|
|
210
|
+
**Files:**
|
|
211
|
+
- Modify: `platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh` (header comment only)
|
|
212
|
+
- Test: `platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh`
|
|
213
|
+
|
|
214
|
+
**Interfaces:**
|
|
215
|
+
- Consumes: nothing new. The behaviour under test is the existing allowed-set diff.
|
|
216
|
+
|
|
217
|
+
- [ ] **Step 1: Write the failing test**
|
|
218
|
+
|
|
219
|
+
Append to `platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh`:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
# --- 10. Bash creating a declared file -> silent, by decision ---------------
|
|
223
|
+
# A declared file is in the allowed set, so the diff never flags it. Kept
|
|
224
|
+
# deliberately: this hook sees only names that appeared during the command and
|
|
225
|
+
# cannot tell a hand-written copy from the owner's own script creating it.
|
|
226
|
+
D=$(new_acct)
|
|
227
|
+
awk '1; /^```allowed-top-level$/{print "wa-channel-bindings.json"}' "$D/SCHEMA.md" > "$D/SCHEMA.md.tmp" && mv "$D/SCHEMA.md.tmp" "$D/SCHEMA.md"
|
|
228
|
+
run_pre "$D" "$(env_json PreToolUse Bash "$SID")" "$ef"
|
|
229
|
+
printf '{}' > "$D/wa-channel-bindings.json"
|
|
230
|
+
run_post "$D" "$(env_json PostToolUse Bash "$SID")" "$ef"
|
|
231
|
+
check "post silent on declared file" 0 $? "" "$(cat "$ef")"
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
- [ ] **Step 2: Run the test**
|
|
235
|
+
|
|
236
|
+
Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh`
|
|
237
|
+
Expected: PASS. This test pins existing behaviour rather than driving a change; it fails only if a future edit makes the post hook flag declared files.
|
|
238
|
+
|
|
239
|
+
- [ ] **Step 3: State the decision in the hook**
|
|
240
|
+
|
|
241
|
+
Add to the header comment of `platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh`:
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
# A declared file (a name a plugin declares as its own in PLUGIN.md) is in the
|
|
245
|
+
# allowed set, so this hook never flags one. That silence is a decision, not an
|
|
246
|
+
# accident: the diff sees only names that appeared during this command, so it
|
|
247
|
+
# cannot tell the agent hand-writing the file from the agent running the owner's
|
|
248
|
+
# own script (the portal index push, the availability publish), and feedback
|
|
249
|
+
# would fire on the legitimate path. The Write/Edit guard blocks the hand-write
|
|
250
|
+
# it can actually identify; this hook stays quiet. A test pins the silence.
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
- [ ] **Step 4: Re-run both hook suites**
|
|
254
|
+
|
|
255
|
+
Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh && bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
|
|
256
|
+
Expected: PASS on both.
|
|
257
|
+
|
|
258
|
+
- [ ] **Step 5: Commit**
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
git add platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh
|
|
262
|
+
git commit -m "docs(2028): state and pin the Bash post-hook's silence on declared files"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### Task 4: End-to-end against the real brand tree
|
|
268
|
+
|
|
269
|
+
**Files:** none modified. Verification only.
|
|
270
|
+
|
|
271
|
+
- [ ] **Step 1: Merge a real schema and read the fence**
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
ACCT=$(mktemp -d) && cp platform/templates/account-schema/SCHEMA.md "$ACCT/SCHEMA.md"
|
|
275
|
+
PROJECT_DIR="$PWD/platform" python3 platform/scripts/lib/account-schema-owned-dirs.py merge "$ACCT"
|
|
276
|
+
awk '/^```declared-files$/{f=1;next} /^```$/{f=0} f' "$ACCT/SCHEMA.md"
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Expected: eight `name<TAB>plugin` lines.
|
|
280
|
+
|
|
281
|
+
- [ ] **Step 2: Drive the real hook against that merged account**
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
( cd "$ACCT" && printf '{"tool_name":"Write","tool_input":{"file_path":"wa-channel-bindings.json"}}' \
|
|
285
|
+
| bash "$OLDPWD/platform/plugins/admin/hooks/fs-schema-guard.sh"; echo "exit=$?" )
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Expected: `reason=declared-file` on stderr and `exit=2`, against the same envelope that returned exit 0 before this change.
|
|
289
|
+
|
|
290
|
+
- [ ] **Step 3: Confirm the reconcile still names no declared file as a stray**
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
PROJECT_DIR="$PWD/platform" python3 platform/scripts/lib/account-schema-owned-dirs.py reconcile "$ACCT" | grep ownedFile
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Expected: `present=true` on every line.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Self-review
|
|
301
|
+
|
|
302
|
+
- Spec coverage: emission (Task 1), deny (Task 2), Bash decision (Task 3), the spec's testing section (Tasks 1-3 plus the mutation check in Task 2 and the end-to-end in Task 4).
|
|
303
|
+
- One spec sentence is tightened here: the "no fence" case uses an account whose allowed set already carries the names, because under the pristine template those names block as `top-level` and would not prove anything about the new branch.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Task 2028 — the write guard tells a declared file from an operator bucket
|
|
2
|
+
|
|
3
|
+
Design, 2026-07-27. Lane: Platform / account schema · write guard.
|
|
4
|
+
|
|
5
|
+
## The fault
|
|
6
|
+
|
|
7
|
+
`fs-schema-guard.sh` is the PreToolUse guard for `Write`, `Edit` and `NotebookEdit`.
|
|
8
|
+
It parses the account's ```` ```allowed-top-level ```` fence and blocks a write whose
|
|
9
|
+
first path segment is absent from it. The standing reconcile parses the same fence
|
|
10
|
+
to decide what is not a stray. One list, two meanings.
|
|
11
|
+
|
|
12
|
+
Task 1902 declared eight control-plane files so the reconcile would stop naming
|
|
13
|
+
them. Because names union into that one fence, the guard began allowing
|
|
14
|
+
agent writes to them: measured, the same envelope returns exit 2 against the
|
|
15
|
+
pre-merge schema and exit 0 against the merged one for `wa-channel-bindings.json`,
|
|
16
|
+
`telegram-channel-bindings.json`, `webchat-channel-bindings.json`,
|
|
17
|
+
`canonical-webchat-session.json`, `session-titles.json`, `agents-disabled.json`,
|
|
18
|
+
`calendar-availability.json` and `data-portal.json`.
|
|
19
|
+
|
|
20
|
+
The fence already permits `account.json`, `SCHEMA.md`, `secrets/` and `.claude/` on
|
|
21
|
+
the untouched shipped template, so it is a layout guard, not a trust boundary. This
|
|
22
|
+
task makes the guard enforce the intent the generated region already states: a
|
|
23
|
+
declared file "is written whole by its owner. Neither is operator data."
|
|
24
|
+
|
|
25
|
+
## Design
|
|
26
|
+
|
|
27
|
+
### 1. A second fence, emitted by the merge
|
|
28
|
+
|
|
29
|
+
`account-schema-owned-dirs.py` `merge()` gains a third generated region, written
|
|
30
|
+
with the same strip-then-append shape as the two that exist, so a re-merge is
|
|
31
|
+
idempotent:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
<!-- declared-files:start -->
|
|
35
|
+
## Declared files (written by platform code, owned by a plugin)
|
|
36
|
+
|
|
37
|
+
...one paragraph of prose...
|
|
38
|
+
|
|
39
|
+
```declared-files
|
|
40
|
+
webchat-channel-bindings.json admin
|
|
41
|
+
wa-channel-bindings.json admin
|
|
42
|
+
data-portal.json cloudflare
|
|
43
|
+
```
|
|
44
|
+
<!-- declared-files:end -->
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
One line per platform-written declared file, in `resolve()` order, as
|
|
48
|
+
`name<TAB>owning-plugin`. The region is emitted only when at least one such file
|
|
49
|
+
is declared.
|
|
50
|
+
|
|
51
|
+
**Two of the eight declared files are agent-written, and are excluded.** The
|
|
52
|
+
review established that `calendar-availability.json` and `data-portal.json` have
|
|
53
|
+
no writer in platform code: `publish-availability.ts:151` and
|
|
54
|
+
`portal-index-push.mjs:52,380` only read them, and the `calendar-site` and
|
|
55
|
+
`data-portal` skills instruct the agent to author them with `Write`
|
|
56
|
+
("there is no separate editor"). Denying them would break the only path that
|
|
57
|
+
creates them. A file declaration may therefore carry `"writtenBy": "agent"`,
|
|
58
|
+
which keeps the name in `allowed-top-level` (so the reconcile still never calls
|
|
59
|
+
it a stray) and out of the declared-files fence (so the guard never denies it).
|
|
60
|
+
The two declarations gain that key, and their descriptions are corrected: both
|
|
61
|
+
named a writer that only reads.
|
|
62
|
+
|
|
63
|
+
The owner column names the *declaring* plugin, which is where the declaration
|
|
64
|
+
lives rather than necessarily the module that writes the file — Task 1902 moved
|
|
65
|
+
declarations onto plugins no brand excludes for reach, so a shared service may
|
|
66
|
+
write a file declared on `admin`. The block message therefore points at the
|
|
67
|
+
owning code and does not promise a plugin tool. `allowed-top-level` is
|
|
68
|
+
not touched, so the reconcile keeps reading exactly what it reads today, and the two
|
|
69
|
+
existing region readers (`platform/lib/account-schema-regions`, the cloudflare
|
|
70
|
+
`schema-exposed-dirs.mjs` copy) both anchor on their own markers and on the
|
|
71
|
+
`allowed-top-level` fence by name, so an appended region is invisible to them.
|
|
72
|
+
|
|
73
|
+
The owning-plugin column exists because the block message has to name the owner,
|
|
74
|
+
and the descriptive region carries the plugin's own description, not its name.
|
|
75
|
+
|
|
76
|
+
### 2. The guard denies
|
|
77
|
+
|
|
78
|
+
`fs-schema-guard.sh` parses the new fence with one awk expression, the same shape it
|
|
79
|
+
already uses for the allowed set. After the top-level check passes and before the
|
|
80
|
+
depth check, a target whose first path segment matches a declared name is blocked:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
[fs-guard] blocked path=<rel> reason=declared-file
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
plus an operator-visible line naming the owning plugin and pointing the agent at it.
|
|
87
|
+
An account whose SCHEMA.md carries no `declared-files` fence denies nothing, which is
|
|
88
|
+
the same fail-open posture the guard already takes on a missing or empty schema.
|
|
89
|
+
|
|
90
|
+
The match is on the first path segment, not on the whole relative path, so a write to
|
|
91
|
+
`wa-channel-bindings.json/anything` is blocked for the same reason.
|
|
92
|
+
|
|
93
|
+
### 3. The Bash post-hook stays quiet
|
|
94
|
+
|
|
95
|
+
A Bash command that creates a declared file emits no stray feedback. That is the
|
|
96
|
+
behaviour Task 1902 produced and it is kept deliberately, not inherited: the post
|
|
97
|
+
hook fires only on a name that appeared during the command, and a declared file
|
|
98
|
+
already exists on any live account, so a Bash edit to one is invisible there
|
|
99
|
+
regardless of the allowed set. A signal that catches only the first creation and
|
|
100
|
+
misses every subsequent edit is not a boundary. The decision is written into the
|
|
101
|
+
hook's header comment and pinned by a test, so a future edit has to argue with it
|
|
102
|
+
rather than flip it silently.
|
|
103
|
+
|
|
104
|
+
## Testing
|
|
105
|
+
|
|
106
|
+
- `fs-schema-guard.test.sh`: one case per declared file asserting exit 2 with
|
|
107
|
+
`reason=declared-file` and the owner name in the message; an operator bucket write
|
|
108
|
+
under the same schema asserting exit 0; a declared name under an account that
|
|
109
|
+
carries it in `allowed-top-level` but has no `declared-files` fence asserting
|
|
110
|
+
exit 0, which pins the deny to the fence rather than to a list baked into the
|
|
111
|
+
hook.
|
|
112
|
+
- `fs-schema-guard-bash.test.sh`: a Bash command that creates a declared file
|
|
113
|
+
asserting exit 0 and no `[fs-guard-bash]` line.
|
|
114
|
+
- `account-schema-owned-dirs.test.sh`: the merge emits the fence with
|
|
115
|
+
`name<TAB>plugin` for each declared file, emits nothing when no file is declared,
|
|
116
|
+
and a second merge leaves the file byte-identical.
|
|
117
|
+
- Mutation check: remove the guard's deny branch and confirm the per-file cases fail.
|
|
118
|
+
- The reconcile's own suite still reports no declared file as a stray.
|
|
119
|
+
|
|
120
|
+
## Out of scope
|
|
121
|
+
|
|
122
|
+
- The fence's pre-existing permissiveness for `account.json`, `secrets/` and
|
|
123
|
+
`.claude/`. It predates Task 1902 and is a separate decision, filed as
|
|
124
|
+
`.tasks/pending/2048-the-allowed-fence-permits-agent-writes-to-the-account-s-own-machinery.md`.
|
|
125
|
+
- The reconcile. It reads the fence correctly and needs no change.
|
|
126
|
+
- Adding or removing declarations. Task 1902 settled the set.
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* ontology roots: - `jobs/` - one folder per Job record. (ASCII hyphen)
|
|
16
16
|
* plugin-owned: - `pages/` — Owned by the … plugin. (em dash)
|
|
17
17
|
*
|
|
18
|
+
* merge() writes a fourth structure, the ```declared-files fence, which is not
|
|
19
|
+
* parsed here: its only reader is the write guard (fs-schema-guard.sh), which
|
|
20
|
+
* needs the declared-FILE set apart from the bucket set. Both consumers of this
|
|
21
|
+
* module group and audit directories, so the fence is deliberately out of scope.
|
|
22
|
+
*
|
|
18
23
|
* .docs/data-portal-folder-index.md records that parsing the ontology region on
|
|
19
24
|
* the wrong separator matches nothing and reads as "this account has no
|
|
20
25
|
* deliverables". This parser sidesteps that failure entirely by anchoring on the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,MAAM,EAEtC,CAAA;AAiBF,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,iEAAiE;IACjE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,gEAAgE;IAChE,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB;;;;;;;OAOG;IACH,MAAM,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,MAAM,EAAE,IAAI,GAAG,eAAe,GAAG,cAAc,CAAA;CAChD;AAiCD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,aAAa,CAazE"}
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* ontology roots: - `jobs/` - one folder per Job record. (ASCII hyphen)
|
|
17
17
|
* plugin-owned: - `pages/` — Owned by the … plugin. (em dash)
|
|
18
18
|
*
|
|
19
|
+
* merge() writes a fourth structure, the ```declared-files fence, which is not
|
|
20
|
+
* parsed here: its only reader is the write guard (fs-schema-guard.sh), which
|
|
21
|
+
* needs the declared-FILE set apart from the bucket set. Both consumers of this
|
|
22
|
+
* module group and audit directories, so the fence is deliberately out of scope.
|
|
23
|
+
*
|
|
19
24
|
* .docs/data-portal-folder-index.md records that parsing the ontology region on
|
|
20
25
|
* the wrong separator matches nothing and reads as "this account has no
|
|
21
26
|
* deliverables". This parser sidesteps that failure entirely by anchoring on the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* ontology roots: - `jobs/` - one folder per Job record. (ASCII hyphen)
|
|
16
16
|
* plugin-owned: - `pages/` — Owned by the … plugin. (em dash)
|
|
17
17
|
*
|
|
18
|
+
* merge() writes a fourth structure, the ```declared-files fence, which is not
|
|
19
|
+
* parsed here: its only reader is the write guard (fs-schema-guard.sh), which
|
|
20
|
+
* needs the declared-FILE set apart from the bucket set. Both consumers of this
|
|
21
|
+
* module group and audit directories, so the fence is deliberately out of scope.
|
|
22
|
+
*
|
|
18
23
|
* .docs/data-portal-folder-index.md records that parsing the ontology region on
|
|
19
24
|
* the wrong separator matches nothing and reads as "this account has no
|
|
20
25
|
* deliverables". This parser sidesteps that failure entirely by anchoring on the
|
|
@@ -154,6 +154,20 @@ mkdir -p "$K/contacts/acme"; : > "$K/contacts/.DS_Store"
|
|
|
154
154
|
run_post "$K" "$(env_json PostToolUse Bash "$SID")" "$ef"
|
|
155
155
|
check "post ignores dot-prefixed new-bucket child" 0 $? "" "$(cat "$ef")"
|
|
156
156
|
|
|
157
|
+
# --- 14. Bash creating a plugin-declared file -> silent, by decision ---------
|
|
158
|
+
# A declared file is in the allowed set, so the diff never flags it. That is
|
|
159
|
+
# deliberate, not inherited: this hook fires only on a name that APPEARED during
|
|
160
|
+
# the command, and a declared file already exists on any live account, so a Bash
|
|
161
|
+
# edit to one is invisible here regardless. Catching only the first creation and
|
|
162
|
+
# missing every edit is not a boundary. The Write/Edit guard blocks the writes it
|
|
163
|
+
# can actually identify; this hook stays quiet. This case pins the silence.
|
|
164
|
+
L=$(new_acct)
|
|
165
|
+
awk '1; /^```allowed-top-level$/{print "wa-channel-bindings.json"}' "$L/SCHEMA.md" > "$L/SCHEMA.md.tmp" && mv "$L/SCHEMA.md.tmp" "$L/SCHEMA.md"
|
|
166
|
+
run_pre "$L" "$(env_json PreToolUse Bash "$SID")" "$ef"
|
|
167
|
+
printf '{}' > "$L/wa-channel-bindings.json"
|
|
168
|
+
run_post "$L" "$(env_json PostToolUse Bash "$SID")" "$ef"
|
|
169
|
+
check "post silent on declared file" 0 $? "" "$(cat "$ef")"
|
|
170
|
+
|
|
157
171
|
rm -f "$ef"
|
|
158
172
|
echo "----- $PASS passed, $FAIL failed -----"
|
|
159
173
|
[ $FAIL -eq 0 ]
|
|
@@ -87,6 +87,44 @@ run_case "domain bucket deep allow" "$(mkenv Write file_path 'jobs/5-oaktree/Quo
|
|
|
87
87
|
run_case "domain bucket file allow" "$(mkenv Write file_path 'jobs/5-oaktree/notes.md')" 0 "" "$ACCT_D"
|
|
88
88
|
run_case "undeclared still blocks" "$(mkenv Write file_path 'invoices/x.pdf')" 2 "reason=top-level" "$ACCT_D"
|
|
89
89
|
|
|
90
|
+
# Declared files: a plugin-declared account-root file is written whole by its
|
|
91
|
+
# owner, so an agent write to it blocks even though the merge unions its name
|
|
92
|
+
# into the allowed set. $ACCT_F models a merged schema (names in the allowed set
|
|
93
|
+
# AND in the declared-files fence); $ACCT_L models the state before this guard
|
|
94
|
+
# existed (names in the allowed set, no fence), which pins the deny to the fence
|
|
95
|
+
# rather than to a list baked into the hook.
|
|
96
|
+
DECLARED_FILES="webchat-channel-bindings.json wa-channel-bindings.json telegram-channel-bindings.json canonical-webchat-session.json session-titles.json agents-disabled.json"
|
|
97
|
+
# Declared but AGENT-written: the calendar-site and data-portal skills author
|
|
98
|
+
# these two with Write and there is no other writer, so they are in the allowed
|
|
99
|
+
# set (and out of the reconcile's stray report) but never in the deny fence.
|
|
100
|
+
AGENT_WRITTEN_FILES="data-portal.json calendar-availability.json"
|
|
101
|
+
owner_of() { echo admin; }
|
|
102
|
+
seed_declared() { # $1=acct $2=emit fence (yes|no)
|
|
103
|
+
cp "$TEMPLATE" "$1/SCHEMA.md"
|
|
104
|
+
for f in $DECLARED_FILES $AGENT_WRITTEN_FILES; do
|
|
105
|
+
awk -v n="$f" '1; /^```allowed-top-level$/{print n}' "$1/SCHEMA.md" > "$1/SCHEMA.md.tmp" && mv "$1/SCHEMA.md.tmp" "$1/SCHEMA.md"
|
|
106
|
+
done
|
|
107
|
+
[ "$2" = "yes" ] || return 0
|
|
108
|
+
{ echo; echo '<!-- declared-files:start -->'; echo '```declared-files'
|
|
109
|
+
for f in $DECLARED_FILES; do printf '%s\t%s\n' "$f" "$(owner_of "$f")"; done
|
|
110
|
+
echo '```'; echo '<!-- declared-files:end -->'; } >> "$1/SCHEMA.md"
|
|
111
|
+
}
|
|
112
|
+
ACCT_F=$(mktemp -d); ACCT_L=$(mktemp -d)
|
|
113
|
+
trap 'rm -rf "$ACCT" "$ACCT_D" "$ACCT_F" "$ACCT_L"' EXIT
|
|
114
|
+
seed_declared "$ACCT_F" yes
|
|
115
|
+
seed_declared "$ACCT_L" no
|
|
116
|
+
mkdir -p "$ACCT_F/projects/acme"
|
|
117
|
+
for f in $DECLARED_FILES; do
|
|
118
|
+
run_case "declared block $f" "$(mkenv Write file_path "$f")" 2 "fs-guard. blocked path=$f reason=declared-file" "$ACCT_F"
|
|
119
|
+
run_case "declared owner $f" "$(mkenv Write file_path "$f")" 2 "$(owner_of "$f") plugin" "$ACCT_F"
|
|
120
|
+
done
|
|
121
|
+
for f in $AGENT_WRITTEN_FILES; do
|
|
122
|
+
run_case "agent-written allow $f" "$(mkenv Write file_path "$f")" 0 "" "$ACCT_F"
|
|
123
|
+
done
|
|
124
|
+
run_case "declared edit blocks" "$(mkenv Edit file_path 'wa-channel-bindings.json')" 2 "reason=declared-file" "$ACCT_F"
|
|
125
|
+
run_case "operator bucket allow" "$(mkenv Write file_path 'projects/acme/a.txt')" 0 "" "$ACCT_F"
|
|
126
|
+
run_case "no fence no deny" "$(mkenv Write file_path 'wa-channel-bindings.json')" 0 "" "$ACCT_L"
|
|
127
|
+
|
|
90
128
|
# Allowed-set parse == documented set.
|
|
91
129
|
EXPECT="projects contacts documents url-get output generated extracted uploads agents specialists sites public cache secrets state logs tmp .quarantine SCHEMA.md account.json AGENTS.md .claude .git"
|
|
92
130
|
GOT=$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCT/SCHEMA.md" | tr '\n' ' ' | sed 's/ *$//')
|
|
@@ -17,6 +17,17 @@
|
|
|
17
17
|
# Diffing against the pre-snapshot (not flagging every non-allowed entry) is what
|
|
18
18
|
# keeps the hook from firing on the legacy backlog on every unrelated Bash call.
|
|
19
19
|
#
|
|
20
|
+
# A plugin-declared file (a name a plugin claims as its own in its PLUGIN.md) is
|
|
21
|
+
# in the allowed set, so this hook never flags one. That silence is a decision,
|
|
22
|
+
# not an accident. This hook fires only on a name that APPEARED during the
|
|
23
|
+
# command: a declared file already exists on any live account, so a Bash edit to
|
|
24
|
+
# one is invisible here no matter what the allowed set says. A signal that catches
|
|
25
|
+
# only the first creation and misses every subsequent edit is not a boundary, and
|
|
26
|
+
# paying for it in feedback noise buys nothing. The Write/Edit guard
|
|
27
|
+
# (fs-schema-guard.sh) blocks the writes it can actually identify, on the
|
|
28
|
+
# declared-files fence; this hook stays quiet, and a case in the test suite pins
|
|
29
|
+
# the silence.
|
|
30
|
+
#
|
|
20
31
|
# Exit codes: 0 = allow, 2 = feedback to the agent (stderr shown). Fail open
|
|
21
32
|
# (exit 0) when the snapshot is missing or the allowed set is empty. Every block
|
|
22
33
|
# logs: [fs-guard-bash] stray path=<name> reason=<top-level|bad-name>
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
# - the target's first path segment must be in the allowed top-level set
|
|
6
6
|
# (parsed from the fenced ```allowed-top-level block of the account's
|
|
7
7
|
# SCHEMA.md — our own structured data, not CLI prose);
|
|
8
|
+
# - a target whose first path segment is a plugin-declared file (parsed from
|
|
9
|
+
# the fenced ```declared-files block of the same SCHEMA.md) is blocked even
|
|
10
|
+
# though that name is in the allowed set: the allowed set is a layout list,
|
|
11
|
+
# and a declared file is written whole by its owning plugin;
|
|
8
12
|
# - a target under an operator-data bucket (projects/ contacts/) may be at
|
|
9
13
|
# most <bucket>/<entity>/<file> deep; documents/ may be at most
|
|
10
14
|
# <bucket>/<folder>/<file> deep. Tool-owned dirs pass at any depth.
|
|
@@ -14,7 +18,7 @@
|
|
|
14
18
|
#
|
|
15
19
|
# Exit codes: 0 = allow, 2 = block (stderr shown to the agent). Fail closed when
|
|
16
20
|
# the tool call cannot be inspected (tty or empty stdin). Every block logs
|
|
17
|
-
# [fs-guard] blocked path=<rel> reason=<top-level|over-deep|bad-name>
|
|
21
|
+
# [fs-guard] blocked path=<rel> reason=<top-level|declared-file|over-deep|bad-name>
|
|
18
22
|
# No task numbers / internal refs in any operator-visible string.
|
|
19
23
|
|
|
20
24
|
set -uo pipefail
|
|
@@ -84,6 +88,26 @@ if ! printf '%s\n' "$ALLOWED" | grep -qxF "$SEG0"; then
|
|
|
84
88
|
exit 2
|
|
85
89
|
fi
|
|
86
90
|
|
|
91
|
+
# Declared-file check. A file declared in the fence is written whole by platform
|
|
92
|
+
# code, so an agent write to it is blocked even though the name is in the allowed
|
|
93
|
+
# set: that set says which names may exist at the account root, not which an
|
|
94
|
+
# agent may author. The owner column names the declaring plugin, which is where
|
|
95
|
+
# the declaration lives, not necessarily the module that does the writing (a
|
|
96
|
+
# shared service may write a file declared on the plugin that no brand excludes),
|
|
97
|
+
# so the message points at the owning code rather than promising a plugin tool.
|
|
98
|
+
# A file the merge marks as agent-written is not in this fence at all. The set
|
|
99
|
+
# comes from the account's own SCHEMA.md, so the hook hard-codes no list. No
|
|
100
|
+
# fence (a brand declaring no owned file, or a schema written before the fence
|
|
101
|
+
# existed) means nothing to deny — the same fail-open posture as the
|
|
102
|
+
# missing-schema case above.
|
|
103
|
+
DECLARED=$(awk '/^```declared-files$/{f=1;next} /^```$/{f=0} f' "$ACCOUNT_DIR/SCHEMA.md")
|
|
104
|
+
if [ -n "$DECLARED" ] && printf '%s\n' "$DECLARED" | awk -F'\t' -v n="$SEG0" '$1==n{found=1} END{exit !found}'; then
|
|
105
|
+
OWNER=$(printf '%s\n' "$DECLARED" | awk -F'\t' -v n="$SEG0" '$1==n{print $2; exit}')
|
|
106
|
+
echo "[fs-guard] blocked path=$REL reason=declared-file" >&2
|
|
107
|
+
echo "Blocked: '$SEG0' is a declared file owned by the $OWNER plugin. The platform writes it whole, so it is control-plane state, not operator data, and a hand-written copy is overwritten by the next platform write. Change it through the code that owns it, never by hand." >&2
|
|
108
|
+
exit 2
|
|
109
|
+
fi
|
|
110
|
+
|
|
87
111
|
# Over-deep check for operator-data buckets. Count path segments.
|
|
88
112
|
depth=$(printf '%s' "$REL" | awk -F/ '{print NF}')
|
|
89
113
|
case "$SEG0" in
|
|
@@ -9,6 +9,12 @@ Invoked by the admin agent directly.
|
|
|
9
9
|
|
|
10
10
|
This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
|
|
11
11
|
|
|
12
|
+
## 2026-07-27 (0.1.513)
|
|
13
|
+
|
|
14
|
+
- Fixed sign-in for sub-accounts: the browser session bridge now checks that you belong to the account you asked for, instead of only ever accepting the first one it found.
|
|
15
|
+
- WhatsApp account checks now use the account's real identifier rather than a folder name, so conversations are attributed to the right account.
|
|
16
|
+
- The saved session-title cache is now pruned as the sessions list is walked, so it no longer grows without limit.
|
|
17
|
+
|
|
12
18
|
## 2026-07-27 (0.1.512)
|
|
13
19
|
|
|
14
20
|
- Signed PDFs keep their clickable links and selectable text. The e-sign stamp no longer falls back to flattening each page into an image, which had been discarding every link in the document.
|