@rubytech/create-maxy-code 0.1.109 → 0.1.110

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.
Files changed (30) hide show
  1. package/dist/index.js +11 -125
  2. package/dist/snap-chromium.js +1 -2
  3. package/dist/uninstall.js +10 -9
  4. package/package.json +1 -1
  5. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  6. package/payload/platform/plugins/admin/mcp/dist/index.js +1 -1
  7. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  8. package/payload/platform/plugins/admin/skills/upgrade/SKILL.md +34 -0
  9. package/payload/platform/plugins/cloudflare/.claude-plugin/plugin.json +1 -1
  10. package/payload/platform/plugins/cloudflare/PLUGIN.md +9 -16
  11. package/payload/platform/plugins/cloudflare/mcp/dist/index.js +7 -12
  12. package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
  13. package/payload/platform/plugins/cloudflare/references/dashboard-guide.md +3 -3
  14. package/payload/platform/plugins/cloudflare/references/manual-setup.md +16 -51
  15. package/payload/platform/plugins/cloudflare/references/reset-guide.md +24 -25
  16. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +29 -149
  17. package/payload/platform/plugins/docs/references/admin-session.md +1 -1
  18. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  19. package/payload/platform/plugins/docs/references/cloudflare.md +20 -29
  20. package/payload/platform/plugins/docs/references/platform.md +1 -1
  21. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  22. package/payload/platform/plugins/docs/references/troubleshooting.md +1 -1
  23. package/payload/platform/scripts/check-no-task-id-leaks.mjs +1 -1
  24. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  25. package/payload/platform/plugins/cloudflare/scripts/__tests__/tunnel-ingress.test.ts +0 -241
  26. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.sh +0 -98
  27. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +0 -715
  28. package/payload/platform/plugins/cloudflare/scripts/reset-tunnel.sh +0 -107
  29. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +0 -852
  30. package/payload/platform/plugins/cloudflare/scripts/tunnel-ingress.ts +0 -291
@@ -1,6 +1,6 @@
1
1
  # Admin Session — restart survival and SDK-resume contract
2
2
 
3
- The admin PIN-gated session-store is the in-memory `Map<sessionKey, Session>` at [`platform/ui/app/lib/claude-agent/session-store.ts`](../../../ui/app/lib/claude-agent/session-store.ts). Every `systemctl --user restart {brand}.service` (notably the one `setup-tunnel.sh` arms 3 s after `step=done` via `systemd-run --on-active=3s`) wipes that Map. This reference documents how an admin session survives the restart without forcing PIN re-entry, and how the SDK conversation chain is preserved across the gap.
3
+ The admin PIN-gated session-store is the in-memory `Map<sessionKey, Session>` at [`platform/ui/app/lib/claude-agent/session-store.ts`](../../../ui/app/lib/claude-agent/session-store.ts). Every `systemctl --user restart {brand}.service` (notably the one the agent arms 3 s after Cloudflare-setup completion via `systemd-run --on-active=3s` to avoid the cgroup trap) wipes that Map. This reference documents how an admin session survives the restart without forcing PIN re-entry, and how the SDK conversation chain is preserved across the gap.
4
4
 
5
5
  ## Signed sessionKey
6
6
 
@@ -216,7 +216,7 @@ Two endpoints, two surfaces, two restart-survival roles:
216
216
  liveness. `processStartedAt` resets on every brand-service restart;
217
217
  Neo4j probe is bounded to 1 s and reports
218
218
  `conversationDb: 'ok' | 'error'`. Use this to confirm the brand
219
- process came back after `setup-tunnel.sh`'s armed restart.
219
+ process came back after a Cloudflare-setup armed restart.
220
220
  - `GET /api/admin/version` (maxy-edge) — installer / brand version
221
221
  string. Hosted on `maxy-edge.service` so the Software Update modal
222
222
  can read it while the brand service is mid-restart.
@@ -1,6 +1,6 @@
1
1
  # Cloudflare Tunnel — the dashboard is the source of truth
2
2
 
3
- Each installation has its own Cloudflare account. Sign-in is OAuth: the setup script prints the Cloudflare Authorize URL into the admin chat as a clickable link; click it, authorize in the new tab, and `cloudflared` writes `cert.pem` to the brand's config directory. The agent never reads or mutates Cloudflare account state directly — whatever you see in your logged-in dashboard is the single source of truth. When something needs doing on the account side (adding a domain, deleting a stray entry, switching accounts), the agent relays the click-paths; you run them in your browser.
3
+ Each installation has its own Cloudflare account. Sign-in is OAuth: the agent invokes `cloudflared tunnel login` via Bash; the Cloudflare Authorize URL streams into the admin chat PTY and the native terminal renders it as a clickable link. Click it, authorise in your own browser, and `cloudflared` writes `cert.pem` to the brand's config directory. The agent never reads or mutates Cloudflare account state directly — whatever you see in your logged-in dashboard is the single source of truth. When something needs doing on the account side (adding a domain, deleting a stray entry, switching accounts), the agent relays the click-paths; you run them in your browser.
4
4
 
5
5
  ## Identity model
6
6
 
@@ -8,34 +8,25 @@ Each installation has its own Cloudflare account. Sign-in is OAuth: the setup sc
8
8
  |------|--------|
9
9
  | **Product identity** (Maxy vs Real Agent) | `brand.json` (`productName`, `configDir`) — known at install. |
10
10
  | **Cloudflare account identity** | `cert.pem` from OAuth. One account per brand per device. |
11
- | **Domain scope** (which zones the operator can route) | Live Cloudflare dashboard at script-invocation time via `list-cf-domains.sh`, not `brand.json`. Brand identity has no authority over which domains the operator's CF account holds. When the scrape returns an unexpected count (e.g. 1 on a two-zone account), the stream log's per-poll `phase=dom-scrape-poll n=<k> count=<n> domains=[…]` trajectory + the on-disk HTML dump at `~/{configDir}/logs/list-cf-domains-<ts>-count<n>-<mode>-pid<pid>.html` (earlier platform fixes — written on every scrape outcome, not just empty ones) give the operator everything they need to triage the cause without re-running. |
12
- | **CDP port the scrape attaches to** | `brand.json` (`cdpPort`, stamped at install time) — the same brand-scoped port `vnc.sh` binds Chromium to. `list-cf-domains.sh <brand>` requires the brand arg; the helper reads `${MAXY_PLATFORM_ROOT}/config/brand.json` (no silent default). Missing brand arg, missing brand.json, or missing `cdpPort` field each exit 1 with one of three named reasons (`brand-arg-missing`, `brand-config-missing`, `cdp-port-unresolved`); the agent reads the exit-code reason from stderr, names the brand, and stops — retrying would re-fire the same wrongly-resolved spawn. |
13
- | **Local tunnel state** | `~/{configDir}/cloudflared/` — `cert.pem`, `<UUID>.json`, `config.yml`, `tunnel.state`, `alias-domains.json`. |
11
+ | **Domain scope** (which zones the operator can route) | Live Cloudflare dashboard the operator picks the zone in the dashboard during OAuth or names it in chat. The agent does not enumerate zones programmatically. |
12
+ | **Local tunnel state** | `~/{configDir}/cloudflared/` `cert.pem`, `<UUID>.json`, `config.yml`, `alias-domains.json`. |
14
13
 
15
- There is no token-based auth for the operator-owned path (Mode A). To switch Cloudflare accounts, run `reset-tunnel.sh` (which deletes the cert and every tunnel on the current account), then run `setup-tunnel.sh` again — `cloudflared tunnel login` inside the setup script will pick a fresh account when you sign in.
14
+ There is no token-based auth for the operator-owned path (Mode A). To switch Cloudflare accounts, the agent runs the reset flow from `plugins/cloudflare/references/reset-guide.md` (deletes the cert and every tunnel on the current account), then the manual-setup flow again — `cloudflared tunnel login` picks a fresh account when you sign in.
16
15
 
17
16
  ## Setup flow
18
17
 
19
- Ask the agent to set up Cloudflare. The agent confirms the domain is already on your Cloudflare account (if not, it quotes the dashboard click-path — see below) and then collects the inputs in plain chat:
18
+ Ask the agent to set up Cloudflare. The agent confirms the domain is already on your Cloudflare account (if not, it quotes the dashboard click-path — see below) and collects the inputs in plain chat:
20
19
 
21
20
  - **Admin address** — the hostname that will serve the admin chat (e.g. `admin.yourdomain.com`).
22
21
  - **Public address** — optional hostname for the public agent (e.g. `public.yourdomain.com` or `chat.yourdomain.com`).
23
22
  - **Proxy apex** — optional bare-domain hostname (e.g. `yourdomain.com`) that should also serve the public agent.
24
23
  - **Admin password** — the password used to gate remote access to the admin surface.
25
24
 
26
- Once the agent has the four inputs, it sets the admin password via `curl -X POST http://127.0.0.1:${PORT}/api/remote-auth/set-password` (the same endpoint onboarding uses), optionally enumerates existing tunnels via `cloudflared tunnel list --output json` so you can pick from the logged-in account, and then invokes `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]` via the Bash tool with either `TUNNEL_ID=<uuid>` (selected) or `TUNNEL_NAME=<name>` (new) in env. After the script returns, the agent appends each non-`public.*` public or apex hostname to `~/{configDir}/alias-domains.json` so `isPublicHost()` classifies it as public.
25
+ The agent then sets the admin password via `curl -X POST http://127.0.0.1:${PORT}/api/remote-auth/set-password` (same endpoint the local onboarding form uses), and works through `plugins/cloudflare/references/manual-setup.md` Steps 1–7 directly via the Bash tool. `cloudflared`'s stdout streams into the PTY verbatim. The OAuth URL is linkified by the terminal; click it in your own browser to authorise. After the tunnel is up, the agent appends each non-`public.*` public or apex hostname to `~/{configDir}/alias-domains.json` so `isPublicHost()` classifies it as public, and starts the brand's cloudflared user service.
27
26
 
28
- The script runs end-to-end:
27
+ If any step's `cloudflared` invocation exits non-zero, the agent names the literal exit code, surfaces the stderr verbatim, and cites `reset-guide.md` for the next action — no retry under a different flag, no Playwright-driven dashboard inspection.
29
28
 
30
- - `cloudflared tunnel login` OAuth sign-in. The script prints the Cloudflare authorize URL; the admin UI surfaces it as a clickable link above the log. Click the link, pick the account that owns your domain, click Authorize. `cert.pem` lands; the script picks it up via its 180 s cert-poll regardless of which browser tab completed the click.
31
- - Tunnel resolution from operator-supplied identity (operator-selected-tunnel fix). The script requires EXACTLY ONE of `TUNNEL_ID` or `TUNNEL_NAME` in env and exits 1 with a literal error on either-both-set or neither-set. Pre-fix the script derived `${BRAND}-$(hostname -s)` locally; that broke the operator-state-is-authoritative doctrine and silently created orphan tunnels whenever the device hostname changed. Stream log emits `step=tunnel-resolve source=operator-selected|operator-created tunnel_id=… tunnel_name=…` once the UUID is known.
32
- - **Zone pre-flight** — for every non-apex hostname the script queries `1.1.1.1` for the registrable parent's NS records and refuses the whole run if they don't point at Cloudflare. Stream log: `step=zone-preflight result=ok|error zones_on_account=… missing_parent_for=…`. Catches "domain not on Cloudflare"; does not catch "domain on a different Cloudflare account than `cert.pem` is bound to" — that case surfaces later via `tunnel-status`.
33
- - `cloudflared tunnel route dns` for each subdomain hostname. Apex hostnames cannot be routed this way — the script prints an **ACTION REQUIRED** block naming the exact dashboard record to add or edit. Stream log emits `step=route-dns hostname=… tunnel_id=…` before the call and `step=route-dns hostname=… result=ok|apex-skip|error` after; on error the bounded cloudflared stderr (≤400 chars) rides in the same phase line. **The script does not parse cloudflared's stdout** — exit code is the sole decision signal, so all three legitimate cloudflared output shapes (new record, overwrite, idempotent "already configured") are treated as success.
34
- - `config.yml` and `tunnel.state` written under `${CFG_DIR}`.
35
- - `systemctl --user restart ${BRAND}.service` — restarts the platform service so the new tunnel spawns via the service's `ExecStartPre=resume-tunnel.sh`. The restart fires a few seconds after the script exits so the script does not kill its own cgroup when invoked via the Bash tool; the chat UI receives a `server_shutdown` SSE frame and reconnects automatically.
36
- - Post-restart verification — `ps -ef | grep '[c]loudflared'` confirms the connector is alive, then `curl -I https://<hostname>` against each subdomain (up to 60 s per host) confirms a non-530 response.
37
-
38
- The agent streams the script's stdout into chat verbatim as it arrives, including any `ACTION REQUIRED` block. If the script exits non-zero, the agent names the literal exit code and cites `reset-guide.md` for the next action — no retry under a different flag, no Playwright-driven dashboard inspection.
29
+ The setup-done claim only fires after the agent runs `curl -I https://<admin-hostname>` from outside the local network and the response shows a `200` line. That HTTP response is the only success terminal.
39
30
 
40
31
  ## Getting a domain on Cloudflare
41
32
 
@@ -49,19 +40,19 @@ Existing website traffic continues to work during and after the switch. Only DNS
49
40
 
50
41
  ## Reset / account switch
51
42
 
52
- Ask the agent to reset Cloudflare. The agent invokes `reset-tunnel.sh` on the device:
43
+ Ask the agent to reset Cloudflare. The agent executes the reset flow from `plugins/cloudflare/references/reset-guide.md`:
53
44
 
54
45
  - Deletes every tunnel on the brand's current Cloudflare account (via the bound cert).
55
46
  - Wipes the brand's `${CFG_DIR}`.
56
- - Exits with the number of tunnels deleted.
47
+ - Stops the brand's cloudflared user service.
57
48
 
58
- The script does **not** stop token-mode connector processes, delete stray misrouted CNAMEs in the dashboard, or touch the platform service. If any of those apply (agent will flag them), the agent guides you through the manual cleanup — `pkill -f 'cloudflared.*tunnel run --token'` on the device, or deleting the stray CNAME in the dashboard.
49
+ The agent does **not** stop token-mode connector processes or delete stray misrouted CNAMEs in the dashboard. If any of those apply, the agent guides you through the manual cleanup — `pkill -f 'cloudflared.*tunnel run --token'` on the device, or deleting the stray CNAME in the dashboard.
59
50
 
60
51
  After reset, run setup again. The fresh `cloudflared tunnel login` will pick whichever Cloudflare account you sign into.
61
52
 
62
53
  ## Manual runbook
63
54
 
64
- When the scripted flow fails partway, or you want to walk through every step yourself, there's a step-by-step runbook at `plugins/cloudflare/references/manual-setup.md`. The runbook mirrors `setup-tunnel.sh` verbatim every scripted step corresponds to a numbered runbook step with isolated command blocks, success conditions, and troubleshooting. Pick up the runbook at whichever step the script failed in.
55
+ The step-by-step runbook at `plugins/cloudflare/references/manual-setup.md` is the contract the agent follows. It is also what an operator runs by hand when needed every numbered step is an isolated `cloudflared` command block with success conditions and troubleshooting.
65
56
 
66
57
  ## Dashboard operations the CLI cannot do
67
58
 
@@ -71,17 +62,17 @@ The CLI cannot add a domain, switch accounts, edit an apex CNAME, or delete stra
71
62
 
72
63
  ### Tunnel won't start
73
64
 
74
- Ask the agent to check. The agent reads `systemctl --user status ${BRAND}.service` and `~/{configDir}/cloudflared/logs/cloudflared.log`. Common states:
65
+ Ask the agent to check. The agent reads `systemctl --user status ${BRAND}-cloudflared.service` and the cloudflared log under `~/{configDir}/cloudflared/`. Common states:
75
66
 
76
- - **No cloudflared process running** — the service's `ExecStartPre=resume-tunnel.sh` exited cleanly but did not spawn the connector. Usually means `tunnel.state` is missing or `config.yml` was written with empty variables. The agent will re-run `setup-tunnel.sh`.
77
- - **`tunnel not found`** — the UUID in `config.yml` does not match any tunnel on the currently-bound account. Usually follows an account switch that didn't reset local state. The agent invokes `reset-tunnel.sh` and then a fresh setup.
67
+ - **No cloudflared process running** — the cloudflared service exited or never started. The agent runs the manual-setup flow to re-issue tunnel creation.
68
+ - **`tunnel not found`** — the UUID in `config.yml` does not match any tunnel on the currently-bound account. Usually follows an account switch that didn't reset local state. The agent runs the reset flow and then a fresh setup.
78
69
 
79
70
  ### URL returns 530
80
71
 
81
72
  DNS propagation or account mismatch. Wait 30–60 seconds and retry first. If the 530 persists:
82
73
 
83
- - The domain may be on a Cloudflare account different from the one `cert.pem` is bound to — re-running `setup-tunnel.sh` re-validates.
84
- - The UDP buffer for QUIC may be undersized on this device — check `cloudflared.log` for `failed to sufficiently increase receive buffer size`.
74
+ - The domain may be on a Cloudflare account different from the one `cert.pem` is bound to — the agent re-runs the manual setup steps to re-validate.
75
+ - The UDP buffer for QUIC may be undersized on this device — check the cloudflared log for `failed to sufficiently increase receive buffer size`.
85
76
 
86
77
  ### URL returns connection refused
87
78
 
@@ -104,8 +95,8 @@ The most common cause is wrong nameservers on the domain. The domain must use Cl
104
95
 
105
96
  ## What the agent does and does not do
106
97
 
107
- **Does:** invokes `setup-tunnel.sh` / `reset-tunnel.sh` via Bash, quotes click-paths from the reference files verbatim, verifies external reachability with `curl -I`.
98
+ **Does:** invokes `cloudflared` directly via Bash, following `plugins/cloudflare/references/manual-setup.md` step by step; quotes click-paths from the reference files verbatim; verifies external reachability with `curl -I` and surfaces the response.
108
99
 
109
- **Does not:** drive the Cloudflare dashboard via Playwright, synthesise alternative `cloudflared` flag sequences, call any Cloudflare API or SDK, write or edit `cert.pem` / `tunnel.state` / `config.yml` / `alias-domains.json` directly.
100
+ **Does not:** drive the Cloudflare dashboard via Playwright, synthesise alternative `cloudflared` flag sequences not in the runbook, call any Cloudflare API or SDK, write or edit `cert.pem` / `config.yml` directly outside the runbook's instructions.
110
101
 
111
- When a script or command fails, the agent reports the failure and cites the relevant recovery step. It does not improvise.
102
+ When a command fails, the agent reports the failure and cites the relevant recovery step. It does not improvise.
@@ -111,7 +111,7 @@ The Data search panel ranks results by combining vector similarity with keyword
111
111
  Both flows run on the native Claude Code PTY surface in admin chat (Task 287). There is no in-app upgrade modal and no Cloudflare setup form — the agent invokes the relevant Bash command directly and its stdout streams into chat verbatim.
112
112
 
113
113
  - **Software update.** Re-run the installer (`npx -y @rubytech/create-<brand>@latest`) from a shell; HeaderMenu's version row turns sage when `installed === latest`.
114
- - **Cloudflare setup.** Operator asks in chat; the agent invokes `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]` via the Bash tool. The script's `[setup-tunnel] step=<phase> …` phase lines and cloudflared's own stdout stream into the PTY; the `OAUTH_URL: <url>` line is linkified by the terminal so the operator clicks it and authorises Cloudflare in their own browser.
114
+ - **Cloudflare setup.** Operator asks in chat; the agent invokes `cloudflared` directly via the Bash tool, following the numbered steps in `plugins/cloudflare/references/manual-setup.md`. cloudflared's stdout and stderr stream into the PTY; the OAuth URL printed by `cloudflared tunnel login` is linkified by the terminal so the operator clicks it and authorises Cloudflare in their own browser.
115
115
 
116
116
  **Mid-turn stream-drop banners.** If a chat turn ends abruptly the bubble shows one of two messages depending on what actually happened. You see "Server is restarting — reconnect will happen automatically." only when the app server itself emits the restart signal — typically during a Software Update or a Cloudflare setup that re-launches the brand service. You see "Lost connection — retrying." when your browser's connection to the Pi dropped mid-stream while the server was still up — typically a flaky Wi-Fi moment or the tunnel hiccupping. Either way the chat resumes once the connection is back; the previously-rendered messages stay on screen so you don't lose context.
117
117
 
@@ -152,7 +152,7 @@ After this, every `console.error("[your-tool]...")` from any tool in the plugin
152
152
 
153
153
  **How the tee decides which file to write to:** the platform sets `STREAM_LOG_PATH` as an environment variable on every MCP server spawn, pointing to the conversation-scoped stream log. The MCP server does not know about conversations — it just trusts `STREAM_LOG_PATH`. Multiple concurrent conversations produce multiple concurrent MCP server processes, each teeing to its own file; no cross-conversation leakage.
154
154
 
155
- **Bash scripts stream straight into the PTY.** Maxy Code's admin and public chat run on the native Claude Code PTY (Task 287). The per-conversation server-side stream log that the retired web-UI dispatcher tailed is gone; agent-invoked shell scripts (`setup-tunnel.sh`, `reset-tunnel.sh`, `list-cf-domains.sh`) print `[<scope>] <key>=<value>` phase lines directly to stdout, which the PTY renders in chat verbatim.
155
+ **Bash commands stream straight into the PTY.** Maxy Code's admin and public chat run on the native Claude Code PTY (Task 287). The per-conversation server-side stream log that the retired web-UI dispatcher tailed is gone; agent-invoked Bash commands (including direct `cloudflared` invocations for Cloudflare setup Task 288) print their stdout and stderr directly, and the PTY renders the output in chat verbatim.
156
156
 
157
157
  **Retrieve MCP diagnostic lines for a conversation:**
158
158
 
@@ -190,7 +190,7 @@ If the initial Cloudflare login fails during setup, {{productName}} will fall ba
190
190
  Both flows run on the native Claude Code PTY surface in admin chat (Task 287). The retired action-runner / terminal-modal troubleshooting sections that lived here have been removed because those surfaces no longer exist; failures now manifest as plain stderr from the agent-invoked Bash command, visible in chat.
191
191
 
192
192
  - **Software update.** Re-run `npx -y @rubytech/create-<brand>@latest` from a shell; if the installer fails, its stdout is the diagnostic record. HeaderMenu turns sage when `installed === latest`.
193
- - **Cloudflare setup.** The agent invokes `~/setup-tunnel.sh` via Bash. Failures surface as the script's own `[setup-tunnel] step=… result=error reason=…` lines in chat plus a non-zero exit. Recovery paths live in the cloudflare plugin's `references/reset-guide.md` and `references/manual-setup.md`.
193
+ - **Cloudflare setup.** The agent invokes `cloudflared` directly via Bash, following the cloudflare plugin's `references/manual-setup.md`. Failures surface as cloudflared's literal stderr plus a non-zero exit. Recovery paths live in `references/reset-guide.md` and `references/manual-setup.md`.
194
194
 
195
195
  ## Orphan Account Directory Archived to `.trash/`
196
196
 
@@ -2,7 +2,7 @@
2
2
  // Build-time gate: rejects `(Task NNN)` citations in operator-visible production
3
3
  // strings. Citations are allowed in inline comments and test titles/bodies only.
4
4
  //
5
- // Live repro that motivated the gate: setup-tunnel.sh on realagent 192.168.88.8
5
+ // Live repro that motivated the gate: a setup script on realagent 192.168.88.8
6
6
  // emitted `Re-run the installer to provision Chromium.` to the
7
7
  // operator's chat surface — the citation made the operator chase task
8
8
  // history rather than execute the verb. Operator surfaces strip citations;
@@ -10,7 +10,7 @@ Your job is to do useful work for the operator. How useful you can be depends on
10
10
 
11
11
  ## Before you speak
12
12
 
13
- Before your first message, do reconnaissance. Treat your starting knowledge as whatever your tools can surface, not what the system prompt names. Search the memory graph for what's known about the operator and their business, read the profile, check today's calendar, active projects, open work, and recent conversations. Infer the domain from the plugins and skills installed; they were chosen for a reason and are evidence of what this operation does.
13
+ Before your first message, do reconnaissance. Treat your starting knowledge as whatever your tools can surface, not what the system prompt names. Search the memory graph for what's known about the operator and their business, read the profile, check today's calendar, active projects, open work, and recent conversations. Infer the domain from the plugins and skills installed; they were chosen for a reason and are evidence of what this operation does. Check whether an upgrade is available for this install (the upgrade skill describes the lookup); if so, offer it in your first message.
14
14
 
15
15
  Then lead with what you found. When you have enough knowledge of the operator and their business to make a useful proposal, follow with one suited to where they are in their day; otherwise, gather more before proposing. Ask only the questions you genuinely could not answer through investigation. You are expected to drive the day, not to solicit instructions for it.
16
16
 
@@ -1,241 +0,0 @@
1
- // acceptance grid for tunnel-ingress.ts pure layer.
2
- //
3
- // Runs under `node --test --experimental-strip-types` (no compile step). No
4
- // filesystem reads except via temp files written by the test itself; no
5
- // network; no shell-outs. Mirrors apt-resolve / samba-provision style.
6
-
7
- import test from "node:test";
8
- import assert from "node:assert/strict";
9
- import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
10
- import { join } from "node:path";
11
- import { tmpdir } from "node:os";
12
- import {
13
- renderConfigYml,
14
- renderTunnelState,
15
- readPersistedHostnames,
16
- probeSambaStanza,
17
- renderAccessPolicyActionRequired,
18
- type IngressSpec,
19
- type TunnelState,
20
- } from "../tunnel-ingress.ts";
21
-
22
- // ---------------------------------------------------------------------------
23
- // renderConfigYml
24
- // ---------------------------------------------------------------------------
25
-
26
- test("renderConfigYml: HTTPS-only matches expected shape byte-for-byte", () => {
27
- const spec: IngressSpec = {
28
- tunnelId: "abc-123",
29
- credentialsPath: "/home/admin/.maxy/cloudflared/abc-123.json",
30
- httpPort: 19200,
31
- httpHostnames: ["admin.maxy.bot", "public.maxy.bot"],
32
- };
33
- assert.equal(
34
- renderConfigYml(spec),
35
- [
36
- "tunnel: abc-123",
37
- "credentials-file: /home/admin/.maxy/cloudflared/abc-123.json",
38
- "ingress:",
39
- " - hostname: admin.maxy.bot",
40
- " service: http://localhost:19200",
41
- " - hostname: public.maxy.bot",
42
- " service: http://localhost:19200",
43
- " - service: http_status:404",
44
- "",
45
- ].join("\n"),
46
- );
47
- });
48
-
49
- test("renderConfigYml: SSH+SMB appear AFTER http hostnames and BEFORE catch-all", () => {
50
- const spec: IngressSpec = {
51
- tunnelId: "t-id",
52
- credentialsPath: "/creds.json",
53
- httpPort: 19200,
54
- httpHostnames: ["admin.maxy.bot"],
55
- sshHostname: "ssh.maxy.bot",
56
- smbHostname: "smb.maxy.bot",
57
- };
58
- const out = renderConfigYml(spec);
59
- const idxHttp = out.indexOf("admin.maxy.bot");
60
- const idxSsh = out.indexOf("ssh.maxy.bot");
61
- const idxSmb = out.indexOf("smb.maxy.bot");
62
- const idx404 = out.indexOf("http_status:404");
63
- assert.ok(idxHttp < idxSsh, "HTTP before SSH");
64
- assert.ok(idxSsh < idxSmb, "SSH before SMB");
65
- assert.ok(idxSmb < idx404, "SMB before catch-all 404");
66
- assert.match(out, /\s+service: ssh:\/\/localhost:22$/m);
67
- assert.match(out, /\s+service: tcp:\/\/localhost:445$/m);
68
- });
69
-
70
- test("renderConfigYml: SSH only — no SMB block when smbHostname is null", () => {
71
- const spec: IngressSpec = {
72
- tunnelId: "t-id",
73
- credentialsPath: "/creds.json",
74
- httpPort: 19200,
75
- httpHostnames: ["admin.maxy.bot"],
76
- sshHostname: "ssh.maxy.bot",
77
- smbHostname: null,
78
- };
79
- const out = renderConfigYml(spec);
80
- assert.ok(out.includes("ssh.maxy.bot"));
81
- assert.ok(!out.includes("tcp://localhost:445"));
82
- });
83
-
84
- test("renderConfigYml: empty string hostnames are treated as absent", () => {
85
- const spec: IngressSpec = {
86
- tunnelId: "t-id",
87
- credentialsPath: "/creds.json",
88
- httpPort: 19200,
89
- httpHostnames: ["admin.maxy.bot"],
90
- sshHostname: "",
91
- smbHostname: "",
92
- };
93
- const out = renderConfigYml(spec);
94
- assert.ok(!out.includes("ssh://localhost:22"));
95
- assert.ok(!out.includes("tcp://localhost:445"));
96
- });
97
-
98
- // ---------------------------------------------------------------------------
99
- // renderTunnelState
100
- // ---------------------------------------------------------------------------
101
-
102
- test("renderTunnelState: base fields preserved when ssh/smb absent", () => {
103
- const state: TunnelState = {
104
- tunnelId: "abc",
105
- tunnelName: "maxy",
106
- domain: "admin.maxy.bot",
107
- configPath: "/home/admin/.maxy/cloudflared/config.yml",
108
- credentialsPath: "/home/admin/.maxy/cloudflared/abc.json",
109
- };
110
- const parsed = JSON.parse(renderTunnelState(state));
111
- assert.deepEqual(parsed, {
112
- tunnelId: "abc",
113
- tunnelName: "maxy",
114
- domain: "admin.maxy.bot",
115
- configPath: "/home/admin/.maxy/cloudflared/config.yml",
116
- credentialsPath: "/home/admin/.maxy/cloudflared/abc.json",
117
- });
118
- });
119
-
120
- test("renderTunnelState: ssh/smb hostnames persist when present", () => {
121
- const state: TunnelState = {
122
- tunnelId: "abc",
123
- tunnelName: "maxy",
124
- domain: "admin.maxy.bot",
125
- configPath: "/cfg.yml",
126
- credentialsPath: "/creds.json",
127
- sshHostname: "ssh.maxy.bot",
128
- smbHostname: "smb.maxy.bot",
129
- };
130
- const parsed = JSON.parse(renderTunnelState(state));
131
- assert.equal(parsed.sshHostname, "ssh.maxy.bot");
132
- assert.equal(parsed.smbHostname, "smb.maxy.bot");
133
- });
134
-
135
- // ---------------------------------------------------------------------------
136
- // readPersistedHostnames
137
- // ---------------------------------------------------------------------------
138
-
139
- test("readPersistedHostnames: returns nulls when file missing", () => {
140
- const got = readPersistedHostnames("/nope/does/not/exist.json");
141
- assert.deepEqual(got, { sshHostname: null, smbHostname: null });
142
- });
143
-
144
- test("readPersistedHostnames: round-trips ssh/smb hostnames from disk", () => {
145
- const dir = mkdtempSync(join(tmpdir(), "tunnel-state-"));
146
- const path = join(dir, "tunnel.state");
147
- try {
148
- writeFileSync(
149
- path,
150
- JSON.stringify({
151
- tunnelId: "abc",
152
- tunnelName: "maxy",
153
- domain: "admin.maxy.bot",
154
- configPath: "/cfg.yml",
155
- credentialsPath: "/creds.json",
156
- sshHostname: "ssh.maxy.bot",
157
- smbHostname: "smb.maxy.bot",
158
- }),
159
- );
160
- const got = readPersistedHostnames(path);
161
- assert.deepEqual(got, { sshHostname: "ssh.maxy.bot", smbHostname: "smb.maxy.bot" });
162
- } finally {
163
- rmSync(dir, { recursive: true, force: true });
164
- }
165
- });
166
-
167
- test("readPersistedHostnames: malformed JSON returns nulls (no throw)", () => {
168
- const dir = mkdtempSync(join(tmpdir(), "tunnel-state-bad-"));
169
- const path = join(dir, "tunnel.state");
170
- try {
171
- writeFileSync(path, "not json");
172
- assert.deepEqual(readPersistedHostnames(path), { sshHostname: null, smbHostname: null });
173
- } finally {
174
- rmSync(dir, { recursive: true, force: true });
175
- }
176
- });
177
-
178
- // ---------------------------------------------------------------------------
179
- // probeSambaStanza
180
- // ---------------------------------------------------------------------------
181
-
182
- test("probeSambaStanza: true when [<brand>] header present", () => {
183
- const dir = mkdtempSync(join(tmpdir(), "smb-probe-"));
184
- const path = join(dir, "smb.conf");
185
- try {
186
- writeFileSync(path, "[global]\nworkgroup = WORKGROUP\n\n[maxy]\n path = /home/admin\n");
187
- assert.equal(probeSambaStanza("maxy", path), true);
188
- } finally {
189
- rmSync(dir, { recursive: true, force: true });
190
- }
191
- });
192
-
193
- test("probeSambaStanza: false when only peer brand present (isolation)", () => {
194
- const dir = mkdtempSync(join(tmpdir(), "smb-probe-peer-"));
195
- const path = join(dir, "smb.conf");
196
- try {
197
- writeFileSync(path, "[global]\n\n[realagent]\n path = /home/admin\n");
198
- assert.equal(probeSambaStanza("maxy", path), false);
199
- } finally {
200
- rmSync(dir, { recursive: true, force: true });
201
- }
202
- });
203
-
204
- test("probeSambaStanza: false when smb.conf missing", () => {
205
- assert.equal(probeSambaStanza("maxy", "/nope/smb.conf"), false);
206
- });
207
-
208
- test("probeSambaStanza: brand names with regex meta characters are escaped", () => {
209
- const dir = mkdtempSync(join(tmpdir(), "smb-probe-regex-"));
210
- const path = join(dir, "smb.conf");
211
- try {
212
- writeFileSync(path, "[some.brand]\n path = /x\n");
213
- assert.equal(probeSambaStanza("some.brand", path), true);
214
- assert.equal(probeSambaStanza("someXbrand", path), false);
215
- } finally {
216
- rmSync(dir, { recursive: true, force: true });
217
- }
218
- });
219
-
220
- // ---------------------------------------------------------------------------
221
- // renderAccessPolicyActionRequired
222
- // ---------------------------------------------------------------------------
223
-
224
- test("renderAccessPolicyActionRequired: empty string when neither hostname set", () => {
225
- assert.equal(
226
- renderAccessPolicyActionRequired({ operatorEmail: "x@y.com" }),
227
- "",
228
- );
229
- });
230
-
231
- test("renderAccessPolicyActionRequired: lists each hostname with operator email", () => {
232
- const out = renderAccessPolicyActionRequired({
233
- sshHostname: "ssh.maxy.bot",
234
- smbHostname: "smb.maxy.bot",
235
- operatorEmail: "joel@example.com",
236
- });
237
- assert.match(out, /SSH: ssh\.maxy\.bot/);
238
- assert.match(out, /SMB: smb\.maxy\.bot/);
239
- assert.match(out, /Emails: joel@example\.com/);
240
- assert.match(out, /Zero Trust → Access → Applications/);
241
- });
@@ -1,98 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Deterministic scrape of the operator's Cloudflare dashboard to discover
3
- # the domains on the logged-in account. Output on stdout is a JSON `string[]`,
4
- # sorted and deduped. Every failure exits non-zero with a `reason=<enum>`
5
- # token on stderr tagged `[list-cf-domains]`.
6
- #
7
- # Usage:
8
- # list-cf-domains.sh <brand>
9
- #
10
- # brand arg is REQUIRED — the prior silent default (string
11
- # fallback to "maxy" when $1 was empty) made every non-Maxy brand's CF
12
- # setup form fail on first use because the wrapper's child node helper
13
- # read the Maxy CDP port from a hardcoded fallback. Missing arg now exits
14
- # 1 with `phase=error reason=brand-arg-missing`.
15
- #
16
- # The wrapper also resolves and exports MAXY_PLATFORM_ROOT from its own
17
- # script location so the .ts helper can read `<root>/config/brand.json` for
18
- # the brand's `cdpPort` (the source of truth). Direct-SSH invocation
19
- # works because the script lives at `<install>/platform/plugins/cloudflare/
20
- # scripts/`, making platform root three dirs up from the resolved script.
21
- #
22
- # Runtime: Node 22's `--experimental-strip-types` runs the .ts helper
23
- # directly, so no tsx / playwright / ws dependency exists.
24
-
25
- set -euo pipefail
26
-
27
- # Resolve symlinks before dirname — ~/list-cf-domains.sh is installed as a
28
- # symlink into $HOME, so the raw BASH_SOURCE[0] points at $HOME, not the
29
- # scripts directory.
30
- SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
31
-
32
- # Phase-line helper. Stream-log plumbing retired in Task 287 — every
33
- # observation goes straight to stdout for the PTY to render.
34
- phase_line() {
35
- local scope=$1
36
- shift
37
- printf '[%s] %s\n' "${scope}" "$*"
38
- }
39
-
40
- if [ "$#" -lt 1 ] || [ -z "${1:-}" ]; then
41
- phase_line list-cf-domains phase=error reason=brand-arg-missing
42
- exit 1
43
- fi
44
-
45
- BRAND="${1}"
46
- CONFIG_DIR=".${BRAND}"
47
- mkdir -p "${HOME}/${CONFIG_DIR}/logs"
48
-
49
- # MAXY_PLATFORM_ROOT may be set by the calling environment. Direct-from-Bash
50
- # invocation has no such env so derive it from the resolved script location:
51
- # scripts/ → cloudflare/ → plugins/ → platform → install root (three dirs up).
52
- if [ -z "${MAXY_PLATFORM_ROOT:-}" ]; then
53
- MAXY_PLATFORM_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
54
- fi
55
-
56
- phase_line list-cf-domains phase=script-start brand="${BRAND}" config_dir="${CONFIG_DIR}"
57
-
58
- TS_ENTRY="${SCRIPT_DIR}/list-cf-domains.ts"
59
-
60
- if [ ! -f "${TS_ENTRY}" ]; then
61
- phase_line list-cf-domains phase=error reason=entry-missing path="${TS_ENTRY}"
62
- exit 1
63
- fi
64
-
65
- # 30s hard ceiling. The node helper has its own per-phase budgets totalling
66
- # ~27s; the wrapper's extra 3s absorbs process startup and CF network jitter.
67
- # Use `timeout --preserve-status` so the exit code of a timeout is distinct
68
- # (124) from a helper-reported failure (1).
69
- HARD_TIMEOUT_SECS=30
70
-
71
- # Env passed through:
72
- # - CONFIG_DIR — consumed by the node helper when writing selector-drift
73
- # dumps to ~/.${BRAND}/logs/list-cf-domains-<ts>.html.
74
- # - BRAND — names the brand for log lines naming the brand on
75
- # config-class failures (helper does not derive it from
76
- # CONFIG_DIR to keep the two concerns independent).
77
- # - MAXY_PLATFORM_ROOT — install root the helper reads `config/brand.json`
78
- # from for `cdpPort`. Resolved above so direct-SSH
79
- # invocation matches the systemd-spawned path.
80
- #
81
- # `node --experimental-strip-types` (stable in Node 22.22) runs the .ts file
82
- # natively: type annotations are stripped, no enums / decorators are used, so
83
- # no TS type-transform is needed. `--no-warnings` suppresses the experimental
84
- # banner which would otherwise leak into stderr and confuse the route parser.
85
- set +e
86
- CONFIG_DIR="${CONFIG_DIR}" BRAND="${BRAND}" MAXY_PLATFORM_ROOT="${MAXY_PLATFORM_ROOT}" \
87
- timeout --preserve-status --signal=TERM "${HARD_TIMEOUT_SECS}" \
88
- node --experimental-strip-types --no-warnings "${TS_ENTRY}"
89
- EXIT_CODE=$?
90
- set -e
91
-
92
- if [ "${EXIT_CODE}" -eq 124 ]; then
93
- phase_line list-cf-domains phase=script-exit code=124 reason=wrapper-timeout budget_secs="${HARD_TIMEOUT_SECS}"
94
- exit 1
95
- fi
96
-
97
- phase_line list-cf-domains phase=script-exit code="${EXIT_CODE}"
98
- exit "${EXIT_CODE}"