@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.
- package/package.json +1 -1
- package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/mcp-stderr-tee/dist/index.js +11 -5
- package/payload/platform/lib/mcp-stderr-tee/dist/index.js.map +1 -1
- package/payload/platform/lib/mcp-stderr-tee/src/index.ts +10 -5
- package/payload/platform/plugins/admin/mcp/dist/lib/review-tools.d.ts.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/lib/review-tools.js +55 -6
- package/payload/platform/plugins/admin/mcp/dist/lib/review-tools.js.map +1 -1
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +11 -7
- package/payload/platform/plugins/cloudflare/PLUGIN.md +10 -13
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js +94 -1030
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts +62 -258
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +297 -882
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/package.json +3 -7
- package/payload/platform/plugins/cloudflare/references/setup-guide.md +51 -77
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +23 -81
- package/payload/platform/plugins/docs/PLUGIN.md +1 -1
- package/payload/platform/plugins/docs/references/cloudflare.md +21 -30
- package/payload/platform/templates/specialists/agents/personal-assistant.md +9 -9
- package/payload/server/server.js +161 -11
- 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
|
|
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
|
|
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 enumeration — those 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
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
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. **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
33
|
+
## Adding an alias address
|
|
66
34
|
|
|
67
|
-
An alias
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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-
|
|
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
|
-
-
|
|
109
|
-
- **
|
|
110
|
-
- **The
|
|
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` —
|
|
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 —
|
|
1
|
+
# Cloudflare Tunnel — the dashboard is the source of truth
|
|
2
2
|
|
|
3
|
-
Each installation has its own Cloudflare account. The
|
|
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
|
|
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
|
|
13
|
-
| **Account binding** | `~/{configDir}/cloudflare/account-binding.json`
|
|
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
|
-
|
|
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
|
|
17
|
+
## The flow
|
|
18
18
|
|
|
19
19
|
1. Operator: "Set up Cloudflare for me."
|
|
20
|
-
2. Agent
|
|
21
|
-
3.
|
|
22
|
-
4.
|
|
23
|
-
5.
|
|
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
|
-
| `
|
|
30
|
-
| `tunnel-
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
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` |
|
|
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`,
|
|
44
|
-
- **Refusal `
|
|
45
|
-
- **Refusal `post-flight-fqdn-mismatch
|
|
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,
|
|
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
|
|
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
|
|
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:** `
|
|
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:** `
|
|
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:**
|
|
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
|
|
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
|
|
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 "
|
|
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
|
|
package/payload/server/server.js
CHANGED
|
@@ -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
|
|
10530
|
-
//
|
|
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
|
|
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);
|