@rubytech/create-realagent 1.0.618 → 1.0.620

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 (24) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -1
  3. package/payload/platform/lib/mcp-stderr-tee/dist/index.js +11 -5
  4. package/payload/platform/lib/mcp-stderr-tee/dist/index.js.map +1 -1
  5. package/payload/platform/lib/mcp-stderr-tee/src/index.ts +10 -5
  6. package/payload/platform/plugins/admin/mcp/dist/lib/review-tools.d.ts.map +1 -1
  7. package/payload/platform/plugins/admin/mcp/dist/lib/review-tools.js +55 -6
  8. package/payload/platform/plugins/admin/mcp/dist/lib/review-tools.js.map +1 -1
  9. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +11 -7
  10. package/payload/platform/plugins/cloudflare/PLUGIN.md +10 -13
  11. package/payload/platform/plugins/cloudflare/mcp/dist/index.js +94 -1030
  12. package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
  13. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts +62 -258
  14. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
  15. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +297 -882
  16. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
  17. package/payload/platform/plugins/cloudflare/mcp/package.json +3 -7
  18. package/payload/platform/plugins/cloudflare/references/setup-guide.md +51 -77
  19. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +23 -81
  20. package/payload/platform/plugins/docs/PLUGIN.md +1 -1
  21. package/payload/platform/plugins/docs/references/cloudflare.md +21 -30
  22. package/payload/platform/templates/specialists/agents/personal-assistant.md +9 -9
  23. package/payload/server/server.js +161 -11
  24. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-binding.test.ts +0 -195
@@ -1,84 +1,42 @@
1
1
  ---
2
2
  name: setup-tunnel
3
- description: Set up a Cloudflare Tunnel using deterministic MCP tools. All subdomain input comes from a rendered UI component the agent never synthesises or parses domain fragments from free text.
3
+ description: Set up a Cloudflare Tunnel. The agent coaches the operator through the Cloudflare dashboard and runs cloudflared CLI commands; it never reads or mutates Cloudflare account state.
4
4
  ---
5
5
 
6
6
  # Set Up Cloudflare Tunnel
7
7
 
8
- Create a Cloudflare Tunnel so the platform is accessible from the public internet. `cloudflare-setup` is the single orchestrator. Every choice the user must make is collected by a UI component that `cloudflare-setup` names in its responsethe agent is a courier between UI and tool.
8
+ Create a Cloudflare Tunnel so the platform is reachable from the public internet. The operator drives the Cloudflare dashboard; the agent drives `cloudflared`. There is no API path in this codebase, no SDK, no account-state enumerationthose capabilities were removed because every prior attempt at them ended up signing the laptop into the wrong Cloudflare account.
9
9
 
10
10
  ## Rules of engagement
11
11
 
12
- - **Never ask the user for a domain, subdomain, FQDN, tunnel name, or label in chat.** Those fields come only from rendered components.
13
- - **Never synthesise tool parameters from free text.** If the user types `rogerblack.maxy.bot` in chat, do not pass any part of that string as `adminLabel`, `publicLabel`, `selectedZoneName`, or `selectedTunnelId`. Render the component, let the UI collect the value.
14
- - **Never retry `cloudflare-setup` with different parameter values on error.** Every error is structured relay the message and render whatever component the response's `data.render` field names.
15
- - **Treat every `awaiting_*` status as a render instruction.** Read `data.render`, call `render-component` with its `name` and `data` verbatim, then stop. Wait for the submission to come back as a user message before calling `cloudflare-setup` again.
16
-
17
- ## Identity and scope
18
-
19
- The bound Cloudflare account (via `cert.pem`) is the universe. Every routable zone must be a zone on that account. There is no brand-declared allowlist; whatever the account holds is fair game. The agent owns the bound account absolutely — it can add, delete, restructure as needed.
20
-
21
- Three checks the tools enforce automatically:
22
-
23
- 1. **Account binding.** `cert.pem`'s account ID must equal the one recorded in `~/{configDir}/cloudflare/account-binding.json`. Drift refuses with `reason=account-drift`. A device with no binding refuses every non-login operation with `reason=unbound-device`. The single recovery is `tunnel-login` (with `force=true` to switch accounts).
24
- 2. **Live account-zone scope.** Before writing any DNS record, the requested hostname's registrable parent must be an active zone on the bound account. Refuse with `reason=scope-mismatch` otherwise — the fix is `cf-add-zone <domain>` or pick a hostname under an existing zone.
25
- 3. **Post-flight FQDN.** The FQDN `cloudflared` actually wrote must equal the requested hostname. A mismatch refuses with `reason=post-flight-fqdn-mismatch`, attempts cleanup, and leaves recovery to `cf-rebuild`.
26
-
27
- There is no API-token path.
28
-
29
- ## User-facing language
30
-
31
- Never say cert.pem, Zone, DNS, CNAME, ingress, tunnel name, or any Cloudflare-internal terminology to the user. Use plain language: "domain", "address", "remote access", "connection".
12
+ - **The dashboard is the source of truth.** The operator's logged-in Cloudflare session is authoritative for which domains exist, which account owns them, and which addresses are already in use. The agent never second-guesses this by querying an API — there is no API path.
13
+ - **Speak dashboard language.** Say "Cloudflare account", "domain", "address", "sign in", "browser". Never say "zone", "CNAME", "account ID", "API", "SDK", or any hexadecimal identifier. Internal logs may use those terms; operator-facing text must not.
14
+ - **Every failure surfaces a dashboard instruction.** When something is wrong, the recovery is always "open Cloudflare in your browser and do X", never "let me call another API".
15
+ - **The operator confirms before the agent re-runs.** After asking the operator to do something in the dashboard, wait for explicit confirmation ("done", "ok", "yes") before the next tool call.
32
16
 
33
17
  ## The flow
34
18
 
35
- 1. **Show the browser.** Call `render-component` with `name: "browser-viewer"` and `data: { title: "Cloudflare" }`. Skip when the viewer is already visible.
36
-
37
- 2. **Call `cloudflare-setup` with no arguments** the first time. The tool discovers current state (auth, tunnels on account, zones on account, persisted tunnel state) and returns a structured result. Parse `status` and act according to the table below.
19
+ 1. **Confirm the operator has a Cloudflare account.** If they do not, tell them: "You will need a Cloudflare account. Sign up free at cloudflare.com, come back and tell me when you are signed in." When they confirm, continue.
38
20
 
39
- ## Status table
21
+ 2. **Confirm the operator's domain is on their Cloudflare account.** Ask: "What domain do you want to use, and is it already on Cloudflare?" If they say no: tell them to open Cloudflare in their browser, go to Websites → Add a site, enter the domain, and follow the dashboard's instructions. The dashboard walks through nameserver changes — the agent does not need to understand the details. When the domain shows as Active in the dashboard, the operator confirms and the agent continues.
40
22
 
41
- | Returned `status` | What to do |
42
- |-------------------------------|-----------|
43
- | `awaiting_auth` | Call `browser_navigate` with `data.authUrl`, then `browser_tabs` with `action: "select"` to bring it front. Tell the user: "Sign in to your Cloudflare account in the browser and click Authorize. Let me know when you're done." Stop. When they confirm, call `cloudflare-setup` again with no arguments. |
44
- | `awaiting_nameservers` | Relay the message verbatim. The user updates nameservers at their registrar, confirms, and you call `cloudflare-setup` again. |
45
- | `awaiting_tunnel_selection` | The account has existing tunnels. Call `render-component` with `data.render` verbatim. When the user submits, the payload is `{"selectedTunnelId":"<value>"}`. Call `cloudflare-setup` with `{ selectedTunnelId: "<value>" }`. The literal value `"__new__"` means the user chose to create a new tunnel. |
46
- | `awaiting_zone_selection` | More than one active domain on the account. Call `render-component` with `data.render` verbatim. When submitted, payload is `{"selectedZoneName":"<value>"}`. Call `cloudflare-setup` with `{ selectedZoneName: "<value>" }`. |
47
- | `awaiting_zone_add_dashboard` | Zero domains on the account. Relay the message. The user adds a domain at `cloudflare.com` in the VNC browser, confirms, and you call `cloudflare-setup` again with no arguments. |
48
- | `awaiting_cleanup_confirmation` | The bound Cloudflare account has artefacts that don't belong to the chosen domain (other zones, other tunnels, stale DNS pointing to dead tunnels). Call `render-component` with `data.render` verbatim — this is a `confirm` component listing what will be deleted. When the user approves, the payload is a JSON string echoing `selectedZoneName` plus `cleanupConfirmed: true`. Call `cloudflare-setup` with the payload's fields. When the user rejects, relay the rejection and stop — do not retry with different parameters. |
49
- | `awaiting_labels` | Call `render-component` with `data.render` verbatim — this is the `tunnel-route-picker`. When the user submits, the payload is `{"adminLabel":"...","publicLabel":"..."}` (public may be absent). Call `cloudflare-setup` with `{ adminLabel: ..., publicLabel?: ... }`. |
50
- | `tunnel-name-taken` | The chosen admin address is already used by another device on the same account. Relay the message and call `render-component` with `data.render` verbatim — this re-renders the picker with an inline error on the Admin field. Proceed as for `awaiting_labels` when the user resubmits. |
51
- | `label-taken` | DNS for one of the requested addresses is already owned by another device. Relay the message and call `render-component` with `data.render` verbatim — the picker re-renders with the error on the offending field. |
52
- | `awaiting_password` | Relay the message. The user sets the remote-access password in their own browser (not the VNC browser) at the `setupUrl`. Wait, then call `cloudflare-setup` again. **Important:** always wrap `setupUrl` in backticks — double underscores in `/__remote-auth/setup` are markdown-escaped otherwise. |
53
- | `complete` | Relay the message with the admin URL (and public URL if present). Done. |
54
- | `error` | Relay the message. Do not retry with mutated parameters. |
23
+ 3. **Run `tunnel-login`.** The tool opens a Cloudflare sign-in URL in the VNC browser. Tell the operator: "Sign in to the Cloudflare account that owns `<domain>`. Pick the right account if you have more than one — the name is in the top-left of the Cloudflare page. Click Authorize when ready." When they confirm, run `tunnel-login` again to record the sign-in.
55
24
 
56
- ## Prerequisites
25
+ 4. **Run `tunnel-create`.** Ask the operator what sub-addresses they want (default: `admin.<domain>` and `public.<domain>`). The tool creates the tunnel and routes both addresses via `cloudflared`. Refusals land here:
26
+ - **`hostname-zone-not-routable`**: the domain is not on the signed-in Cloudflare account. Relay: "This laptop is signed into a Cloudflare account that does not have `<domain>`. Open Cloudflare in your browser — the name in the top-left is the account this laptop will talk to. If that is not the account that owns `<domain>`, switch to the right account using the dropdown, then tell me."
27
+ - **`post-flight-fqdn-mismatch`**: `cloudflared` wrote the address under a different domain than asked. Same underlying cause (wrong account signed in). Relay the same dashboard instruction, and also ask the operator to delete the stray record from the other account — the agent cannot clean it up.
57
28
 
58
- Before starting, call `tunnel-status` to assess current state.
29
+ 5. **Run `tunnel-enable`.** Starts the tunnel daemon and verifies the admin URL is reachable through Cloudflare. Reports success with the live URLs.
59
30
 
60
- - **Tunnel already running with no DNS warnings:** Confirm to the user and stop.
61
- - **Tunnel running but DNS NOT RESOLVING warning:** Call `cloudflare-setup` — it will re-route DNS idempotently.
62
- - **cloudflared not installed:** `cloudflare-setup` installs it automatically.
63
- - **`bound: false` (cert/binding missing or mismatched):** Full flow required. Call `cloudflare-setup` and follow the status table.
31
+ 6. **Run `tunnel-status`.** The e2e probe should report `healthy: true`. If `boundAccountOwnsHostnames` is `false`, stop — the tunnel is running on this laptop but nothing from the internet is reaching it. Relay the dashboard-switch instruction from step 4.
64
32
 
65
- ## Add alias domain
33
+ ## Adding an alias address
66
34
 
67
- An alias domain (e.g. `maxy.chat`) serves the public chat directly the URL stays as the alias domain in the browser bar. Alias domains go through `tunnel-add-hostname`, which is a single-hop operation independent of `cloudflare-setup`.
35
+ An alias address (e.g. `maxy.chat`) serves the public chat directly. The domain must already be on the same Cloudflare account the laptop is signed into.
68
36
 
69
- ### Prerequisites
70
-
71
- - A working tunnel (the main setup flow must be complete)
72
- - The alias domain must be an Active zone on the Cloudflare account
73
- - The tunnel ID (from `tunnel-status`)
74
-
75
- ### Flow
76
-
77
- 1. Confirm the alias domain with the user.
78
- 2. Call `cf-zone-status`. The alias domain must appear as Active. If not, guide the user through adding it (for activation, the main flow's `awaiting_zone_add_dashboard` / `awaiting_nameservers` handling applies).
79
- 3. Call `tunnel-status` for the tunnel ID.
80
- 4. Call `tunnel-add-hostname` with the alias domain and tunnel ID.
81
- 5. Verify with `dns-lookup`. Tell the user: "Your public chat is now live at https://{alias_domain}." DNS propagation takes 1-5 minutes.
37
+ 1. Ask the operator: "Is `<alias>` on the same Cloudflare account this laptop is signed into?" If not, guide them to add it via Websites → Add a site in the dashboard, wait for Active, then continue.
38
+ 2. Run `tunnel-add-hostname` with the alias and the tunnel UUID from `tunnel-status`. Refusals surface the same dashboard instructions as step 4.
39
+ 3. Verify with `tunnel-status`. DNS propagation takes 1-5 minutes.
82
40
 
83
41
  ## Reinstalling / upgrading the platform
84
42
 
@@ -86,27 +44,11 @@ When re-running the installer, pass `--hostname` and `--port` to preserve the de
86
44
 
87
45
  1. Call `system-status` to get the current `hostname` and `port`.
88
46
  2. Derive the brand package (e.g., `Maxy` → `@rubytech/create-maxy`, `Real Agent` → `@rubytech/create-realagent`).
89
- 3. Run: `npx -y @rubytech/create-realagent --hostname muvin --port 19400`
90
-
91
- Without these flags, the installer falls back to detection which is unreliable when the OS hostname was previously reset to a brand default.
92
-
93
- ## Diagnosis and recovery
94
-
95
- When something is wrong (refusal, dead URL, operator reports trouble):
96
-
97
- - **`cf-verify`** — non-mutating audit. Returns the bound account's zones, tunnels, and CNAMEs plus device-side cert / binding / tunnel state. Also returns `pollution` (default no-intent view: account artefacts the device doesn't reference). Run this first; works in any state.
98
- - **`cf-rebuild`** — nuclear cleanup of the bound account. Deletes every tunnel, CNAME, and zone NOT in the `preserve` set. Defaults `preserve` to whatever the device's `tunnel.state` and `alias-domains.json` reference. When `preserve.cnames` is unset, CNAMEs on preserved zones survive EXCEPT those pointing to `<deletedTunnelId>.cfargotunnel.com` (stale tunnel pointers are always scrubbed). Pass explicit `preserve.zones`, `preserve.tunnelIds`, or `preserve.cnames` to override. Refuses with `reason=no-intent` when called with no `preserve` AND no device-persisted intent — pass an explicit empty preserve (`{ zones: [], tunnelIds: [] }`) to state "nuke everything". `dryRun=true` plans without mutating. The agent owns the bound account absolutely — anything not in `preserve` is treated as junk.
99
-
100
- Recovery vocabulary by refusal reason:
101
- - `account-drift` or `unbound-device` → `tunnel-login force=true` (clears cert + binding) then re-authenticate.
102
- - `scope-mismatch` → `cf-add-zone <domain>` (if the user owns the domain), or pick a hostname under an existing zone.
103
- - `post-flight-fqdn-mismatch` → `cf-rebuild` to reconcile.
104
- - `no-intent` → pass explicit `preserve` when calling `cf-rebuild` directly, or use `cloudflare-setup` (which always passes explicit intent).
47
+ 3. Run: `npx -y @rubytech/create-maxy --hostname muvin --port 19400`
105
48
 
106
49
  ## Important
107
50
 
108
- - **`cloudflare-setup` is the onboarding orchestrator.** Every branch decision is already encoded in its returned `status`. Do not call `cf-add-zone` or `tunnel-enable` directly from this flow `cloudflare-setup` invokes them as needed.
109
- - **Never interact with Cloudflare dashboard forms.** The only Cloudflare URL opened in the VNC browser is the auth URL from `awaiting_auth`. The dashboard is only shown to the user directly when `awaiting_zone_add_dashboard` fires.
110
- - **The user signs in themselves.** Open the URL, tell the user what to do, wait. Do not evaluate the page.
111
- - **All tunnel MCP tools are idempotent.** Safe to call after any partial failure — they read filesystem state top-to-bottom every call.
51
+ - **Never interact with Cloudflare dashboard forms on the operator's behalf.** The only Cloudflare URL opened in the VNC browser is the sign-in URL from `tunnel-login`. Everything else in the dashboard, the operator does themselves.
52
+ - **The operator signs in themselves.** Open the URL, tell them what to do, wait. The agent does not evaluate the page.
53
+ - **The tunnel tools are idempotent.** Safe to call after any partial failure they read filesystem state top-to-bottom on every call.
112
54
  - **DNS propagation takes 1-5 minutes.** Nameserver propagation takes minutes to 24 hours.
@@ -30,7 +30,7 @@ Load these when performing admin tasks or diagnosing platform behaviour:
30
30
 
31
31
  - **Platform architecture** → `references/platform.md` — how the platform works, agent types, the plugin model
32
32
  - **Platform internals** → `references/internals.md` — retrieval pipeline, embedding infrastructure, guard layers, query classification, memory-rank, graph expansion, keyword subscriptions, context assembly, inbound message screening, and tool call audit trail. Load when answering architecture questions, assessing whether a capability exists, diagnosing retrieval behaviour, or reviewing security and privacy features.
33
- - **Cloudflare** → `references/cloudflare.md` — declarative brand-zone scope, the four-step guard, `cf-verify` / `cf-rebuild` recovery flow, how to add a zone for a brand.
33
+ - **Cloudflare** → `references/cloudflare.md` — dashboard-first tunnel setup, the `cloudflared`-CLI-only tool surface, single-recovery-path (re-login) for every wrong-account failure.
34
34
  - **Deployment** → `references/deployment.md` — Pi setup, Cloudflare tunnel, start script
35
35
 
36
36
  ## References
@@ -1,51 +1,42 @@
1
- # Cloudflare Tunnel — bound account is the universe
1
+ # Cloudflare Tunnel — the dashboard is the source of truth
2
2
 
3
- Each installation has its own Cloudflare account. The user signs in via OAuth (`tunnel-login`); `cloudflared` writes `cert.pem`. The plugin binds to that account on first successful login (`account-binding.json`) and refuses to operate if the cert is later rotated under a different account.
3
+ Each installation has its own Cloudflare account. The operator signs into it via OAuth (`tunnel-login`); `cloudflared` writes `cert.pem` locally. The plugin records which account that sign-in landed on (`account-binding.json`) and refuses if the operator later signs in under a different account without explicit intent.
4
4
 
5
- The agent has absolute authority over the bound account: it can add zones, delete zones, create or delete tunnels, write or remove CNAMEs. There is no inherited "brand zone" allowlist. The user's Cloudflare account state what zones they own, what's already on it is the entire universe.
5
+ The agent never reads or mutates Cloudflare account state via an API there is no API path in this codebase. Whatever the operator sees in their logged-in dashboard is the single source of truth. When something needs doing on the account side (adding a domain, deleting a stray record, switching accounts), the agent tells the operator what to click; the operator clicks; then the agent runs the next `cloudflared` command.
6
6
 
7
7
  ## Identity
8
8
 
9
9
  | Fact | Source |
10
10
  |------|--------|
11
11
  | **Product identity** (Maxy vs Real Agent) | `brand.json` (`productName`, `configDir`) — known at install. |
12
- | **Cloudflare account identity** | `cert.pem` from OAuth (`tunnel-login`). One account per installation. |
13
- | **Account binding** | `~/{configDir}/cloudflare/account-binding.json` written on first successful login — drift detection only. |
12
+ | **Cloudflare account identity** | `cert.pem` from OAuth (`tunnel-login`). One account per laptop. |
13
+ | **Account binding** | `~/{configDir}/cloudflare/account-binding.json` — drift detection only. |
14
14
 
15
- `cf-set-token` does not exist. The only auth path is `tunnel-login`. To switch Cloudflare accounts, run `tunnel-login force=true` to clear cert + binding, then re-authenticate.
15
+ There is no token-based path. To switch Cloudflare accounts, run `tunnel-login` with `force=true`. This signs the laptop out (deletes cert + binding), then a fresh sign-in picks a different account.
16
16
 
17
- ## The flow (embarrassingly simple)
17
+ ## The flow
18
18
 
19
19
  1. Operator: "Set up Cloudflare for me."
20
- 2. Agent calls `cloudflare-setup`. The orchestrator inspects the bound account, asks the operator to pick a zone (if more than one is active), surfaces pollution (other zones, other tunnels, stale CNAMEs pointing to dead tunnels), and asks for explicit cleanup confirmation before deleting.
21
- 3. After cleanup, the orchestrator asks for admin / public subdomain labels (defaults offered).
22
- 4. Tunnel created, DNS routed, URL verified. Operator accesses the live URL.
23
- 5. Optional: alias domains (e.g. `maxy.chat`) via `tunnel-add-hostname` (must be a zone on the bound account; run `cf-add-zone` first if not).
20
+ 2. Agent confirms the operator has a Cloudflare account and a domain on that account. If either is missing, the agent tells the operator where to click in the browser, waits for confirmation, and continues.
21
+ 3. Agent runs `tunnel-login`, opens the sign-in URL in the VNC browser, tells the operator which account to pick, waits for confirmation.
22
+ 4. Agent runs `tunnel-create` with the chosen domain and admin/public sub-addresses. `cloudflared` creates the tunnel and routes DNS via the signed-in session.
23
+ 5. Agent runs `tunnel-enable`. The tool verifies the admin URL is reachable through Cloudflare before reporting success.
24
+ 6. Optional: alias addresses via `tunnel-add-hostname`.
24
25
 
25
26
  ## Tools
26
27
 
27
28
  | Tool | Use |
28
29
  |------|-----|
29
- | `cloudflare-setup` | Onboarding orchestrator. UI-driven state machine including the cleanup confirmation step. |
30
- | `tunnel-login` | OAuth only auth path. `force=true` clears cert + binding. |
31
- | `cf-verify` | Read-only snapshot of account + device + pollution (no-intent view). Run first. |
32
- | `cf-rebuild` | Nuclear cleanup. Deletes everything not in `preserve`. Refuses (`reason=no-intent`) when called with no `preserve` AND no device-persisted intent. |
33
- | `cf-add-zone` | Add a domain to the bound account. Idempotent. |
34
- | `cf-zone-status` | List zones on the bound account. |
35
- | `tunnel-create` | Create a tunnel and route DNS for chosen subdomains. Refuses hostnames not on a zone the account owns. |
36
- | `tunnel-add-hostname` | Add an alias hostname to an existing tunnel. |
30
+ | `tunnel-login` | Browser OAuth. `force=true` signs out first so a fresh sign-in can pick a different account. |
31
+ | `tunnel-install` | Install the `cloudflared` binary. |
32
+ | `tunnel-create` | Create tunnel, route DNS for chosen sub-addresses. Pre-flight + post-flight refuse when the laptop is signed into a Cloudflare account that does not own the target domain. |
33
+ | `tunnel-add-hostname` | Add an alias address. Same pre-flight/post-flight refusals as `tunnel-create`. |
37
34
  | `tunnel-enable` / `tunnel-disable` | Start / stop the tunnel daemon. |
38
- | `tunnel-status` | Operational state including `bound`. |
39
- | `dns-lookup` | DNS diagnostics. |
35
+ | `tunnel-status` | Full state including end-to-end probe of every configured address. `healthy: true` requires every address to probe `ok`. `boundAccountOwnsHostnames: false` means the laptop is running a tunnel nothing from the internet is reaching — almost always "signed into the wrong Cloudflare account." |
36
+ | `dns-lookup` | DNS diagnostics against public resolvers. |
40
37
 
41
38
  ## When something is wrong
42
39
 
43
- - **Refusal `account-drift` or `unbound-device`** → `tunnel-login force=true`, re-authenticate.
44
- - **Refusal `scope-mismatch`** → the requested hostname's parent zone isn't on the bound account. Run `cf-add-zone <domain>` (if user owns it) or pick another zone.
45
- - **Refusal `post-flight-fqdn-mismatch`** defence-in-depth fired; cleanup attempted. Run `cf-rebuild` to reconcile.
46
- - **Refusal `no-intent`** → `cf-rebuild` was called with no `preserve` on a device with no persisted tunnel or alias. Pass explicit `preserve` (empty arrays are valid "nuke everything" intent) or drive cleanup through `cloudflare-setup` instead.
47
- - **General mess on the account** → `cf-verify` to see, then `cf-rebuild` (with `dryRun: true` first if cautious) to clean.
48
-
49
- ## What pollution means
50
-
51
- Anything on the bound account that the user's current intended state (`tunnel.state` + `alias-domains.json`) doesn't reference is junk. Old tunnels, stale CNAMEs, zones from previous setups — all candidates for deletion. The agent never assumes anything is precious; the user states intent and the agent obliterates the rest.
40
+ - **Refusal `account-drift` or `unbound-device`** → `tunnel-login force=true`, then a fresh browser sign-in.
41
+ - **Refusal `hostname-zone-not-routable`** → the domain is not on the signed-in Cloudflare account. Operator opens the dashboard, either adds the domain (Websites Add a site) or switches to the correct account using the top-left dropdown. Agent then retries.
42
+ - **Refusal `post-flight-fqdn-mismatch` or `bound-account-does-not-own-hostname`** the laptop is signed into a Cloudflare account that does not own the target domain. `cloudflared` routed the address under a different domain. Operator switches Cloudflare accounts in the browser, agent runs `tunnel-login force=true`, then retries.
@@ -3,7 +3,7 @@ name: personal-assistant
3
3
  description: "Your personal assistant — scheduling, platform administration, messaging channels, system health, and browser automation. Delegate when a task involves managing your calendar, configuring the platform, operating messaging channels, or completing interactive browser tasks."
4
4
  summary: "Handles the operational tasks you'd give a personal assistant — scheduling meetings, managing your platform settings, connecting messaging channels, and completing browser-based tasks on your behalf. For example, when you want to schedule a weekly check-in, set up Telegram, or fill out an online form."
5
5
  model: claude-sonnet-4-6
6
- tools: mcp__admin__system-status, mcp__admin__brand-settings, mcp__admin__account-manage, mcp__admin__account-update, mcp__admin__logs-read, mcp__admin__plugin-read, mcp__admin__api-key-store, mcp__admin__api-key-verify, mcp__admin__render-component, mcp__admin__file-attach, mcp__admin__wifi, mcp__contacts__contact-create, mcp__contacts__contact-lookup, mcp__contacts__contact-update, mcp__contacts__contact-delete, mcp__contacts__contact-list, mcp__contacts__contact-export, mcp__contacts__contact-erase, mcp__contacts__group-create, mcp__contacts__group-manage, mcp__cloudflare__cf-add-zone, mcp__cloudflare__cf-zone-status, mcp__cloudflare__cf-verify, mcp__cloudflare__cf-rebuild, mcp__cloudflare__tunnel-status, mcp__cloudflare__tunnel-install, mcp__cloudflare__tunnel-login, mcp__cloudflare__tunnel-create, mcp__cloudflare__tunnel-enable, mcp__cloudflare__tunnel-disable, mcp__cloudflare__tunnel-add-hostname, mcp__cloudflare__dns-lookup, mcp__telegram__message, mcp__telegram__message-history, mcp__telegram__telegram-webhook-register, mcp__whatsapp__whatsapp-login-start, mcp__whatsapp__whatsapp-login-wait, mcp__whatsapp__whatsapp-status, mcp__whatsapp__whatsapp-disconnect, mcp__whatsapp__whatsapp-send, mcp__whatsapp__whatsapp-send-document, mcp__whatsapp__whatsapp-config, mcp__whatsapp__whatsapp-activity, mcp__whatsapp__whatsapp-conversations, mcp__whatsapp__whatsapp-messages, mcp__whatsapp__whatsapp-group-info, mcp__email__email-setup, mcp__email__email-read, mcp__email__email-send, mcp__email__email-reply, mcp__email__email-search, mcp__email__email-graph-query, mcp__email__email-otp-extract, mcp__email__email-status, mcp__email__email-auto-respond-config, mcp__scheduling__schedule-event, mcp__scheduling__schedule-list, mcp__scheduling__schedule-get, mcp__scheduling__schedule-update, mcp__scheduling__schedule-cancel, mcp__scheduling__schedule-export-ics, mcp__scheduling__schedule-import-ics, mcp__scheduling__time-resolve, mcp__memory__memory-search, mcp__memory__profile-update, mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_navigate_back, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_click, mcp__plugin_playwright_playwright__browser_fill, mcp__plugin_playwright_playwright__browser_fill_form, mcp__plugin_playwright_playwright__browser_type, mcp__plugin_playwright_playwright__browser_press_key, mcp__plugin_playwright_playwright__browser_hover, mcp__plugin_playwright_playwright__browser_select_option, mcp__plugin_playwright_playwright__browser_wait_for, mcp__plugin_playwright_playwright__browser_handle_dialog, mcp__plugin_playwright_playwright__browser_evaluate, mcp__plugin_playwright_playwright__browser_console_messages, mcp__plugin_playwright_playwright__browser_resize, mcp__plugin_playwright_playwright__browser_tabs, mcp__plugin_playwright_playwright__browser_close
6
+ tools: mcp__admin__system-status, mcp__admin__brand-settings, mcp__admin__account-manage, mcp__admin__account-update, mcp__admin__logs-read, mcp__admin__plugin-read, mcp__admin__api-key-store, mcp__admin__api-key-verify, mcp__admin__render-component, mcp__admin__file-attach, mcp__admin__wifi, mcp__contacts__contact-create, mcp__contacts__contact-lookup, mcp__contacts__contact-update, mcp__contacts__contact-delete, mcp__contacts__contact-list, mcp__contacts__contact-export, mcp__contacts__contact-erase, mcp__contacts__group-create, mcp__contacts__group-manage, mcp__cloudflare__tunnel-status, mcp__cloudflare__tunnel-install, mcp__cloudflare__tunnel-login, mcp__cloudflare__tunnel-create, mcp__cloudflare__tunnel-enable, mcp__cloudflare__tunnel-disable, mcp__cloudflare__tunnel-add-hostname, mcp__cloudflare__dns-lookup, mcp__telegram__message, mcp__telegram__message-history, mcp__telegram__telegram-webhook-register, mcp__whatsapp__whatsapp-login-start, mcp__whatsapp__whatsapp-login-wait, mcp__whatsapp__whatsapp-status, mcp__whatsapp__whatsapp-disconnect, mcp__whatsapp__whatsapp-send, mcp__whatsapp__whatsapp-send-document, mcp__whatsapp__whatsapp-config, mcp__whatsapp__whatsapp-activity, mcp__whatsapp__whatsapp-conversations, mcp__whatsapp__whatsapp-messages, mcp__whatsapp__whatsapp-group-info, mcp__email__email-setup, mcp__email__email-read, mcp__email__email-send, mcp__email__email-reply, mcp__email__email-search, mcp__email__email-graph-query, mcp__email__email-otp-extract, mcp__email__email-status, mcp__email__email-auto-respond-config, mcp__scheduling__schedule-event, mcp__scheduling__schedule-list, mcp__scheduling__schedule-get, mcp__scheduling__schedule-update, mcp__scheduling__schedule-cancel, mcp__scheduling__schedule-export-ics, mcp__scheduling__schedule-import-ics, mcp__scheduling__time-resolve, mcp__memory__memory-search, mcp__memory__profile-update, mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_navigate_back, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_click, mcp__plugin_playwright_playwright__browser_fill, mcp__plugin_playwright_playwright__browser_fill_form, mcp__plugin_playwright_playwright__browser_type, mcp__plugin_playwright_playwright__browser_press_key, mcp__plugin_playwright_playwright__browser_hover, mcp__plugin_playwright_playwright__browser_select_option, mcp__plugin_playwright_playwright__browser_wait_for, mcp__plugin_playwright_playwright__browser_handle_dialog, mcp__plugin_playwright_playwright__browser_evaluate, mcp__plugin_playwright_playwright__browser_console_messages, mcp__plugin_playwright_playwright__browser_resize, mcp__plugin_playwright_playwright__browser_tabs, mcp__plugin_playwright_playwright__browser_close
7
7
  ---
8
8
 
9
9
  # Personal Assistant
@@ -41,21 +41,21 @@ Manages events, appointments, and recurring triggers in the graph.
41
41
 
42
42
  ## Cloudflare Tunnel
43
43
 
44
- Guides setting up a Cloudflare Tunnel so the platform is reachable via a custom domain. The bound Cloudflare account — whatever account the cert was issued for — is the entire universe of routable zones. The agent has absolute authority over that account: add zones, delete zones, create or delete tunnels, write or remove CNAMEs. Pollution is anything on the account that does not match the user's current intent. Authentication is OAuth-only via `tunnel-login`; on first success the device records an account binding (`account-binding.json`) and refuses subsequent operations if the cert is later rotated under a different Cloudflare account.
44
+ Guides setting up a Cloudflare Tunnel so the platform is reachable via a custom domain. The operator's logged-in Cloudflare dashboard is the single source of truth which domains exist, which account owns them, and which addresses are in use. The agent never reads or mutates account state via any API path. Sign-in is OAuth-only via `tunnel-login`; the laptop records which Cloudflare account that sign-in landed on so a later account switch is detected and surfaced.
45
45
 
46
- **Auth path:** `tunnel-login` (one-click OAuth in VNC browser). Pass `force=true` to clear cert + binding when switching Cloudflare accounts. There is no API-token auth path the plugin recognises only the cert-bound account identity.
46
+ **Auth path:** `tunnel-login` (one-click OAuth in VNC browser). Pass `force=true` to sign this laptop out (clears the local sign-in file) so a fresh sign-in can pick a different Cloudflare account. There is no token-based path.
47
47
 
48
- **Setup flow:** `cloudflare-setup` is the single orchestrator. It inspects the bound account, asks the user to pick a zone, surfaces any pollution on the account (other zones, other tunnels, stale CNAMEs pointing to dead tunnels), requires explicit confirmation before deleting, and then creates the tunnel with chosen admin/public subdomains. Every decision comes from a rendered UI component the agent is a courier for submissions, never synthesising values from free text.
48
+ **Setup flow:** Run `tunnel-login`, then `tunnel-create` with the domain and sub-address choices. The tools enforce pre-flight (does the domain's registrable parent have Cloudflare nameservers?) and post-flight (did `cloudflared` route the address under the requested name?) safety checks. A refusal always surfaces a dashboard instruction — never an API retry.
49
49
 
50
- **Diagnostic flow:** `cf-verify` returns the bound account's zones, tunnels, and CNAMEs alongside the device's cert / binding / tunnel state. It does not tag in-scope or out-of-scopethe agent decides what is pollution from conversation context. Non-mutating; safe to run in any state, including before login.
50
+ **Diagnostic flow:** `tunnel-status` does an end-to-end probe per configured address (DNS + HTTPS via Cloudflare's edge). `healthy: true` requires every address to probe `ok`. `boundAccountOwnsHostnames: false` means the tunnel is running on this laptop but nothing from the internet is reaching it almost always because the laptop is signed into a Cloudflare account that does not own the target domain.
51
51
 
52
- **Recovery flow:** `cf-rebuild` nukes everything on the bound account that is not in the caller's explicit `preserve` set. When called with no `preserve` AND no device-persisted intent (no `tunnel.state`, no `alias-domains.json`), it refuses with `reason=no-intent` rather than guess. Stale CNAMEs pointing to `<deletedTunnelId>.cfargotunnel.com` on otherwise-preserved zones are always scrubbed. Pass `dryRun=true` to preview.
52
+ **Recovery flow:** The single recovery path for every wrong-account failure is: operator opens Cloudflare in their browser, confirms the account name in the top-left is the one that owns the target domain (or switches accounts using the dropdown), confirms to the agent, then the agent runs `tunnel-login force=true` to re-authenticate. Any stray records `cloudflared` created under the wrong domain are cleaned up by the operator in the wrong-account dashboard the agent cannot reach account state.
53
53
 
54
- **DNS lookups:** `dns-lookup` for hostname resolution. Nameserver-not-yet-propagated is the most common operator issue; `cf-zone-status` reports activation status.
54
+ **DNS lookups:** `dns-lookup` for hostname resolution. Nameserver-not-yet-propagated is the most common operator issue `tunnel-status`'s e2e probe names the failure mode directly (`dns-missing`, `cname-points-elsewhere`, `edge-unreachable`, `tunnel-not-matched`).
55
55
 
56
- **Alias domains:** `tunnel-add-hostname` routes an additional hostname to an existing tunnel. The hostname must be a zone on the bound account; if it isn't, run `cf-add-zone <domain>` first.
56
+ **Alias addresses:** `tunnel-add-hostname` adds an additional address to an existing tunnel. The alias's domain must be on the same Cloudflare account this laptop is signed into — if not, the tool refuses with dashboard guidance.
57
57
 
58
- **User-facing language:** Say "connection" not "certificate", "domain" not "zone", "address" not "hostname". No Cloudflare internal terminology.
58
+ **User-facing language:** Say "Cloudflare account", "domain", "address", "sign in", "browser". Never say "zone", "CNAME", "account ID", "API", "SDK", or any hexadecimal identifier.
59
59
 
60
60
  **Verification:** Always verify URLs work by navigating to them in the browser. Never claim a URL works without browser verification.
61
61
 
@@ -7684,11 +7684,6 @@ var ADMIN_CORE_TOOLS = [
7684
7684
  "mcp__admin__action-approve",
7685
7685
  "mcp__admin__action-reject",
7686
7686
  "mcp__admin__action-edit",
7687
- "mcp__cloudflare__cloudflare-setup",
7688
- "mcp__cloudflare__cf-add-zone",
7689
- "mcp__cloudflare__cf-zone-status",
7690
- "mcp__cloudflare__cf-verify",
7691
- "mcp__cloudflare__cf-rebuild",
7692
7687
  "mcp__cloudflare__tunnel-status",
7693
7688
  "mcp__cloudflare__tunnel-install",
7694
7689
  "mcp__cloudflare__tunnel-login",
@@ -10382,8 +10377,10 @@ var VALID_TYPES = /* @__PURE__ */ new Set([
10382
10377
  "silent-catch",
10383
10378
  "file-write-storm",
10384
10379
  "stale-log",
10385
- "rate-limit"
10380
+ "rate-limit",
10381
+ "absent-followup"
10386
10382
  ]);
10383
+ var MAX_FOLLOWUP_WINDOW_MS = 6e5;
10387
10384
  var VALID_SOURCES = /* @__PURE__ */ new Set([
10388
10385
  "any",
10389
10386
  "server",
@@ -10393,6 +10390,7 @@ var VALID_SOURCES = /* @__PURE__ */ new Set([
10393
10390
  "session",
10394
10391
  "public",
10395
10392
  "mcp",
10393
+ "cloudflared",
10396
10394
  "config-dir"
10397
10395
  ]);
10398
10396
  var VALID_SCOPES = /* @__PURE__ */ new Set(["global", "session"]);
@@ -10522,12 +10520,33 @@ function defaultRules() {
10522
10520
  scope: "session",
10523
10521
  suggestedAction: "The WebFetch SPA preflight has fired more than once in this conversation. Either the agent is ignoring the loud-failure directive (retrying WebFetch after seeing WEBFETCH_CANNOT_READ_JS_SPA), or multiple SPA URLs are being asked about. Read the conversation's stream log for the [tool-use] / [tool-result] sequence around each occurrence \u2014 if the agent dispatched WebFetch on the same URL or substituted Playwright silently, revisit the IDENTITY.md `Tool Failure Discipline` paragraph that names structured-error handling."
10524
10522
  },
10523
+ {
10524
+ // Task 538: fires when a [spawn] line appears in a conversation's stream
10525
+ // log but no subprocess-lifecycle marker follows within 10s. The three
10526
+ // acceptable followups are Task 535's contract — at least one must be
10527
+ // emitted immediately at every spawn site. Their absence means
10528
+ // `teeProcStderrToStreamLog` regressed, the markers drifted, or the
10529
+ // spawn site was added without wiring them up. Session scope so a single
10530
+ // broken conversation fires exactly once, not N times for every spawn.
10531
+ id: "subproc-tee-silent-spawn",
10532
+ name: "Subprocess spawn without a stderr-tee lifecycle marker",
10533
+ type: "absent-followup",
10534
+ logSource: "system",
10535
+ pattern: "\\[spawn\\] pid=\\d+",
10536
+ followupPattern: "\\[subproc-stderr-tee-attached\\]|\\[subproc-debug-unavailable\\]|\\[subproc-stderr-skip\\]",
10537
+ followupWindowMs: 1e4,
10538
+ thresholdCount: 0,
10539
+ thresholdWindowMinutes: 0,
10540
+ scope: "session",
10541
+ suggestedAction: "The main-subprocess tee infrastructure has regressed \u2014 a spawn produced no lifecycle marker. Re-check `teeProcStderrToStreamLog` is invoked at the spawn site and that `[subproc-debug-unavailable]` or `[subproc-stderr-skip]` is written immediately when the tee cannot attach (Task 535 contract)."
10542
+ },
10525
10543
  {
10526
10544
  // Task 533: surface every Cloudflare-plugin refusal. The plugin emits
10527
10545
  // exactly one [cloudflare:refuse] line per refusal with a structured
10528
10546
  // reason field; any single occurrence on a previously-clean device
10529
- // means scope, account-binding, or post-flight FQDN drift the
10530
- // operator should run cf-verify before acting.
10547
+ // means the bound Cloudflare account does not match the operator's
10548
+ // intent (or the post-flight FQDN drifted) and the operator needs to
10549
+ // act in the dashboard.
10531
10550
  id: "cloudflare-refuse",
10532
10551
  name: "Cloudflare plugin refusal",
10533
10552
  type: "silent-catch",
@@ -10535,7 +10554,40 @@ function defaultRules() {
10535
10554
  pattern: "\\[cloudflare:refuse\\]|\\[cloudflare:post-flight-mismatch\\]",
10536
10555
  thresholdCount: 0,
10537
10556
  thresholdWindowMinutes: 0,
10538
- suggestedAction: "The Cloudflare plugin refused an operation or detected a post-flight FQDN mismatch. Run `cf-verify` to inspect current device state. If the reason is `account-drift` or `unbound-device`, run `tunnel-login force=true` to clear cert + binding and re-authenticate. If the reason is `scope-mismatch`, the requested hostname is outside the brand's declared zones \u2014 fix at brand.json + republish. If `post-flight-fqdn-mismatch`, run `cf-rebuild` to reconcile."
10557
+ suggestedAction: "The Cloudflare plugin refused an operation. Read the refusal `reason` field in the adjacent log line. For `account-drift` or `unbound-device`, run `tunnel-login force=true` while the operator is signed into the correct Cloudflare account in the browser. For `hostname-zone-not-routable`, the domain is not on Cloudflare yet \u2014 guide the operator to add it via the Cloudflare dashboard. For `post-flight-fqdn-mismatch` or `bound-account-does-not-own-hostname`, the laptop is signed into the wrong Cloudflare account \u2014 guide the operator to switch accounts in the dashboard, then re-run `tunnel-login force=true`."
10558
+ },
10559
+ {
10560
+ // Task 540: the single highest-priority refusal — surface it immediately
10561
+ // and independently of the generic cloudflare-refuse rule so the admin
10562
+ // agent sees it on the very next turn. This is the exact class that
10563
+ // burned the operator for 8 days across 9+ sessions (Apr 11–18, 2026):
10564
+ // tunnel running locally, dashboard serving the wrong account, nothing
10565
+ // from the internet reaches the laptop, and no prior telemetry surfaced
10566
+ // it in time for the agent to self-correct.
10567
+ id: "cloudflare-bound-account-mismatch",
10568
+ name: "Cloudflare bound account does not own the configured hostnames",
10569
+ type: "silent-catch",
10570
+ logSource: "any",
10571
+ pattern: '"reason":"bound-account-does-not-own-hostname"',
10572
+ thresholdCount: 0,
10573
+ thresholdWindowMinutes: 0,
10574
+ suggestedAction: "This laptop is signed into a Cloudflare account that does not own the hostnames the tunnel is configured to serve. Run `tunnel-status` to confirm, then tell the operator verbatim: 'The tunnel is running on this laptop but nothing from the internet is reaching it. The Cloudflare account this laptop is signed into doesn't own your domain. Open Cloudflare in your browser \u2014 is the account name in the top-left the one that owns your domain? If not, switch to the correct one, then tell me and I will re-sign-in.' When the operator confirms the correct account is selected, run `tunnel-login force=true`."
10575
+ },
10576
+ {
10577
+ // Task 540: cloudflared.log is the one file most likely to carry the
10578
+ // "tunnel is having real-world connectivity problems" signal — QUIC
10579
+ // connection failures, connector drops, edge unreachability. Prior to
10580
+ // this rule it was written but never read (the review-detector had no
10581
+ // rule coverage for it). A single ERR line is worth surfacing; the
10582
+ // tee'd output is typically noise-free.
10583
+ id: "cloudflared-edge-errors",
10584
+ name: "cloudflared edge connectivity errors",
10585
+ type: "silent-catch",
10586
+ logSource: "cloudflared",
10587
+ pattern: "^\\S+ ERR (Failed to refresh protocol|no more connections active|Failed to dial a quic connection)",
10588
+ thresholdCount: 0,
10589
+ thresholdWindowMinutes: 0,
10590
+ suggestedAction: "cloudflared is reporting edge connectivity errors in its daemon log. Read the last 20 lines of `cloudflared.log` to see the surrounding context. Transient QUIC drops are normal; sustained failures (more than a handful in a minute) point to either a network issue on this laptop or an edge-side routing problem. Run `tunnel-status` to check whether end-to-end probing still succeeds."
10539
10591
  }
10540
10592
  ];
10541
10593
  }
@@ -10680,6 +10732,17 @@ function validateRule(input, label, seenIds) {
10680
10732
  };
10681
10733
  if (typeof r.watchPath === "string") rule.watchPath = r.watchPath;
10682
10734
  if (typeof r.staleHours === "number") rule.staleHours = r.staleHours;
10735
+ if (typeof r.followupPattern === "string") {
10736
+ if (r.followupPattern.length > 0) {
10737
+ try {
10738
+ new RegExp(r.followupPattern);
10739
+ } catch (err) {
10740
+ throw new Error(`${label}: followupPattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
10741
+ }
10742
+ }
10743
+ rule.followupPattern = r.followupPattern;
10744
+ }
10745
+ if (typeof r.followupWindowMs === "number") rule.followupWindowMs = r.followupWindowMs;
10683
10746
  if (typeof r.suppressedUntil === "string") rule.suppressedUntil = r.suppressedUntil;
10684
10747
  if (r.scope !== void 0) {
10685
10748
  if (typeof r.scope !== "string" || !VALID_SCOPES.has(r.scope)) {
@@ -10697,6 +10760,17 @@ function validateRule(input, label, seenIds) {
10697
10760
  throw new Error(`${label}: stale-log rules require a positive staleHours`);
10698
10761
  }
10699
10762
  }
10763
+ if (rule.type === "absent-followup") {
10764
+ if (rule.pattern.length === 0) {
10765
+ throw new Error(`${label}: absent-followup rules require a non-empty pattern`);
10766
+ }
10767
+ if (typeof rule.followupPattern !== "string" || rule.followupPattern.length === 0) {
10768
+ throw new Error(`${label}: absent-followup rules require a non-empty followupPattern`);
10769
+ }
10770
+ if (typeof rule.followupWindowMs !== "number" || rule.followupWindowMs <= 0 || rule.followupWindowMs > MAX_FOLLOWUP_WINDOW_MS) {
10771
+ throw new Error(`${label}: absent-followup rules require followupWindowMs in (0, ${MAX_FOLLOWUP_WINDOW_MS}]`);
10772
+ }
10773
+ }
10700
10774
  return rule;
10701
10775
  }
10702
10776
 
@@ -10742,6 +10816,10 @@ function discoverSourceFiles(configDir2, accountLogDir2, logicalSource) {
10742
10816
  const p = resolve8(configDir2, "logs", "vnc-boot.log");
10743
10817
  return existsSync8(p) ? [{ logicalSource: "vnc", filepath: p }] : [];
10744
10818
  }
10819
+ if (logicalSource === "cloudflared") {
10820
+ const p = resolve8(configDir2, "logs", "cloudflared.log");
10821
+ return existsSync8(p) ? [{ logicalSource: "cloudflared", filepath: p }] : [];
10822
+ }
10745
10823
  const prefix = {
10746
10824
  system: "claude-agent-stream-",
10747
10825
  error: "claude-agent-stderr-",
@@ -10786,7 +10864,8 @@ function discoverAllSources(configDir2, accountLogDir2) {
10786
10864
  ...discoverSourceFiles(configDir2, accountLogDir2, "error"),
10787
10865
  ...discoverSourceFiles(configDir2, accountLogDir2, "session"),
10788
10866
  ...discoverSourceFiles(configDir2, accountLogDir2, "public"),
10789
- ...discoverSourceFiles(configDir2, accountLogDir2, "mcp")
10867
+ ...discoverSourceFiles(configDir2, accountLogDir2, "mcp"),
10868
+ ...discoverSourceFiles(configDir2, accountLogDir2, "cloudflared")
10790
10869
  ];
10791
10870
  }
10792
10871
  function readNewLines(filepath, prev) {
@@ -11168,7 +11247,8 @@ function newRuleState() {
11168
11247
  matchTimestampsByScope: /* @__PURE__ */ new Map(),
11169
11248
  lastAlertAt: null,
11170
11249
  cumulativeSinceLastAlert: 0,
11171
- lastSeenAt: null
11250
+ lastSeenAt: null,
11251
+ pendingFollowups: []
11172
11252
  };
11173
11253
  }
11174
11254
  function evaluateTextRule(rule, lines, state, nowMs) {
@@ -11266,6 +11346,58 @@ function evaluateStaleLogRule(rule, lastMtimeMs, state, nowMs) {
11266
11346
  };
11267
11347
  return { match: match2, state: updated };
11268
11348
  }
11349
+ function evaluateAbsentFollowupRule(rule, lines, state, nowMs) {
11350
+ if (isSuppressed(rule, nowMs)) return { matches: [], state };
11351
+ if (!rule.pattern || !rule.followupPattern || !rule.followupWindowMs) {
11352
+ return { matches: [], state };
11353
+ }
11354
+ const triggerRegex = compileRegex(rule.pattern);
11355
+ const followupRegex = compileRegex(rule.followupPattern);
11356
+ const pending = [...state.pendingFollowups ?? []];
11357
+ for (const line of lines) {
11358
+ if (triggerRegex.test(line)) {
11359
+ pending.push({
11360
+ scope: scopeKeyFor(rule, line),
11361
+ timestamp: nowMs,
11362
+ line,
11363
+ fulfilled: false
11364
+ });
11365
+ continue;
11366
+ }
11367
+ if (followupRegex.test(line)) {
11368
+ const scope = scopeKeyFor(rule, line);
11369
+ for (const entry of pending) {
11370
+ if (!entry.fulfilled && entry.scope === scope) {
11371
+ entry.fulfilled = true;
11372
+ break;
11373
+ }
11374
+ }
11375
+ }
11376
+ }
11377
+ const matches = [];
11378
+ const kept = [];
11379
+ for (const entry of pending) {
11380
+ const age = nowMs - entry.timestamp;
11381
+ if (age >= rule.followupWindowMs) {
11382
+ if (!entry.fulfilled) {
11383
+ matches.push({
11384
+ ruleId: rule.id,
11385
+ ruleName: rule.name,
11386
+ matchedAt: nowMs,
11387
+ sampleEvidence: toSample(entry.line),
11388
+ suggestedAction: rule.suggestedAction,
11389
+ missedForMs: age
11390
+ });
11391
+ }
11392
+ continue;
11393
+ }
11394
+ kept.push(entry);
11395
+ }
11396
+ return {
11397
+ matches,
11398
+ state: { ...state, pendingFollowups: kept }
11399
+ };
11400
+ }
11269
11401
  var ALERT_WINDOW_MS = 60 * 60 * 1e3;
11270
11402
  function rateLimitDecision(state, nowMs) {
11271
11403
  const since = state.lastAlertAt === null ? Infinity : nowMs - state.lastAlertAt;
@@ -11386,6 +11518,24 @@ async function runScanCycle(runtime) {
11386
11518
  const result = evaluateStaleLogRule(rule, lastMs, state, cycleStart);
11387
11519
  state = result.state;
11388
11520
  match2 = result.match;
11521
+ } else if (rule.type === "absent-followup") {
11522
+ let inputLines = [];
11523
+ if (rule.logSource === "any") {
11524
+ for (const [src, lines] of linesBySource.entries()) {
11525
+ if (src !== "config-dir") inputLines.push(...lines);
11526
+ }
11527
+ } else {
11528
+ inputLines = linesBySource.get(rule.logSource) ?? [];
11529
+ }
11530
+ const result = evaluateAbsentFollowupRule(rule, inputLines, state, cycleStart);
11531
+ state = result.state;
11532
+ for (const m of result.matches) {
11533
+ const safeTrigger = m.sampleEvidence.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
11534
+ console.error(
11535
+ `[review-detector] absent-followup rule=${rule.id} trigger="${safeTrigger}" missed_for_ms=${m.missedForMs ?? ""}`
11536
+ );
11537
+ matches.push(m);
11538
+ }
11389
11539
  }
11390
11540
  runtime.ruleState.set(rule.id, state);
11391
11541
  if (match2) matches.push(match2);