@rubytech/create-sitedesk-code 0.1.513 → 0.1.514
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-2052-account-machinery-write-fence.md +250 -0
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md +2 -1
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2052-account-machinery-write-fence-design.md +150 -0
- package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh +41 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh +8 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard.sh +40 -1
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
- package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +1 -1
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +1 -1
- package/payload/platform/templates/account-schema/SCHEMA.md +17 -0
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-bksN0XPe.js → AdminLoginScreens-Cs1jSnOF.js} +1 -1
- package/payload/server/public/assets/AdminLoginScreens-Cs1jSnOF.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-Cs1jSnOF.js.gz +0 -0
- package/payload/server/public/assets/{AdminShell-BMPEWNZk.js → AdminShell-DkrURZnF.js} +1 -1
- package/payload/server/public/assets/AdminShell-DkrURZnF.js.br +0 -0
- package/payload/server/public/assets/AdminShell-DkrURZnF.js.gz +0 -0
- package/payload/server/public/assets/{activity-ByWYF5dC.js → activity-BgZISMuE.js} +1 -1
- package/payload/server/public/assets/activity-BgZISMuE.js.br +0 -0
- package/payload/server/public/assets/activity-BgZISMuE.js.gz +0 -0
- package/payload/server/public/assets/{admin-6QN-k3zQ.js → admin-DjFysc3-.js} +1 -1
- package/payload/server/public/assets/admin-DjFysc3-.js.br +0 -0
- package/payload/server/public/assets/admin-DjFysc3-.js.gz +0 -0
- package/payload/server/public/assets/{agents-C0rTfrYd.js → agents-Bx-MSZW6.js} +1 -1
- package/payload/server/public/assets/agents-Bx-MSZW6.js.br +0 -0
- package/payload/server/public/assets/agents-Bx-MSZW6.js.gz +0 -0
- package/payload/server/public/assets/{browser-4tTaem8N.js → browser-1I7syhHu.js} +1 -1
- package/payload/server/public/assets/browser-1I7syhHu.js.br +0 -0
- package/payload/server/public/assets/browser-1I7syhHu.js.gz +0 -0
- package/payload/server/public/assets/{calendar-BEjnkKG1.js → calendar-D_04JIo1.js} +1 -1
- package/payload/server/public/assets/calendar-D_04JIo1.js.br +0 -0
- package/payload/server/public/assets/calendar-D_04JIo1.js.gz +0 -0
- package/payload/server/public/assets/chat-BFH3FM5a.js +1 -0
- package/payload/server/public/assets/chat-BFH3FM5a.js.br +0 -0
- package/payload/server/public/assets/chat-BFH3FM5a.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-Ux9-qLkk.js +1 -0
- package/payload/server/public/assets/chevron-right-BwIuCfyG.js +1 -0
- package/payload/server/public/assets/chevron-right-BwIuCfyG.js.br +0 -0
- package/payload/server/public/assets/clock-DhJdZRhC.js +1 -0
- package/payload/server/public/assets/clock-DhJdZRhC.js.br +0 -0
- package/payload/server/public/assets/data-ChY_uSIk.js +1 -0
- package/payload/server/public/assets/data-ChY_uSIk.js.br +0 -0
- package/payload/server/public/assets/data-ChY_uSIk.js.gz +0 -0
- package/payload/server/public/assets/{file-text-DMKw3nIk.js → file-text-DkvKqHMx.js} +1 -1
- package/payload/server/public/assets/file-text-DkvKqHMx.js.br +0 -0
- package/payload/server/public/assets/file-text-DkvKqHMx.js.gz +0 -0
- package/payload/server/public/assets/{graph-C3vW8lvw.js → graph-CCZhrwZX.js} +1 -1
- package/payload/server/public/assets/graph-CCZhrwZX.js.br +0 -0
- package/payload/server/public/assets/graph-CCZhrwZX.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-BZgd0L6n.js → graph-labels-BJ7puQkX.js} +1 -1
- package/payload/server/public/assets/graph-labels-BJ7puQkX.js.br +0 -0
- package/payload/server/public/assets/graph-labels-BJ7puQkX.js.gz +0 -0
- package/payload/server/public/assets/{maximize-2-la3IBA3M.js → maximize-2-Xdj804U1.js} +1 -1
- package/payload/server/public/assets/maximize-2-Xdj804U1.js.br +0 -0
- package/payload/server/public/assets/maximize-2-Xdj804U1.js.gz +0 -0
- package/payload/server/public/assets/{operator-D9NgVL9_.js → operator-BkfNpBs7.js} +1 -1
- package/payload/server/public/assets/operator-BkfNpBs7.js.br +0 -0
- package/payload/server/public/assets/operator-BkfNpBs7.js.gz +0 -0
- package/payload/server/public/assets/{page-DC11gesX.js → page-BoYc9c4a.js} +1 -1
- package/payload/server/public/assets/page-BoYc9c4a.js.br +0 -0
- package/payload/server/public/assets/page-BoYc9c4a.js.gz +0 -0
- package/payload/server/public/assets/{page-BWFHRIAH.js → page-D5n8ShE7.js} +1 -1
- package/payload/server/public/assets/page-D5n8ShE7.js.br +0 -0
- package/payload/server/public/assets/page-D5n8ShE7.js.gz +0 -0
- package/payload/server/public/assets/{public-DQbbLHQN.js → public-CX5XSrxD.js} +1 -1
- package/payload/server/public/assets/public-CX5XSrxD.js.br +0 -0
- package/payload/server/public/assets/public-CX5XSrxD.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-OQD5si8N.js → rotate-ccw-dh_OhL23.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-dh_OhL23.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-dh_OhL23.js.gz +0 -0
- package/payload/server/public/assets/{routines-asrBGZUR.js → routines-bb7dxcrr.js} +1 -1
- package/payload/server/public/assets/routines-bb7dxcrr.js.br +0 -0
- package/payload/server/public/assets/routines-bb7dxcrr.js.gz +0 -0
- package/payload/server/public/assets/{skills-D4ECwoxa.js → skills-TeMwxXmJ.js} +1 -1
- package/payload/server/public/assets/skills-TeMwxXmJ.js.br +0 -0
- package/payload/server/public/assets/skills-TeMwxXmJ.js.gz +0 -0
- package/payload/server/public/assets/{tasks-U3QdbUsO.js → tasks-BqBBJlkg.js} +1 -1
- package/payload/server/public/assets/tasks-BqBBJlkg.js.br +0 -0
- package/payload/server/public/assets/tasks-BqBBJlkg.js.gz +0 -0
- package/payload/server/public/assets/{time-entry-format-BgdTKgYr.js → time-entry-format-Cy_T-wiD.js} +1 -1
- package/payload/server/public/assets/time-entry-format-Cy_T-wiD.js.br +3 -0
- package/payload/server/public/assets/time-entry-format-Cy_T-wiD.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-Ck_3VhT9.js → triangle-alert-D-bSbPnX.js} +1 -1
- package/payload/server/public/assets/triangle-alert-D-bSbPnX.js.br +1 -0
- package/payload/server/public/assets/triangle-alert-D-bSbPnX.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-BdUwyNpa.js → useCopyFeedback-L7L55nFl.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-L7L55nFl.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-L7L55nFl.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BYl2c7xA.css +1 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BYl2c7xA.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BYl2c7xA.css.gz +0 -0
- package/payload/server/public/assets/{useVoiceRecorder-ChnBrzbZ.js → useVoiceRecorder-MuZ7lpet.js} +1 -1
- package/payload/server/public/assets/useVoiceRecorder-MuZ7lpet.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-MuZ7lpet.js.gz +0 -0
- package/payload/server/public/assets/{wrench-Cpee3C3J.js → wrench-DUG1SZQB.js} +1 -1
- package/payload/server/public/assets/wrench-DUG1SZQB.js.br +0 -0
- package/payload/server/public/assets/wrench-DUG1SZQB.js.gz +0 -0
- package/payload/server/public/brand-defaults.css +2 -0
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +6 -6
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +37 -6
- package/payload/server/public/assets/AdminLoginScreens-bksN0XPe.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-bksN0XPe.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-BMPEWNZk.js.br +0 -0
- package/payload/server/public/assets/AdminShell-BMPEWNZk.js.gz +0 -0
- package/payload/server/public/assets/activity-ByWYF5dC.js.br +0 -0
- package/payload/server/public/assets/activity-ByWYF5dC.js.gz +0 -0
- package/payload/server/public/assets/admin-6QN-k3zQ.js.br +0 -0
- package/payload/server/public/assets/admin-6QN-k3zQ.js.gz +0 -0
- package/payload/server/public/assets/agents-C0rTfrYd.js.br +0 -0
- package/payload/server/public/assets/agents-C0rTfrYd.js.gz +0 -0
- package/payload/server/public/assets/browser-4tTaem8N.js.br +0 -0
- package/payload/server/public/assets/browser-4tTaem8N.js.gz +0 -0
- package/payload/server/public/assets/calendar-BEjnkKG1.js.br +0 -0
- package/payload/server/public/assets/calendar-BEjnkKG1.js.gz +0 -0
- package/payload/server/public/assets/chat-DBgzjDIE.js +0 -1
- package/payload/server/public/assets/chat-DBgzjDIE.js.br +0 -0
- package/payload/server/public/assets/chat-DBgzjDIE.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-DqYv3oFh.js +0 -1
- package/payload/server/public/assets/chevron-right-CQfPGsFb.js +0 -1
- package/payload/server/public/assets/chevron-right-CQfPGsFb.js.br +0 -0
- package/payload/server/public/assets/clock-Dn6FHB51.js +0 -1
- package/payload/server/public/assets/clock-Dn6FHB51.js.br +0 -0
- package/payload/server/public/assets/clock-Dn6FHB51.js.gz +0 -0
- package/payload/server/public/assets/data-D_e6Vdfd.js +0 -1
- package/payload/server/public/assets/data-D_e6Vdfd.js.br +0 -0
- package/payload/server/public/assets/data-D_e6Vdfd.js.gz +0 -0
- package/payload/server/public/assets/file-text-DMKw3nIk.js.br +0 -0
- package/payload/server/public/assets/file-text-DMKw3nIk.js.gz +0 -0
- package/payload/server/public/assets/graph-C3vW8lvw.js.br +0 -0
- package/payload/server/public/assets/graph-C3vW8lvw.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-BZgd0L6n.js.br +0 -0
- package/payload/server/public/assets/graph-labels-BZgd0L6n.js.gz +0 -0
- package/payload/server/public/assets/maximize-2-la3IBA3M.js.br +0 -0
- package/payload/server/public/assets/maximize-2-la3IBA3M.js.gz +0 -0
- package/payload/server/public/assets/operator-D9NgVL9_.js.br +0 -0
- package/payload/server/public/assets/operator-D9NgVL9_.js.gz +0 -0
- package/payload/server/public/assets/page-BWFHRIAH.js.br +0 -0
- package/payload/server/public/assets/page-BWFHRIAH.js.gz +0 -0
- package/payload/server/public/assets/page-DC11gesX.js.br +0 -0
- package/payload/server/public/assets/page-DC11gesX.js.gz +0 -0
- package/payload/server/public/assets/public-DQbbLHQN.js.br +0 -0
- package/payload/server/public/assets/public-DQbbLHQN.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-OQD5si8N.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-OQD5si8N.js.gz +0 -0
- package/payload/server/public/assets/routines-asrBGZUR.js.br +0 -0
- package/payload/server/public/assets/routines-asrBGZUR.js.gz +0 -0
- package/payload/server/public/assets/skills-D4ECwoxa.js.br +0 -0
- package/payload/server/public/assets/skills-D4ECwoxa.js.gz +0 -0
- package/payload/server/public/assets/tasks-U3QdbUsO.js.br +0 -0
- package/payload/server/public/assets/tasks-U3QdbUsO.js.gz +0 -0
- package/payload/server/public/assets/time-entry-format-BgdTKgYr.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-BgdTKgYr.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-Ck_3VhT9.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-Ck_3VhT9.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-BdUwyNpa.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-BdUwyNpa.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DOWQXDT8.css +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-DOWQXDT8.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DOWQXDT8.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-ChnBrzbZ.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-ChnBrzbZ.js.gz +0 -0
- package/payload/server/public/assets/wrench-Cpee3C3J.js.br +0 -2
- package/payload/server/public/assets/wrench-Cpee3C3J.js.gz +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-CJKjEPNN.js → useSubAccountSwitcher-DH0P6QMV.js} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-CJKjEPNN.js.br → useSubAccountSwitcher-DH0P6QMV.js.br} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-CJKjEPNN.js.gz → useSubAccountSwitcher-DH0P6QMV.js.gz} +0 -0
package/package.json
CHANGED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Account-machinery write fence Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Stop the account agent writing the five entries in the shipped account schema that are the account's own machinery rather than operator data.
|
|
6
|
+
|
|
7
|
+
**Architecture:** A new fenced `agent-denied-top-level` block in the shipped `SCHEMA.md` template lists the five names with a per-name reason. `fs-schema-guard.sh` parses it with the same awk shape it already uses for the other two fences and blocks a write whose first path segment matches, reporting `reason=account-machinery`. The `allowed-top-level` fence is not touched, so the standing reconcile keeps reading exactly what it reads today.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** bash, awk, python3. The hook and its tests are plain bash; no build and no `npm install`.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- The deny set and each name's reason come from the account's own `SCHEMA.md`. The hook hard-codes no list.
|
|
14
|
+
- `allowed-top-level` is not modified. All five names stay in it, so the standing reconcile never reports them as strays.
|
|
15
|
+
- No fence means nothing is denied. An account whose `SCHEMA.md` predates this change keeps today's behaviour, matching the guard's existing fail-open posture on a missing schema and on a missing `declared-files` fence.
|
|
16
|
+
- No task numbers or internal refs in any operator-visible string.
|
|
17
|
+
- Block log line shape is fixed: `[fs-guard] blocked path=<rel> reason=account-machinery`.
|
|
18
|
+
- The separator inside the fence is a literal TAB, matching the `declared-files` fence.
|
|
19
|
+
- `fs-schema-guard-bash-post.sh` is not modified.
|
|
20
|
+
- Baseline before any change: `fs-schema-guard.test.sh` is `39 passed, 0 failed`; `fs-schema-guard-bash.test.sh` is `15 passed, 0 failed`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
### Task 1: Ship the deny fence in the account schema template
|
|
25
|
+
|
|
26
|
+
**Files:**
|
|
27
|
+
- Modify: `platform/templates/account-schema/SCHEMA.md` (append a section after the "## Allowed top-level entries" section)
|
|
28
|
+
- Test: `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
|
|
29
|
+
|
|
30
|
+
**Interfaces:**
|
|
31
|
+
- Consumes: nothing.
|
|
32
|
+
- Produces: a fenced block whose header line is exactly three backticks followed by `agent-denied-top-level`, one line per name as `name<TAB>reason`, in the order `account.json`, `SCHEMA.md`, `secrets`, `.claude`, `.git`. Task 2 parses it.
|
|
33
|
+
|
|
34
|
+
This task is data only. After it the guard's behaviour is unchanged, and every pre-existing case must still pass — that is the evidence the appended fence disturbs no existing reader.
|
|
35
|
+
|
|
36
|
+
- [ ] **Step 1: Write the failing assertions**
|
|
37
|
+
|
|
38
|
+
Append these two assertions to `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`, immediately after the existing `allowed-set parse` assertion and before the `echo "----- $PASS passed, $FAIL failed -----"` line:
|
|
39
|
+
|
|
40
|
+
````bash
|
|
41
|
+
# Deny-fence parse == documented set. The guard reads this fence, so pinning its
|
|
42
|
+
# contents here means a name cannot be added or dropped without a test edit.
|
|
43
|
+
EXPECT_D="account.json SCHEMA.md secrets .claude .git"
|
|
44
|
+
GOT_D=$(awk '/^```agent-denied-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCT/SCHEMA.md" | cut -f1 | tr '\n' ' ' | sed 's/ *$//')
|
|
45
|
+
if [ "$GOT_D" = "$EXPECT_D" ]; then echo "PASS: deny-set parse"; PASS=$((PASS+1));
|
|
46
|
+
else echo "FAIL: deny-set parse: got [$GOT_D]" >&2; FAIL=$((FAIL+1)); fi
|
|
47
|
+
|
|
48
|
+
# Every deny entry carries a reason column: the block message has no owning
|
|
49
|
+
# plugin to name, so the schema must supply the explanation.
|
|
50
|
+
NOREASON=$(awk '/^```agent-denied-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCT/SCHEMA.md" | awk -F'\t' 'NF<2 || $2==""{print $1}')
|
|
51
|
+
if [ -z "$NOREASON" ]; then echo "PASS: deny-set reasons"; PASS=$((PASS+1));
|
|
52
|
+
else echo "FAIL: deny-set reasons missing for [$NOREASON]" >&2; FAIL=$((FAIL+1)); fi
|
|
53
|
+
````
|
|
54
|
+
|
|
55
|
+
- [ ] **Step 2: Run the suite to verify the new assertion fails**
|
|
56
|
+
|
|
57
|
+
Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
|
|
58
|
+
|
|
59
|
+
Expected: `FAIL: deny-set parse: got []` on stderr, and the run ends `----- 40 passed, 1 failed -----` with a non-zero exit. `deny-set reasons` passes at this point only because an absent fence yields no rows; it becomes meaningful after Step 3.
|
|
60
|
+
|
|
61
|
+
- [ ] **Step 3: Add the fence to the template**
|
|
62
|
+
|
|
63
|
+
Append this section to the end of `platform/templates/account-schema/SCHEMA.md`, after the closing fence of the `allowed-top-level` block. **The separator between each name and its reason is a literal TAB character, not spaces.**
|
|
64
|
+
|
|
65
|
+
`````markdown
|
|
66
|
+
## Entries you may not write
|
|
67
|
+
|
|
68
|
+
Five of the entries above are this account's own machinery rather than operator
|
|
69
|
+
data. Each is listed below with the reason it is off-limits. The write guard
|
|
70
|
+
blocks a write whose first path segment matches one of them and quotes that
|
|
71
|
+
reason back. Change any of them through the code that owns it: platform code
|
|
72
|
+
writes all five, and none is written through an agent file edit.
|
|
73
|
+
|
|
74
|
+
```agent-denied-top-level
|
|
75
|
+
account.json the account's identity and settings, changed through the account and admin tools
|
|
76
|
+
SCHEMA.md the layout rules the write guard itself reads, so editing it widens its own fence
|
|
77
|
+
secrets provisioned credentials, written by the code that mints each one
|
|
78
|
+
.claude the agent's own settings and hooks, seeded when the account is provisioned
|
|
79
|
+
.git the account directory's git internals, managed by git
|
|
80
|
+
```
|
|
81
|
+
`````
|
|
82
|
+
|
|
83
|
+
- [ ] **Step 4: Verify the tab separator landed**
|
|
84
|
+
|
|
85
|
+
Run:
|
|
86
|
+
|
|
87
|
+
````bash
|
|
88
|
+
awk '/^```agent-denied-top-level$/{f=1;next} /^```$/{f=0} f' platform/templates/account-schema/SCHEMA.md | cat -A | head -5
|
|
89
|
+
````
|
|
90
|
+
|
|
91
|
+
Expected: each of the five lines shows `^I` between the name and the reason, and ends `$`. If any line shows spaces instead, fix it before continuing — the guard splits on TAB and a space-separated line parses as a name with no reason.
|
|
92
|
+
|
|
93
|
+
- [ ] **Step 5: Run the full guard suite**
|
|
94
|
+
|
|
95
|
+
Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
|
|
96
|
+
|
|
97
|
+
Expected: `----- 41 passed, 0 failed -----`. Every pre-existing case must still pass, which is the proof the appended fence does not disturb the `allowed-top-level` parse, the `declared-files` parse, or any existing account fixture.
|
|
98
|
+
|
|
99
|
+
- [ ] **Step 6: Run the neighbouring suites that read the same schema**
|
|
100
|
+
|
|
101
|
+
Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh`
|
|
102
|
+
|
|
103
|
+
Expected: `----- 15 passed, 0 failed -----`
|
|
104
|
+
|
|
105
|
+
Run: `bash platform/scripts/__tests__/account-schema-owned-dirs.test.sh`
|
|
106
|
+
|
|
107
|
+
Expected: the suite's own pass line with zero failures. This is the regression check that the owned-dirs merge still finds and rewrites the `allowed-top-level` fence and its three marker-delimited regions with an extra fence present in the file.
|
|
108
|
+
|
|
109
|
+
- [ ] **Step 7: Commit**
|
|
110
|
+
|
|
111
|
+
````bash
|
|
112
|
+
git add maxy-code/platform/templates/account-schema/SCHEMA.md maxy-code/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh
|
|
113
|
+
git commit -m "feat(2052): ship the agent-denied-top-level fence in the account schema template"
|
|
114
|
+
````
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### Task 2: The guard denies a write to a fenced name
|
|
119
|
+
|
|
120
|
+
**Files:**
|
|
121
|
+
- Modify: `platform/plugins/admin/hooks/fs-schema-guard.sh` (header comment block at lines 2-22; new branch inserted after the top-level check's closing `fi` at line 89)
|
|
122
|
+
- Test: `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
|
|
123
|
+
|
|
124
|
+
**Interfaces:**
|
|
125
|
+
- Consumes: the `agent-denied-top-level` fence from Task 1, `name<TAB>reason` per line.
|
|
126
|
+
- Produces: exit 2 with `[fs-guard] blocked path=<rel> reason=account-machinery` on stderr, plus an operator-visible line quoting that name's reason. Nothing later depends on it.
|
|
127
|
+
|
|
128
|
+
- [ ] **Step 1: Write the failing tests**
|
|
129
|
+
|
|
130
|
+
Add this block to `platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`, immediately after the `run_case "no fence no deny" ...` line and before the `# Allowed-set parse == documented set.` comment:
|
|
131
|
+
|
|
132
|
+
````bash
|
|
133
|
+
# Account machinery: five entries in the allowed set are the account's own
|
|
134
|
+
# control plane, denied by name in the agent-denied-top-level fence. $ACCT is
|
|
135
|
+
# the pristine template copy, so it carries the fence. $ACCT_M models a schema
|
|
136
|
+
# written before the fence existed (names in the allowed set, fence stripped),
|
|
137
|
+
# which pins the deny to the fence rather than to a list baked into the hook.
|
|
138
|
+
ACCT_M=$(mktemp -d)
|
|
139
|
+
trap 'rm -rf "$ACCT" "$ACCT_D" "$ACCT_F" "$ACCT_L" "$ACCT_M"' EXIT
|
|
140
|
+
awk '/^```agent-denied-top-level$/{skip=1;next} skip && /^```$/{skip=0;next} !skip' "$TEMPLATE" > "$ACCT_M/SCHEMA.md"
|
|
141
|
+
|
|
142
|
+
DENIED_NAMES="account.json SCHEMA.md secrets .claude .git"
|
|
143
|
+
for n in $DENIED_NAMES; do
|
|
144
|
+
run_case "machinery block $n" "$(mkenv Write file_path "$n")" 2 "fs-guard. blocked path=$n reason=account-machinery"
|
|
145
|
+
done
|
|
146
|
+
# Segment-0 match: a nested path under a denied entry blocks for the same reason.
|
|
147
|
+
run_case "machinery nested settings" "$(mkenv Write file_path '.claude/settings.json')" 2 "fs-guard. blocked path=.claude/settings.json reason=account-machinery"
|
|
148
|
+
run_case "machinery nested secret" "$(mkenv Write file_path 'secrets/cloudflare.env')" 2 "reason=account-machinery"
|
|
149
|
+
# Every write tool, not just Write.
|
|
150
|
+
run_case "machinery edit blocks" "$(mkenv Edit file_path 'account.json')" 2 "reason=account-machinery"
|
|
151
|
+
run_case "machinery notebook blocks" "$(mkenv NotebookEdit notebook_path '.claude/n.ipynb')" 2 "reason=account-machinery"
|
|
152
|
+
# The schema's own reason reaches the operator-visible message.
|
|
153
|
+
run_case "machinery reason text" "$(mkenv Write file_path 'SCHEMA.md')" 2 "widens its own fence"
|
|
154
|
+
# An operator bucket under the same schema is unaffected.
|
|
155
|
+
run_case "machinery bucket allow" "$(mkenv Write file_path 'projects/acme/a.txt')" 0 ""
|
|
156
|
+
# No fence, no deny.
|
|
157
|
+
for n in $DENIED_NAMES; do
|
|
158
|
+
run_case "no deny fence allows $n" "$(mkenv Write file_path "$n")" 0 "" "$ACCT_M"
|
|
159
|
+
done
|
|
160
|
+
````
|
|
161
|
+
|
|
162
|
+
- [ ] **Step 2: Run the suite to verify the new cases fail**
|
|
163
|
+
|
|
164
|
+
Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
|
|
165
|
+
|
|
166
|
+
Expected: the ten `machinery *` cases FAIL with `(exit=0, want 2; ...)`. The five `no deny fence allows *` cases and `machinery bucket allow` already pass. The run ends `----- 47 passed, 10 failed -----` with a non-zero exit.
|
|
167
|
+
|
|
168
|
+
- [ ] **Step 3: Add the deny branch to the guard**
|
|
169
|
+
|
|
170
|
+
In `platform/plugins/admin/hooks/fs-schema-guard.sh`, insert this block immediately after the top-level check's closing `fi` (line 89) and before the `# Declared-file check.` comment:
|
|
171
|
+
|
|
172
|
+
````bash
|
|
173
|
+
# Account-machinery check. Some entries in the allowed set are the account's own
|
|
174
|
+
# control plane rather than operator data: the identity file, the schema this
|
|
175
|
+
# hook parses, the credential store, the agent's own settings and hooks, and the
|
|
176
|
+
# git dir. The allowed set says which names may EXIST at the account root, not
|
|
177
|
+
# which an agent may author, so each of these is named in a second fence together
|
|
178
|
+
# with the reason it is off-limits. Every one of them is written by platform code
|
|
179
|
+
# from bash or Node, never through Write/Edit/NotebookEdit, so denying them costs
|
|
180
|
+
# no legitimate writer. The set and its reasons come from the account's own
|
|
181
|
+
# SCHEMA.md, so the hook hard-codes no list. There is no owning plugin to name
|
|
182
|
+
# here, which is why this fence carries a reason column the declared-files fence
|
|
183
|
+
# does not need. No fence (a schema written before the fence existed) means
|
|
184
|
+
# nothing to deny — the same fail-open posture as the missing-schema case above.
|
|
185
|
+
DENIED=$(awk '/^```agent-denied-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCOUNT_DIR/SCHEMA.md")
|
|
186
|
+
if [ -n "$DENIED" ] && printf '%s\n' "$DENIED" | awk -F'\t' -v n="$SEG0" '$1==n{found=1} END{exit !found}'; then
|
|
187
|
+
WHY=$(printf '%s\n' "$DENIED" | awk -F'\t' -v n="$SEG0" '$1==n{print $2; exit}')
|
|
188
|
+
echo "[fs-guard] blocked path=$REL reason=account-machinery" >&2
|
|
189
|
+
echo "Blocked: '$SEG0' is this account's own machinery, not operator data: $WHY. Change it through the code that owns it, never by hand." >&2
|
|
190
|
+
exit 2
|
|
191
|
+
fi
|
|
192
|
+
````
|
|
193
|
+
|
|
194
|
+
- [ ] **Step 4: Update the hook's header comment**
|
|
195
|
+
|
|
196
|
+
In the same file, in the header comment block, insert this bullet after the `allowed top-level set` bullet and before the `plugin-declared file` bullet, so the header lists all three rules the hook enforces:
|
|
197
|
+
|
|
198
|
+
````bash
|
|
199
|
+
# - a target whose first path segment is named in the fenced
|
|
200
|
+
# ```agent-denied-top-level block is blocked even though that name is in the
|
|
201
|
+
# allowed set: those entries are the account's own machinery (identity,
|
|
202
|
+
# schema, credentials, agent settings, git dir), written by platform code
|
|
203
|
+
# and never by an agent file edit. The block message quotes the reason the
|
|
204
|
+
# fence carries for that name;
|
|
205
|
+
````
|
|
206
|
+
|
|
207
|
+
Then extend the exit-codes paragraph's log-line list so it reads:
|
|
208
|
+
|
|
209
|
+
````bash
|
|
210
|
+
# [fs-guard] blocked path=<rel> reason=<top-level|account-machinery|declared-file|over-deep|bad-name>
|
|
211
|
+
````
|
|
212
|
+
|
|
213
|
+
- [ ] **Step 5: Run the suite to verify it passes**
|
|
214
|
+
|
|
215
|
+
Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
|
|
216
|
+
|
|
217
|
+
Expected: `----- 57 passed, 0 failed -----`
|
|
218
|
+
|
|
219
|
+
- [ ] **Step 6: Mutation check**
|
|
220
|
+
|
|
221
|
+
Comment out the `exit 2` inside the new branch and re-run the suite.
|
|
222
|
+
|
|
223
|
+
Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh`
|
|
224
|
+
|
|
225
|
+
Expected: the ten `machinery *` cases FAIL. Then restore the line and re-run.
|
|
226
|
+
|
|
227
|
+
Expected after restore: `----- 57 passed, 0 failed -----`
|
|
228
|
+
|
|
229
|
+
- [ ] **Step 7: Confirm the untouched surfaces still pass**
|
|
230
|
+
|
|
231
|
+
Run: `bash platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh`
|
|
232
|
+
|
|
233
|
+
Expected: `----- 15 passed, 0 failed -----`. The Bash post-hook still reads only `allowed-top-level` and `declared-files`, and still stays silent on all five names.
|
|
234
|
+
|
|
235
|
+
Run: `bash platform/scripts/__tests__/account-schema-owned-dirs.test.sh`
|
|
236
|
+
|
|
237
|
+
Expected: zero failures.
|
|
238
|
+
|
|
239
|
+
- [ ] **Step 8: Commit**
|
|
240
|
+
|
|
241
|
+
````bash
|
|
242
|
+
git add maxy-code/platform/plugins/admin/hooks/fs-schema-guard.sh maxy-code/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh
|
|
243
|
+
git commit -m "feat(2052): deny agent writes to the account's own machinery"
|
|
244
|
+
````
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Reach note for the debrief
|
|
249
|
+
|
|
250
|
+
`provision-account-dir.sh:40-41` copies the template to `<accountDir>/SCHEMA.md`, and `setup-account.sh:59` calls that for the house account only. The standing all-accounts reconcile merges just the marker-delimited regions, so it never carries the template body. The house account therefore gains the fence on the next install, while a client sub-account, provisioned once at creation, keeps a fence-less schema indefinitely and the guard stays fail-open there. That gap is filed as `.tasks/pending/2056-the-account-machinery-deny-fence-never-reaches-an-existing-sub-account.md`, following the durable-backfill pattern of Task 1683 and Task 1929. Separately, `platform/` ships inside the bundled `create-*-code` payload, so reaching a device at all needs an operator-gated installer publish plus an `npx` upgrade. Both belong in the debrief's known-limitations section.
|
|
@@ -121,6 +121,7 @@ rather than flip it silently.
|
|
|
121
121
|
|
|
122
122
|
- The fence's pre-existing permissiveness for `account.json`, `secrets/` and
|
|
123
123
|
`.claude/`. It predates Task 1902 and is a separate decision, filed as
|
|
124
|
-
`.tasks/
|
|
124
|
+
`.tasks/archive/2052-the-allowed-fence-permits-agent-writes-to-the-account-s-own-machinery.md`
|
|
125
|
+
(renumbered from 2048; landed 2026-07-27).
|
|
125
126
|
- The reconcile. It reads the fence correctly and needs no change.
|
|
126
127
|
- Adding or removing declarations. Task 1902 settled the set.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Task 2052 — the write guard denies the account's own machinery
|
|
2
|
+
|
|
3
|
+
Design, 2026-07-27. Lane: Platform / account schema · write guard.
|
|
4
|
+
|
|
5
|
+
## The fault
|
|
6
|
+
|
|
7
|
+
`fs-schema-guard.sh` allows a `Write`, `Edit` or `NotebookEdit` whose first path
|
|
8
|
+
segment is in the account's ```` ```allowed-top-level ```` fence. The shipped
|
|
9
|
+
template lists `account.json`, `SCHEMA.md`, `secrets/`, `.claude/` and `.git`, so
|
|
10
|
+
each is agent-writable. Measured against the untouched shipped template, an
|
|
11
|
+
envelope targeting `account.json` returns exit 0.
|
|
12
|
+
|
|
13
|
+
None of the five is operator data:
|
|
14
|
+
|
|
15
|
+
- `account.json` carries the account identity, role, tier and enabled plugins.
|
|
16
|
+
`admin-user-management/SKILL.md:39` states the position already: "Direct `Edit`
|
|
17
|
+
or `Write` on `account.json` is forbidden by IDENTITY.md doctrine — there is no
|
|
18
|
+
server-side gate, so the doctrine line is the only thing standing between an
|
|
19
|
+
agent slip and a silently corrupted account file." Mutations have dedicated
|
|
20
|
+
tools (`account-update`, `plugin-toggle-enabled`, `admin-add`, `admin-remove`).
|
|
21
|
+
- `SCHEMA.md` is the schema the guard itself parses. A write to it widens the
|
|
22
|
+
fence the guard reads on the next call.
|
|
23
|
+
- `.claude/` holds the account's project-level `settings.json`, which is where
|
|
24
|
+
`provision-account-dir.sh:62,77-143` registers this guard and both Bash hooks. A
|
|
25
|
+
write to it can delete the guard's own registration.
|
|
26
|
+
- `secrets/` holds provisioned credentials.
|
|
27
|
+
- `.git` is the account directory's git repository, created at
|
|
28
|
+
`provision-account-dir.sh:59` so Claude Code discovers `.claude/`.
|
|
29
|
+
|
|
30
|
+
## Decision per name
|
|
31
|
+
|
|
32
|
+
All five are denied to the agent's write tools. No legitimate agent
|
|
33
|
+
`Write`/`Edit`/`NotebookEdit` path exists for any of them:
|
|
34
|
+
|
|
35
|
+
| Name | Who writes it |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `account.json` | `provision-account-dir.sh:310-349` and the account MCP tools |
|
|
38
|
+
| `SCHEMA.md` | `provision-account-dir.sh:40-41` and the owned-dirs merge |
|
|
39
|
+
| `secrets/` | the code that mints each credential; the one skill-instructed path is a Bash append (`cloudflare/skills/data-portal/SKILL.md:161`), which this guard never sees |
|
|
40
|
+
| `.claude/` | `provision-account-dir.sh:26,61-143` |
|
|
41
|
+
| `.git` | git itself |
|
|
42
|
+
|
|
43
|
+
## Design
|
|
44
|
+
|
|
45
|
+
### 1. A third fence, shipped in the template
|
|
46
|
+
|
|
47
|
+
`platform/templates/account-schema/SCHEMA.md` gains a fenced block below the
|
|
48
|
+
allowed-top-level section:
|
|
49
|
+
|
|
50
|
+
````
|
|
51
|
+
```agent-denied-top-level
|
|
52
|
+
account.json the account's identity and settings, changed through the account and admin tools
|
|
53
|
+
SCHEMA.md the schema this guard reads, so a hand edit widens the guard's own fence
|
|
54
|
+
secrets provisioned credentials, written by the code that mints each one
|
|
55
|
+
.claude the agent's own settings and hooks, seeded when the account is provisioned
|
|
56
|
+
.git the account directory's git internals
|
|
57
|
+
```
|
|
58
|
+
````
|
|
59
|
+
|
|
60
|
+
One line per name as `name<TAB>reason`, the same shape as the `declared-files`
|
|
61
|
+
fence. The reason column exists because there is no owning plugin to name here,
|
|
62
|
+
so the block message needs the schema to supply its own explanation.
|
|
63
|
+
|
|
64
|
+
The block is authored in the template rather than generated. The owned-dirs merge
|
|
65
|
+
(`account-schema-owned-dirs.py`) rewrites only its three marker-delimited regions
|
|
66
|
+
and the `allowed-top-level` fence in place, so a fence in the template body
|
|
67
|
+
survives a re-merge untouched. `allowed-top-level` is not modified, so the
|
|
68
|
+
standing reconcile keeps reading exactly what it reads today and still never
|
|
69
|
+
names any of the five as a stray — the one-list-two-meanings fault Task 2028
|
|
70
|
+
fixed is not reintroduced.
|
|
71
|
+
|
|
72
|
+
### 2. The guard denies
|
|
73
|
+
|
|
74
|
+
`fs-schema-guard.sh` parses the new fence with one awk expression, the same shape
|
|
75
|
+
it uses for the other two. The check sits after the top-level check and before
|
|
76
|
+
the declared-file check, and matches on the first path segment, so
|
|
77
|
+
`.claude/settings.json` and `secrets/cloudflare.env` are both blocked. No name is
|
|
78
|
+
in both fences (the declared set is the six platform-written `*.json` files the
|
|
79
|
+
merge emits), so the ordering never changes which reason is reported.
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
[fs-guard] blocked path=<rel> reason=account-machinery
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The operator-visible line names the segment and carries that name's reason from
|
|
86
|
+
the fence, then points at the code that owns it. No task numbers or internal refs
|
|
87
|
+
appear in it, per the guard's existing rule.
|
|
88
|
+
|
|
89
|
+
An account whose `SCHEMA.md` carries no `agent-denied-top-level` fence denies
|
|
90
|
+
nothing. That is the same fail-open posture the guard already takes on a missing
|
|
91
|
+
schema and on a missing `declared-files` fence.
|
|
92
|
+
|
|
93
|
+
Which accounts lack it is not uniform, and the split matters. The fence lives in
|
|
94
|
+
the template body, and only `provision_account_dir` copies that body
|
|
95
|
+
(`provision-account-dir.sh:40-41`, an unconditional `cp` of the whole template).
|
|
96
|
+
`setup-account.sh:59` calls it once, for the house account. The standing
|
|
97
|
+
all-accounts reconcile merges only the marker-delimited regions. So the house
|
|
98
|
+
account gains the fence on the next install, and a client sub-account, which is
|
|
99
|
+
provisioned once at creation, keeps a fence-less schema indefinitely. Closing
|
|
100
|
+
that is filed as `.tasks/pending/2056-the-account-machinery-deny-fence-never-reaches-an-existing-sub-account.md`,
|
|
101
|
+
following the durable-backfill pattern Task 1683 and Task 1929 already
|
|
102
|
+
established in `setup-account.sh`.
|
|
103
|
+
|
|
104
|
+
### 3. Provision and reconcile are unaffected
|
|
105
|
+
|
|
106
|
+
Both write `SCHEMA.md` and `account.json`, and neither goes through the agent's
|
|
107
|
+
write tools: provisioning is bash (`provision-account-dir.sh`) and the standing
|
|
108
|
+
reconcile is Node (`account-dir-schema-reconcile.ts`). The guard is a PreToolUse
|
|
109
|
+
hook on `Write`/`Edit`/`NotebookEdit` only, so it never sees either writer.
|
|
110
|
+
|
|
111
|
+
### 4. The Bash post-hook stays unchanged
|
|
112
|
+
|
|
113
|
+
`fs-schema-guard-bash-post.sh` reads `allowed-top-level` and the `declared-files`
|
|
114
|
+
fence. This change touches neither, and all five names stay in `allowed-top-level`,
|
|
115
|
+
so the post hook keeps not flagging them. That is the deliberate choice, not an
|
|
116
|
+
inherited one: the post hook fires only on a top-level name that appeared during
|
|
117
|
+
the command, and `account.json`, `SCHEMA.md`, `.claude` and `.git` all exist from
|
|
118
|
+
provision onward, so a signal there would catch nothing on a live account.
|
|
119
|
+
|
|
120
|
+
## Testing
|
|
121
|
+
|
|
122
|
+
`fs-schema-guard.test.sh` gains:
|
|
123
|
+
|
|
124
|
+
- one case per name asserting exit 2 with `reason=account-machinery` and that
|
|
125
|
+
name's reason text present in the message;
|
|
126
|
+
- a nested case for `.claude/settings.json` and `secrets/cloudflare.env`
|
|
127
|
+
asserting exit 2, pinning the segment-0 match;
|
|
128
|
+
- an operator bucket write under the same schema asserting exit 0;
|
|
129
|
+
- a case under a schema that lists the five in `allowed-top-level` but carries no
|
|
130
|
+
`agent-denied-top-level` fence asserting exit 0, which pins the deny to the
|
|
131
|
+
fence rather than to a list baked into the hook;
|
|
132
|
+
- the existing template-parity assertion extended so the shipped fence contents
|
|
133
|
+
are pinned.
|
|
134
|
+
|
|
135
|
+
Mutation check: remove the deny branch and confirm the per-name cases fail.
|
|
136
|
+
Revert.
|
|
137
|
+
|
|
138
|
+
The reconcile suite and `fs-schema-guard-bash.test.sh` must both still pass
|
|
139
|
+
unchanged, which is the evidence that `allowed-top-level` was not disturbed.
|
|
140
|
+
|
|
141
|
+
## Out of scope
|
|
142
|
+
|
|
143
|
+
- The `declared-files` fence and the plugin-declaration mechanism. Task 2028
|
|
144
|
+
settled both.
|
|
145
|
+
- `.quarantine`, which is historical and already documented as never authored
|
|
146
|
+
into directly.
|
|
147
|
+
- `AGENTS.md`, which is in the allowed fence but outside the five names this task
|
|
148
|
+
names. The same overwritten-on-provision rationale may apply to it; that
|
|
149
|
+
decision is filed as
|
|
150
|
+
`.tasks/backlog/2057-agents-md-is-refreshed-every-provision-but-stays-agent-writable.md`.
|
|
@@ -125,11 +125,52 @@ run_case "declared edit blocks" "$(mkenv Edit file_path 'wa-channel-bindings.j
|
|
|
125
125
|
run_case "operator bucket allow" "$(mkenv Write file_path 'projects/acme/a.txt')" 0 "" "$ACCT_F"
|
|
126
126
|
run_case "no fence no deny" "$(mkenv Write file_path 'wa-channel-bindings.json')" 0 "" "$ACCT_L"
|
|
127
127
|
|
|
128
|
+
# Account machinery: five entries in the allowed set are the account's own
|
|
129
|
+
# control plane, denied by name in the agent-denied-top-level fence. $ACCT is
|
|
130
|
+
# the pristine template copy, so it carries the fence. $ACCT_M models a schema
|
|
131
|
+
# written before the fence existed (names in the allowed set, fence stripped),
|
|
132
|
+
# which pins the deny to the fence rather than to a list baked into the hook.
|
|
133
|
+
ACCT_M=$(mktemp -d)
|
|
134
|
+
trap 'rm -rf "$ACCT" "$ACCT_D" "$ACCT_F" "$ACCT_L" "$ACCT_M"' EXIT
|
|
135
|
+
awk '/^```agent-denied-top-level$/{skip=1;next} skip && /^```$/{skip=0;next} !skip' "$TEMPLATE" > "$ACCT_M/SCHEMA.md"
|
|
136
|
+
|
|
137
|
+
DENIED_NAMES="account.json SCHEMA.md secrets .claude .git"
|
|
138
|
+
for n in $DENIED_NAMES; do
|
|
139
|
+
run_case "machinery block $n" "$(mkenv Write file_path "$n")" 2 "fs-guard. blocked path=$n reason=account-machinery"
|
|
140
|
+
done
|
|
141
|
+
# Segment-0 match: a nested path under a denied entry blocks for the same reason.
|
|
142
|
+
run_case "machinery nested settings" "$(mkenv Write file_path '.claude/settings.json')" 2 "fs-guard. blocked path=.claude/settings.json reason=account-machinery"
|
|
143
|
+
run_case "machinery nested secret" "$(mkenv Write file_path 'secrets/cloudflare.env')" 2 "reason=account-machinery"
|
|
144
|
+
# Every write tool, not just Write.
|
|
145
|
+
run_case "machinery edit blocks" "$(mkenv Edit file_path 'account.json')" 2 "reason=account-machinery"
|
|
146
|
+
run_case "machinery notebook blocks" "$(mkenv NotebookEdit notebook_path '.claude/n.ipynb')" 2 "reason=account-machinery"
|
|
147
|
+
# The schema's own reason reaches the operator-visible message.
|
|
148
|
+
run_case "machinery reason text" "$(mkenv Write file_path 'SCHEMA.md')" 2 "widens its own fence"
|
|
149
|
+
# An operator bucket under the same schema is unaffected.
|
|
150
|
+
run_case "machinery bucket allow" "$(mkenv Write file_path 'projects/acme/a.txt')" 0 ""
|
|
151
|
+
# No fence, no deny.
|
|
152
|
+
for n in $DENIED_NAMES; do
|
|
153
|
+
run_case "no deny fence allows $n" "$(mkenv Write file_path "$n")" 0 "" "$ACCT_M"
|
|
154
|
+
done
|
|
155
|
+
|
|
128
156
|
# Allowed-set parse == documented set.
|
|
129
157
|
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"
|
|
130
158
|
GOT=$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCT/SCHEMA.md" | tr '\n' ' ' | sed 's/ *$//')
|
|
131
159
|
if [ "$GOT" = "$EXPECT" ]; then echo "PASS: allowed-set parse"; PASS=$((PASS+1));
|
|
132
160
|
else echo "FAIL: allowed-set parse: got [$GOT]" >&2; FAIL=$((FAIL+1)); fi
|
|
133
161
|
|
|
162
|
+
# Deny-fence parse == documented set. The guard reads this fence, so pinning its
|
|
163
|
+
# contents here means a name cannot be added or dropped without a test edit.
|
|
164
|
+
EXPECT_D="account.json SCHEMA.md secrets .claude .git"
|
|
165
|
+
GOT_D=$(awk '/^```agent-denied-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCT/SCHEMA.md" | cut -f1 | tr '\n' ' ' | sed 's/ *$//')
|
|
166
|
+
if [ "$GOT_D" = "$EXPECT_D" ]; then echo "PASS: deny-set parse"; PASS=$((PASS+1));
|
|
167
|
+
else echo "FAIL: deny-set parse: got [$GOT_D]" >&2; FAIL=$((FAIL+1)); fi
|
|
168
|
+
|
|
169
|
+
# Every deny entry carries a reason column: the block message has no owning
|
|
170
|
+
# plugin to name, so the schema must supply the explanation.
|
|
171
|
+
NOREASON=$(awk '/^```agent-denied-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCT/SCHEMA.md" | awk -F'\t' 'NF<2 || $2==""{print $1}')
|
|
172
|
+
if [ -z "$NOREASON" ]; then echo "PASS: deny-set reasons"; PASS=$((PASS+1));
|
|
173
|
+
else echo "FAIL: deny-set reasons missing for [$NOREASON]" >&2; FAIL=$((FAIL+1)); fi
|
|
174
|
+
|
|
134
175
|
echo "----- $PASS passed, $FAIL failed -----"
|
|
135
176
|
[ $FAIL -eq 0 ]
|
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
# declared-files fence; this hook stays quiet, and a case in the test suite pins
|
|
29
29
|
# the silence.
|
|
30
30
|
#
|
|
31
|
+
# The same reasoning covers the ```agent-denied-top-level fence, and for the same
|
|
32
|
+
# reason it needs no code here: those five names are in the allowed set too, and
|
|
33
|
+
# account.json, SCHEMA.md, .claude and .git all exist from provision onward, so
|
|
34
|
+
# they can never APPEAR during a command on a live account. secrets/ can appear
|
|
35
|
+
# once, and is deliberately left alone — the data-portal skill creates it with a
|
|
36
|
+
# shell redirect as its sanctioned path, so flagging that first creation would
|
|
37
|
+
# fire on the one legitimate writer and stay blind to every edit after it.
|
|
38
|
+
#
|
|
31
39
|
# Exit codes: 0 = allow, 2 = feedback to the agent (stderr shown). Fail open
|
|
32
40
|
# (exit 0) when the snapshot is missing or the allowed set is empty. Every block
|
|
33
41
|
# logs: [fs-guard-bash] stray path=<name> reason=<top-level|bad-name>
|
|
@@ -5,6 +5,12 @@
|
|
|
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 named in the fenced
|
|
9
|
+
# ```agent-denied-top-level block is blocked even though that name is in the
|
|
10
|
+
# allowed set: those entries are the account's control plane (identity,
|
|
11
|
+
# schema, credentials, agent settings, git dir), and none of them is ever
|
|
12
|
+
# authored by an agent file edit. The block message quotes the reason the
|
|
13
|
+
# fence carries for that name;
|
|
8
14
|
# - a target whose first path segment is a plugin-declared file (parsed from
|
|
9
15
|
# the fenced ```declared-files block of the same SCHEMA.md) is blocked even
|
|
10
16
|
# though that name is in the allowed set: the allowed set is a layout list,
|
|
@@ -18,7 +24,7 @@
|
|
|
18
24
|
#
|
|
19
25
|
# Exit codes: 0 = allow, 2 = block (stderr shown to the agent). Fail closed when
|
|
20
26
|
# the tool call cannot be inspected (tty or empty stdin). Every block logs
|
|
21
|
-
# [fs-guard] blocked path=<rel> reason=<top-level|declared-file|over-deep|bad-name>
|
|
27
|
+
# [fs-guard] blocked path=<rel> reason=<top-level|account-machinery|declared-file|over-deep|bad-name>
|
|
22
28
|
# No task numbers / internal refs in any operator-visible string.
|
|
23
29
|
|
|
24
30
|
set -uo pipefail
|
|
@@ -88,6 +94,39 @@ if ! printf '%s\n' "$ALLOWED" | grep -qxF "$SEG0"; then
|
|
|
88
94
|
exit 2
|
|
89
95
|
fi
|
|
90
96
|
|
|
97
|
+
# Account-machinery check. Some entries in the allowed set are the account's own
|
|
98
|
+
# control plane rather than operator data: the identity file, the schema this
|
|
99
|
+
# hook parses, the credential store, the agent's own settings and hooks, and the
|
|
100
|
+
# git dir. The allowed set says which names may EXIST at the account root, not
|
|
101
|
+
# which an agent may author, so each of these is named in a second fence together
|
|
102
|
+
# with the reason it is off-limits. None of the five is authored through
|
|
103
|
+
# Write/Edit/NotebookEdit: platform bash and Node write four of them, and git
|
|
104
|
+
# itself writes .git, so denying them costs no legitimate writer. This denies
|
|
105
|
+
# the agent's FILE-EDIT tools only, which is the whole surface the hook governs
|
|
106
|
+
# — a Bash command still reaches these paths, and one sanctioned path relies on
|
|
107
|
+
# that (the data-portal skill appends its fetch secret to secrets/ with a shell
|
|
108
|
+
# redirect). The set and its reasons come from the account's own SCHEMA.md, so
|
|
109
|
+
# the hook hard-codes no list. There is no owning plugin to name here, which is
|
|
110
|
+
# why this fence carries a reason column the declared-files fence does not need.
|
|
111
|
+
#
|
|
112
|
+
# Not to be confused with the same phrase in account-dir-schema-reconcile.ts,
|
|
113
|
+
# whose "account's own machinery" is the never-a-stray set and covers a
|
|
114
|
+
# different membership (.quarantine and AGENTS.md are in it; secrets/ is not).
|
|
115
|
+
#
|
|
116
|
+
# No fence means nothing to deny, the same fail-open posture as the
|
|
117
|
+
# missing-schema case above. Which accounts lack it is not uniform: setup-account.sh
|
|
118
|
+
# re-provisions only the house account, and provision copies the template whole,
|
|
119
|
+
# so the house account gains the fence on the next install while a client
|
|
120
|
+
# sub-account — provisioned once, at creation — keeps a fence-less schema until a
|
|
121
|
+
# durable backfill lands.
|
|
122
|
+
DENIED=$(awk '/^```agent-denied-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCOUNT_DIR/SCHEMA.md")
|
|
123
|
+
if [ -n "$DENIED" ] && printf '%s\n' "$DENIED" | awk -F'\t' -v n="$SEG0" '$1==n{found=1} END{exit !found}'; then
|
|
124
|
+
WHY=$(printf '%s\n' "$DENIED" | awk -F'\t' -v n="$SEG0" '$1==n{print $2; exit}')
|
|
125
|
+
echo "[fs-guard] blocked path=$REL reason=account-machinery" >&2
|
|
126
|
+
echo "Blocked: '$SEG0' is this account's own machinery, not operator data: $WHY. Change it through the code that owns it." >&2
|
|
127
|
+
exit 2
|
|
128
|
+
fi
|
|
129
|
+
|
|
91
130
|
# Declared-file check. A file declared in the fence is written whole by platform
|
|
92
131
|
# code, so an agent write to it is blocked even though the name is in the allowed
|
|
93
132
|
# set: that set says which names may exist at the account root, not which an
|
|
@@ -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.514)
|
|
13
|
+
|
|
14
|
+
- Three delete and danger buttons had no visible hover state, because their hover colour resolved to white. They now have a real hover step on the colour ramp.
|
|
15
|
+
- The assistant can no longer write to your account's own configuration and machinery files, which are now fenced off at the write guard.
|
|
16
|
+
- WhatsApp escalation checks now compare against the account a message was actually routed to, rather than a folder name, so an escalation is judged against the right account.
|
|
17
|
+
|
|
12
18
|
## 2026-07-27 (0.1.513)
|
|
13
19
|
|
|
14
20
|
- 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.
|
|
@@ -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:83-86 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
|
|
@@ -137,7 +137,7 @@ A passive-bound phone that DMs a requirement gets exactly one `:Task` filed unde
|
|
|
137
137
|
|
|
138
138
|
**Recall surfaces the attachment, not just the text.** A recalled message that carried media now includes an `attachment` reference on its read-tool row (`whatsapp-messages` and both modes of `whatsapp-conversation-graph-state`): `attachmentId`, `filename`, `mimeType`, a `sizeBytes` (bytes, as stored — the line renders it as KB/MB), an absolute on-disk `path` to the stored bytes, and a `readableTextPath` when an extracted-text sibling exists. The reference resolves against the same store account the route read the thread from (the house account when projecting a sub-account), so the path always points at the house-stored bytes, never the sub-account's own empty uploads dir. The read-tool output renders it as an indented `[attachment: <name> (<mime>, <size>); Read: <path>]` line, and the agent reads that path directly (an image renders when Read). Inbound WhatsApp media runs best-effort text extraction (the earlier `skipDocToText` opt-out was retired): a convertible container (xlsx, docx, ods, and the rest of `CONVERTIBLE_DOC_MIME`) gets an `.extracted.txt` sibling, so `readableTextPath` is populated for those; an image or other non-convertible type has no sibling and surfaces the bytes `path` alone. Observable on each read route's result line as `withAttachments=<n>`; a record that names an attachment whose bytes are missing under the store account logs `[whatsapp-read-tool] op=attachment-miss attachmentId=<8> storeAccount=<id>`, which separates a genuine house-store gap from a sub-account mis-resolution.
|
|
139
139
|
|
|
140
|
-
**Single source of truth + fail-closed.** The effective account is resolved **exactly once**, in the gate: `checkDmAccess` returns `effectiveAccountId` (the house account for owner/`adminPhones`/public, the bound sub-account for a manager), and that value threads through the inbound payload → gateway → `ensureChannelSession`, which spawns into it **without re-reading** the `accountManagers` map. There is no second resolution and no `?? accountId` house fallback: if a manager's bound sub-account is not a valid account, the inbound is **rejected** (no session spawned, no reply), never routed to the house. This closes the escalation where a divergence between two independent map reads handed a scoped manager a house-owner admin session. Observable signals: `op=account-manager-route … effectiveAccount=… source=gate` on a routed manager inbound; `op=account-manager-reject … reason=unresolved-effective-account` on the fail-closed drop; a standing `op=escalation-tripwire` belt that can only fire if a future change reintroduces the divergence.
|
|
140
|
+
**Single source of truth + fail-closed.** The effective account is resolved **exactly once**, in the gate: `checkDmAccess` returns `effectiveAccountId` (the house account for owner/`adminPhones`/public, the bound sub-account for a manager), and that value threads through the inbound payload → gateway → `ensureChannelSession`, which spawns into it **without re-reading** the `accountManagers` map. There is no second resolution and no `?? accountId` house fallback: if a manager's bound sub-account is not a valid account, the inbound is **rejected** (no session spawned, no reply), never routed to the house. This closes the escalation where a divergence between two independent map reads handed a scoped manager a house-owner admin session. Observable signals: `op=account-manager-route … effectiveAccount=… source=gate` on a routed manager inbound; `op=account-manager-reject … reason=unresolved-effective-account` on the fail-closed drop; a standing `op=escalation-tripwire` belt that can only fire if a future change reintroduces the divergence. The belt compares the manager's effective account against the **house** UUID, not against the account inbound persists to — on an install that sets `channelRoutingAccountId` those differ, and a manager bound to the routing target is a legitimate binding that must admit. The binding write path now refuses a house-account binding at write time, so no new one can be created; a binding written before that refusal existed is still on disk and is what the belt catches.
|
|
141
141
|
|
|
142
142
|
**The scheduler path fails closed too.** A scheduled dispatch (`POST /api/channel/schedule-inject`) reaches the same spawn machine without going through the gate, resolving its own effective account via `effectiveAccountFor`. That resolver carries the same single-source + fail-closed shape: a non-manager destination scopes to the house (an owner/admin's real scope), a valid manager scopes to the bound sub-account, and a manager whose bound sub-account is not a valid account resolves to nothing — the route **rejects** (`op=schedule-account-manager-reject … reason=unresolved-effective-account`, HTTP 403, no spawn, no reply), never routing to the house. There is no `?? accountId` fallback on this path either. Telegram scheduled dispatch is house-only by construction (no account-manager routing), so there is nothing to fail closed there.
|
|
143
143
|
|
|
@@ -112,3 +112,20 @@ AGENTS.md
|
|
|
112
112
|
.claude
|
|
113
113
|
.git
|
|
114
114
|
```
|
|
115
|
+
|
|
116
|
+
## Entries you may not write
|
|
117
|
+
|
|
118
|
+
Five of the entries above are this account's own machinery rather than operator
|
|
119
|
+
data. Each is listed below with the reason it is off-limits. The write guard
|
|
120
|
+
blocks a write whose first path segment matches one of them and quotes that
|
|
121
|
+
reason back. Change any of them through the code that owns it. Each has an
|
|
122
|
+
owner already, named in its reason below, and none of the five is ever authored
|
|
123
|
+
by editing the file here.
|
|
124
|
+
|
|
125
|
+
```agent-denied-top-level
|
|
126
|
+
account.json the account's identity and settings, changed through the account and admin tools
|
|
127
|
+
SCHEMA.md the layout rules the write guard itself reads, so editing it widens its own fence
|
|
128
|
+
secrets provisioned credentials, written by the code that mints each one
|
|
129
|
+
.claude the agent's own settings and hooks, seeded when the account is provisioned
|
|
130
|
+
.git the account directory's git internals, managed by git
|
|
131
|
+
```
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Activity — Maxy</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico">
|
|
8
|
-
<script type="module" crossorigin src="/assets/activity-
|
|
8
|
+
<script type="module" crossorigin src="/assets/activity-BgZISMuE.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/chunk-CltuBf4Z.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/useSubAccountSwitcher-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminShell-
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/triangle-alert-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/useSubAccountSwitcher-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/useSubAccountSwitcher-DH0P6QMV.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminShell-DkrURZnF.js">
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/triangle-alert-D-bSbPnX.js">
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/useSubAccountSwitcher-BYl2c7xA.css">
|
|
14
14
|
<link rel="stylesheet" href="/brand-defaults.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|