@rubytech/create-maxy-code 0.1.336 → 0.1.337
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/dist/index.js +10 -23
- package/package.json +2 -2
- package/payload/platform/plugins/cloudflare/references/api.md +2 -1
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +2 -0
- package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +2 -2
- package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +100 -0
- package/payload/platform/scripts/check-routing-prose-bash.mjs +134 -0
- package/payload/platform/scripts/lib/provision-account-dir.sh +8 -20
- package/payload/platform/scripts/resume-tunnel.sh +44 -13
- package/payload/platform/templates/agents/admin/AGENTS.md +1 -1
- package/payload/platform/templates/specialists/agents/coding-assistant.md +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +3 -4
- package/payload/server/{chunk-GRKG3KNN.js → chunk-NE7G5GT7.js} +0 -28
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/server.js +23 -71
package/dist/index.js
CHANGED
|
@@ -608,29 +608,16 @@ function writeChromiumBinaryPathFile() {
|
|
|
608
608
|
console.log(` Wrote ${target} → ${RESOLVED_CHROMIUM_BIN}`);
|
|
609
609
|
logFile(` [snap-chromium] wrote ${target} contents=${RESOLVED_CHROMIUM_BIN}`);
|
|
610
610
|
}
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
// Called as the FINAL install action (not step 1): RuntimeWatchdogSec=20s is a
|
|
622
|
-
// tight timer, and the heavy npm-install/swap-thrash steps stall PID1 past 20s
|
|
623
|
-
// on a constrained Pi. Arming early reboot-looped the install (the board reset
|
|
624
|
-
// mid-build, relaunched under the same load, reset again). Deferring the arm to
|
|
625
|
-
// after the build/swap window, then applying it with `daemon-reexec` so it is
|
|
626
|
-
// active immediately (RuntimeWatchdogUSec non-zero), keeps kernel-hang recovery
|
|
627
|
-
// without the loop.
|
|
628
|
-
// Remove any prior hardware-watchdog drop-in this installer (or an earlier
|
|
629
|
-
// version) wrote, then daemon-reexec so the live RuntimeWatchdogUSec
|
|
630
|
-
// goes back to systemd's default (0 = off). The feature is permanently
|
|
631
|
-
// disabled; configureHardwareWatchdog() no longer re-creates the file at
|
|
632
|
-
// install end. See configureHardwareWatchdog() comment for the rationale
|
|
633
|
-
// (the 20s timer reboot-looped constrained Pis when re-armed at install end).
|
|
611
|
+
// Scrub any hardware-watchdog drop-in this installer (or an earlier version)
|
|
612
|
+
// left on the box, then daemon-reexec so the live RuntimeWatchdogUSec returns
|
|
613
|
+
// to systemd's default (0 = off). The hardware-watchdog feature is retired:
|
|
614
|
+
// a RuntimeWatchdogSec=20s timer (inert above the ~15s BCM2835 kernel cap, so
|
|
615
|
+
// it cannot be loosened) reboot-looped constrained Pis — mid-install when armed
|
|
616
|
+
// early, and again at the post-install load peak when armed at install end. It
|
|
617
|
+
// bit both muvin and beacons. configureHardwareWatchdog() no longer arms
|
|
618
|
+
// anything; see its comment for the full rationale. Kernel-freeze recovery is
|
|
619
|
+
// left to the brand service's software watchdog (Type=notify WATCHDOG_USEC),
|
|
620
|
+
// which restarts the UI process rather than resetting the whole SoC.
|
|
634
621
|
//
|
|
635
622
|
// Runs unconditionally at install start. Idempotent: if no drop-in is on
|
|
636
623
|
// disk, this is a no-op.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rubytech/create-maxy-code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.337",
|
|
4
4
|
"description": "Install Maxy — AI for Productive People",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-maxy-code": "./dist/index.js"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"bundle": "node scripts/bundle.js",
|
|
15
15
|
"test": "npm run build && node --test 'dist/__tests__/*.test.js'",
|
|
16
|
-
"prepublishOnly": "bash ../../platform/scripts/verify-skill-tool-surface.sh && node ../../platform/scripts/check-plugin-tools-mcp-consistency.mjs && node ../../platform/scripts/check-specialist-tool-surface.mjs && node ../../platform/scripts/check-no-task-id-leaks.mjs && node ../../platform/scripts/check-no-esm-require.mjs && node ../../platform/scripts/check-no-raw-mcp-registrations.mjs && node ../../platform/scripts/check-skill-load-coverage.mjs && node ../../platform/scripts/check-architecture-skill-no-drift.mjs && node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && bash ../../platform/scripts/smoke-boot-services.sh && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
|
|
16
|
+
"prepublishOnly": "bash ../../platform/scripts/verify-skill-tool-surface.sh && node ../../platform/scripts/check-plugin-tools-mcp-consistency.mjs && node ../../platform/scripts/check-specialist-tool-surface.mjs && node ../../platform/scripts/check-routing-prose-bash.mjs && node ../../platform/scripts/check-no-task-id-leaks.mjs && node ../../platform/scripts/check-no-esm-require.mjs && node ../../platform/scripts/check-no-raw-mcp-registrations.mjs && node ../../platform/scripts/check-skill-load-coverage.mjs && node ../../platform/scripts/check-architecture-skill-no-drift.mjs && node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && bash ../../platform/scripts/smoke-boot-services.sh && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
@@ -247,7 +247,8 @@ The tally is **names only** — a token value is never printed (the redaction bi
|
|
|
247
247
|
|
|
248
248
|
## When to use which path
|
|
249
249
|
|
|
250
|
-
- **Tunnel create/route
|
|
250
|
+
- **Tunnel create/route** → `cloudflared` with the OAuth cert, per `manual-setup.md`. The API can manage tunnels too, but the cert path is the established one for create/route.
|
|
251
|
+
- **Tunnel run/resume** → `cloudflared tunnel run` authenticates with the per-tunnel credentials-file named in `config.yml`, not the cert, so the connector (and the `resume-tunnel.sh` supervisor) respawns a configured tunnel with no cert present. This is what lets a tunnel created via the API-token credentials-file path resume on reboot like an OAuth one.
|
|
251
252
|
- **Apex CNAME** → API (above) or dashboard (`dashboard-guide.md`). Either flattens correctly.
|
|
252
253
|
- **Pages deploy** → `wrangler` with the reused `<brand>-pages-d1` token, per `hosting-sites.md`.
|
|
253
254
|
- **D1 read/write** → `wrangler d1 execute --remote` with the reused D1-Edit token, per `d1-data-capture.md`.
|
|
@@ -431,6 +431,8 @@ The correct production pattern is already in place on every Maxy device: the bra
|
|
|
431
431
|
|
|
432
432
|
**The connector is a supervised, self-healing unit.** `resume-tunnel.sh` does not run cloudflared as a child of the brand service. It spawns it into its own transient systemd **service** — `cloudflared-<brand>.service` under `cloudflared.slice` — with `Restart=always`. Two consequences: a brand-service restart no longer reaps the connector (the cgroup decoupling), and systemd resurrects the connector on **any** exit — crash, OOM, manual kill. The connector also runs with `--no-autoupdate`, so it never downloads a new binary and replaces itself; the version is installer-owned (pinned in the installer, bumped deliberately). `StartLimitIntervalSec=300`/`StartLimitBurst=5` mean a genuinely broken binary surfaces as a `failed` unit instead of looping forever.
|
|
433
433
|
|
|
434
|
+
**What the supervisor needs to resume.** `resume-tunnel.sh` respawns the connector with `cloudflared … --config "${CFG_DIR}/config.yml" tunnel run`, which authenticates with the per-tunnel **credentials-file** named in that config — not `cert.pem`. The account cert is a create/route credential; the run/resume path never reads it. So a tunnel resumes on reboot whenever `config.yml` and its `credentials-file` are present, whether the tunnel was created via OAuth (this runbook) or supplied as an API-token credentials-file. The supervisor passes `--origincert` only when `cert.pem` exists; absent it, the token connector still runs. For a configured tunnel (a valid `tunnel.state`) where neither a usable credentials-file nor a cert is present, `resume-tunnel.sh` logs `op=resume-refused reason=missing-run-credential` at brand-service start instead of skipping silently — so a non-resumable tunnel is visible at the next start rather than discovered as an outage.
|
|
435
|
+
|
|
434
436
|
**Observability.** A connector death is now a logged, countable event, not an invisible gap:
|
|
435
437
|
|
|
436
438
|
```
|
|
@@ -5,7 +5,7 @@ description: Cloudflare operations for the install — tunnel setup/diagnosis/re
|
|
|
5
5
|
|
|
6
6
|
# Cloudflare operations
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Run by the admin/main session, the operator-infra surface that holds the `Bash` tool. Every step here issues `cloudflared` / `wrangler` over a shell, so this is not specialist work; the admin session loads this skill and runs it directly.
|
|
9
9
|
|
|
10
10
|
This is the entry point for every Cloudflare task on the install. Pick the operation class, read the matching reference, run the command via Bash, relay the literal output, and prove the outcome with a live signal. There are no Cloudflare MCP tools; the plugin registers none. The agent never browser-automates the dashboard — the operator clicks where a click is genuinely required.
|
|
11
11
|
|
|
@@ -21,7 +21,7 @@ This is the entry point for every Cloudflare task on the install. Pick the opera
|
|
|
21
21
|
|
|
22
22
|
## Two auth paths, by operation class
|
|
23
23
|
|
|
24
|
-
- **Tunnel auth is OAuth.** `cloudflared tunnel login` writes `cert.pem`; the operator clicks the linkified OAuth URL in their own browser.
|
|
24
|
+
- **Tunnel create/route auth is OAuth.** `cloudflared tunnel login` writes `cert.pem`; the operator clicks the linkified OAuth URL in their own browser. The cert authorises tunnel **create** and **route**, and OAuth is the prescribed path for them. **Running** a tunnel is a separate credential: `cloudflared tunnel run` — and the `resume-tunnel.sh` supervisor that respawns the connector on reboot — authenticates with the per-tunnel **credentials-file** named in `config.yml`, not the cert. So a tunnel resumes whether or not `cert.pem` is present; `--origincert` is passed on the run only when the cert exists. See `references/manual-setup.md`.
|
|
25
25
|
- **API auth is the master token.** The operator provisions one fully-scoped master token once (an **advanced**, operator-guided dashboard step — the agent does not automate it) and stores it at the account-scoped secrets file. The agent reads the master only to **provision one stable narrow token per scope** — `<brand>-pages-d1`, `<brand>-dns`, `<brand>-access` — loading it from the secrets file, minting it once if absent (no expiry), persisting it, and reusing it for every later operation in that scope. A standing **reconcile** pass revokes strays. See `references/api.md` for the storage convention, the provisioning-and-reuse flow, the reconcile pass, and the binding redaction discipline.
|
|
26
26
|
|
|
27
27
|
Neither the master token nor any per-scope token is ever written into a project tree, committed, or echoed into chat — the per-scope tokens persist only in the account-scoped secrets file, never in a deployable project tree.
|
|
@@ -522,6 +522,103 @@ TBL
|
|
|
522
522
|
return 0
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
# ---------------------------------------------------------------------------
|
|
526
|
+
# Credentials-file (API-token) tunnel resume.
|
|
527
|
+
#
|
|
528
|
+
# A tunnel created via the API-token path has a credentials-file in config.yml
|
|
529
|
+
# and NO cert.pem. `cloudflared tunnel run` authenticates with the
|
|
530
|
+
# credentials-file, not the account cert, so resume-tunnel must spawn such a
|
|
531
|
+
# tunnel WITHOUT --origincert and must NOT refuse it for the missing cert.
|
|
532
|
+
|
|
533
|
+
# CASE H: token tunnel (credentials-file present, no cert) → spawns without --origincert.
|
|
534
|
+
case_token_tunnel_spawns_without_origincert() {
|
|
535
|
+
local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
536
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
537
|
+
local home="$root/home"
|
|
538
|
+
# Token model: remove the OAuth cert; point config.yml at an existing creds json.
|
|
539
|
+
rm -f "$home/.maxy-code/cloudflared/cert.pem"
|
|
540
|
+
local creds="$home/.maxy-code/cloudflared/test-tunnel-id.json"
|
|
541
|
+
echo '{"AccountTag":"x","TunnelSecret":"y","TunnelID":"test-tunnel-id"}' > "$creds"
|
|
542
|
+
cat > "$home/.maxy-code/cloudflared/config.yml" <<YML
|
|
543
|
+
tunnel: test-tunnel-id
|
|
544
|
+
credentials-file: $creds
|
|
545
|
+
ingress:
|
|
546
|
+
- service: http_status:404
|
|
547
|
+
YML
|
|
548
|
+
|
|
549
|
+
HOME="$home" \
|
|
550
|
+
PATH="$root/bin:$PATH" \
|
|
551
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
552
|
+
bash "$RESUME"
|
|
553
|
+
local rc=$?
|
|
554
|
+
local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
|
|
555
|
+
cleanup "$root"
|
|
556
|
+
|
|
557
|
+
if [[ $rc -ne 0 ]]; then echo " expected exit 0, got $rc"; return 1; fi
|
|
558
|
+
echo "$log" | grep -q "spawned service=cloudflared-maxy-code.service" \
|
|
559
|
+
|| { echo " token tunnel did not spawn"; echo "$log"; return 1; }
|
|
560
|
+
echo "$log" | grep -q "op=resume-refused" \
|
|
561
|
+
&& { echo " token tunnel wrongly refused"; echo "$log"; return 1; }
|
|
562
|
+
echo "$log" | grep -q "\[stub\] arg=--origincert" \
|
|
563
|
+
&& { echo " --origincert passed for a token tunnel (cert absent)"; echo "$log"; return 1; }
|
|
564
|
+
return 0
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# CASE I: configured tunnel, no usable run credential → loud resume-refused, no spawn.
|
|
568
|
+
# config.yml references a missing credentials-file AND there is no cert.pem.
|
|
569
|
+
case_refuse_when_no_run_credential() {
|
|
570
|
+
local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
571
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
572
|
+
local home="$root/home"
|
|
573
|
+
rm -f "$home/.maxy-code/cloudflared/cert.pem"
|
|
574
|
+
cat > "$home/.maxy-code/cloudflared/config.yml" <<YML
|
|
575
|
+
tunnel: test-tunnel-id
|
|
576
|
+
credentials-file: $home/.maxy-code/cloudflared/does-not-exist.json
|
|
577
|
+
ingress:
|
|
578
|
+
- service: http_status:404
|
|
579
|
+
YML
|
|
580
|
+
|
|
581
|
+
HOME="$home" \
|
|
582
|
+
PATH="$root/bin:$PATH" \
|
|
583
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
584
|
+
bash "$RESUME"
|
|
585
|
+
local rc=$?
|
|
586
|
+
local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
|
|
587
|
+
cleanup "$root"
|
|
588
|
+
|
|
589
|
+
if [[ $rc -ne 0 ]]; then echo " expected exit 0, got $rc"; return 1; fi
|
|
590
|
+
echo "$log" | grep -q "op=resume-refused brand=maxy-code tunnel=test-tunnel-id reason=missing-run-credential" \
|
|
591
|
+
|| { echo " missing resume-refused line"; echo "$log"; return 1; }
|
|
592
|
+
echo "$log" | grep -q "spawned service=" \
|
|
593
|
+
&& { echo " spawned despite no run credential"; echo "$log"; return 1; }
|
|
594
|
+
return 0
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
# CASE J: corrupt tunnel.state → loud resume-refused, exits 0 (must never block
|
|
598
|
+
# the brand service). Exercises resume_refused on the invalid-state-json path,
|
|
599
|
+
# where TUNNEL_ID is not yet assigned — confirms the helper survives under set -u.
|
|
600
|
+
case_refuse_on_invalid_state_json() {
|
|
601
|
+
local root; root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
602
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
603
|
+
local home="$root/home"
|
|
604
|
+
echo 'this is not json {{{' > "$home/.maxy-code/cloudflared/tunnel.state"
|
|
605
|
+
|
|
606
|
+
HOME="$home" \
|
|
607
|
+
PATH="$root/bin:$PATH" \
|
|
608
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
609
|
+
bash "$RESUME"
|
|
610
|
+
local rc=$?
|
|
611
|
+
local log; log=$(cat "$home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
|
|
612
|
+
cleanup "$root"
|
|
613
|
+
|
|
614
|
+
if [[ $rc -ne 0 ]]; then echo " expected exit 0 on corrupt state, got $rc"; return 1; fi
|
|
615
|
+
echo "$log" | grep -q "op=resume-refused brand=maxy-code tunnel=unknown reason=invalid-state-json" \
|
|
616
|
+
|| { echo " missing invalid-state-json resume-refused line"; echo "$log"; return 1; }
|
|
617
|
+
echo "$log" | grep -q "spawned service=" \
|
|
618
|
+
&& { echo " spawned on corrupt state"; echo "$log"; return 1; }
|
|
619
|
+
return 0
|
|
620
|
+
}
|
|
621
|
+
|
|
525
622
|
run_case "skip when service active" case_skip_when_scope_active
|
|
526
623
|
run_case "spawn succeeds and logs verified" case_spawn_succeeds
|
|
527
624
|
run_case "spawn with no connection logs ERROR exits 0" case_spawn_no_connection
|
|
@@ -535,6 +632,9 @@ run_case "reap: audit line names brand tunnel id" case_reap_audit_tunnel_id
|
|
|
535
632
|
run_case "reap: foreign co-resident connector untouched" case_reap_foreign_untouched
|
|
536
633
|
run_case "reap: SIGTERM survivor escalates to SIGKILL" case_reap_sigkill_escalation
|
|
537
634
|
run_case "reap: skip when active unit's MainPID is unresolved" case_reap_skip_when_mainpid_unresolved
|
|
635
|
+
run_case "token tunnel spawns without --origincert" case_token_tunnel_spawns_without_origincert
|
|
636
|
+
run_case "configured tunnel with no run credential refuses loudly" case_refuse_when_no_run_credential
|
|
637
|
+
run_case "corrupt tunnel.state refuses loudly and exits 0" case_refuse_on_invalid_state_json
|
|
538
638
|
|
|
539
639
|
echo ""
|
|
540
640
|
echo "Results: $PASS passed, $FAIL failed"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Routing-prose ↔ `tools:` reconciliation gate (Task 1000).
|
|
3
|
+
//
|
|
4
|
+
// A specialist agent file can name a shell-only Cloudflare CLI in its routing
|
|
5
|
+
// prose — `cloudflared` or `wrangler` — neither of which has an MCP tool: the
|
|
6
|
+
// only way to run them is the CC-native `Bash` tool. When the prose tells the
|
|
7
|
+
// agent to drive one of those CLIs but the file's `tools:` frontmatter grants
|
|
8
|
+
// no `Bash`, the dispatched specialist is structurally unable to do step one.
|
|
9
|
+
// It emits no event; it just flails (Task 1000: `personal-assistant` owned the
|
|
10
|
+
// Cloudflare runbook, lacked `Bash`, and degenerated into VNC pixel-driving to
|
|
11
|
+
// reach a terminal). This gate reconciles declared-need against declared-tools
|
|
12
|
+
// at publish time so the mismatch never ships.
|
|
13
|
+
//
|
|
14
|
+
// Scope: every specialist agent .md under
|
|
15
|
+
// platform/templates/specialists/agents/ and premium-plugins/**/agents/ that
|
|
16
|
+
// carries a `tools:` line. A file with NO `tools:` line is unrestricted
|
|
17
|
+
// (admin-like — inherits all tools including Bash) and is skipped: the gate is
|
|
18
|
+
// about a gated grant contradicting its own prose, not about whether a CLI is
|
|
19
|
+
// named at all.
|
|
20
|
+
//
|
|
21
|
+
// Pure-JS, no build step: runs in `prepublishOnly` before `npm run build`,
|
|
22
|
+
// mirroring check-specialist-tool-surface.mjs. Exits 1 with one
|
|
23
|
+
// `[routing-prose-bash] specialist=… cli=… reason=missing-bash-tool` line per
|
|
24
|
+
// `(specialist, cli)` defect, matching that script's grep-able log shape.
|
|
25
|
+
|
|
26
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
27
|
+
import { join, resolve, basename, dirname } from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const DEFAULT_MAXY_CODE_ROOT = resolve(SCRIPT_DIR, '..', '..');
|
|
32
|
+
|
|
33
|
+
// Shell-only Cloudflare CLIs: no MCP tool exists, so naming one in routing
|
|
34
|
+
// prose implies the agent must hold `Bash`.
|
|
35
|
+
const SHELL_ONLY_CLIS = ['cloudflared', 'wrangler'];
|
|
36
|
+
|
|
37
|
+
const SKIP_AGENT_BASENAMES = new Set(['IDENTITY', 'SOUL', 'AGENTS', 'LEARNINGS']);
|
|
38
|
+
|
|
39
|
+
function splitFrontmatter(raw) {
|
|
40
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
41
|
+
if (!m) return { frontmatter: null, body: raw };
|
|
42
|
+
return { frontmatter: m[1], body: m[2] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// True only when the `tools:` line lists the bare CC-native `Bash` token.
|
|
46
|
+
// Returns null when there is no `tools:` line at all (unrestricted file).
|
|
47
|
+
export function toolsHasBash(frontmatter) {
|
|
48
|
+
if (!frontmatter) return null;
|
|
49
|
+
const m = frontmatter.match(/^tools:\s*(.+)$/m);
|
|
50
|
+
if (!m) return null;
|
|
51
|
+
const tokens = m[1].split(',').map(t => t.trim());
|
|
52
|
+
return tokens.includes('Bash');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Returns the sorted list of shell-only CLIs the body names (word-boundary,
|
|
56
|
+
// case-insensitive). Empty when none are named.
|
|
57
|
+
export function proseNamesClis(body) {
|
|
58
|
+
const found = [];
|
|
59
|
+
for (const cli of SHELL_ONLY_CLIS) {
|
|
60
|
+
if (new RegExp(`\\b${cli}\\b`, 'i').test(body)) found.push(cli);
|
|
61
|
+
}
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Walk the given agent dirs; return one defect per (specialist, cli) where the
|
|
66
|
+
// prose names a shell-only CLI but the gated `tools:` line lacks `Bash`.
|
|
67
|
+
export function runForDirs(agentDirs) {
|
|
68
|
+
const defects = [];
|
|
69
|
+
let inspected = 0;
|
|
70
|
+
for (const dir of agentDirs) {
|
|
71
|
+
let entries;
|
|
72
|
+
try { entries = readdirSync(dir); } catch { continue; }
|
|
73
|
+
for (const entry of entries.sort()) {
|
|
74
|
+
const fullPath = join(dir, entry);
|
|
75
|
+
let st;
|
|
76
|
+
try { st = statSync(fullPath); } catch { continue; }
|
|
77
|
+
if (!st.isFile() || !entry.endsWith('.md')) continue;
|
|
78
|
+
if (SKIP_AGENT_BASENAMES.has(basename(entry, '.md').toUpperCase())) continue;
|
|
79
|
+
const { frontmatter, body } = splitFrontmatter(readFileSync(fullPath, 'utf-8'));
|
|
80
|
+
const hasBash = toolsHasBash(frontmatter);
|
|
81
|
+
if (hasBash === null) continue; // unrestricted file — not a gated grant
|
|
82
|
+
inspected += 1;
|
|
83
|
+
if (hasBash) continue;
|
|
84
|
+
for (const cli of proseNamesClis(body)) {
|
|
85
|
+
defects.push({ specialistPath: fullPath, cli });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { defects, inspected };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function collectAgentDirs(maxyCodeRoot) {
|
|
93
|
+
const dirs = [];
|
|
94
|
+
const templates = join(maxyCodeRoot, 'platform', 'templates', 'specialists', 'agents');
|
|
95
|
+
if (existsSync(templates)) dirs.push(templates);
|
|
96
|
+
const premiumRoot = join(maxyCodeRoot, 'premium-plugins');
|
|
97
|
+
if (existsSync(premiumRoot)) {
|
|
98
|
+
for (const bundle of readdirSync(premiumRoot).sort()) {
|
|
99
|
+
const bundleAgents = join(premiumRoot, bundle, 'agents');
|
|
100
|
+
try { if (statSync(bundleAgents).isDirectory()) dirs.push(bundleAgents); } catch { /* skip */ }
|
|
101
|
+
const subPluginsDir = join(premiumRoot, bundle, 'plugins');
|
|
102
|
+
try {
|
|
103
|
+
if (statSync(subPluginsDir).isDirectory()) {
|
|
104
|
+
for (const sub of readdirSync(subPluginsDir).sort()) {
|
|
105
|
+
const subAgents = join(subPluginsDir, sub, 'agents');
|
|
106
|
+
try { if (statSync(subAgents).isDirectory()) dirs.push(subAgents); } catch { /* skip */ }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch { /* skip */ }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return dirs;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function main(maxyCodeRoot) {
|
|
116
|
+
const { defects, inspected } = runForDirs(collectAgentDirs(maxyCodeRoot));
|
|
117
|
+
for (const d of defects) {
|
|
118
|
+
const rel = d.specialistPath.startsWith(maxyCodeRoot + '/')
|
|
119
|
+
? d.specialistPath.slice(maxyCodeRoot.length + 1)
|
|
120
|
+
: d.specialistPath;
|
|
121
|
+
console.error(`[routing-prose-bash] specialist=${rel} cli=${d.cli} reason=missing-bash-tool`);
|
|
122
|
+
}
|
|
123
|
+
if (defects.length > 0) {
|
|
124
|
+
console.error(`[routing-prose-bash] FAIL: ${defects.length} defect(s) across ${inspected} gated specialist(s).`);
|
|
125
|
+
console.error(` A specialist whose routing prose names cloudflared/wrangler must list Bash in its tools:, or the prose must move to a Bash-capable surface.`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
console.log(`[routing-prose-bash] ok inspected=${inspected}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
132
|
+
const override = process.env.MAXY_CODE_ROOT;
|
|
133
|
+
main(override ? resolve(override) : DEFAULT_MAXY_CODE_ROOT);
|
|
134
|
+
}
|
|
@@ -303,26 +303,14 @@ else:
|
|
|
303
303
|
PY
|
|
304
304
|
fi
|
|
305
305
|
|
|
306
|
-
#
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
uid = os.environ["USER_ID"]
|
|
315
|
-
with open(path) as f:
|
|
316
|
-
cfg = json.load(f)
|
|
317
|
-
cfg.setdefault("admins", [])
|
|
318
|
-
if not any(a.get("userId") == uid for a in cfg["admins"]):
|
|
319
|
-
cfg["admins"].append({"userId": uid, "role": "owner"})
|
|
320
|
-
with open(path, "w") as f:
|
|
321
|
-
json.dump(cfg, f, indent=2); f.write("\n")
|
|
322
|
-
print(f" Stamped userId={uid} as owner in account.json admins")
|
|
323
|
-
PY
|
|
324
|
-
fi
|
|
325
|
-
fi
|
|
306
|
+
# Task 999 — no admins[] owner stamp at provision time. account.json admins[]
|
|
307
|
+
# is no longer the access authority: every authenticated install admin sees
|
|
308
|
+
# every account. The old stamp wrote users.json[0] (the first user, not the
|
|
309
|
+
# operating admin) into every new account, so a client account a non-first
|
|
310
|
+
# admin created listed the wrong owner and stayed invisible to them. The
|
|
311
|
+
# house account's owner record is authored durably by set-pin (writeAdminEntry
|
|
312
|
+
# in onboarding) and account.json survives reinstalls, so nothing load-bearing
|
|
313
|
+
# is lost here.
|
|
326
314
|
|
|
327
315
|
echo " Account $ACCOUNT_ID at $ACCOUNT_DIR (role=$ROLE)"
|
|
328
316
|
}
|
|
@@ -36,13 +36,29 @@ log() {
|
|
|
36
36
|
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] resume-tunnel: $1" >> "$LOG_FILE"
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
# Derive brand identifiers + the OAuth cert path up front so every refusal log
|
|
40
|
+
# below can name the brand. BRAND strips the leading dot from CONFIG_DIR
|
|
41
|
+
# (.maxy-code → cloudflared-maxy-code.service). CERT_PATH is the OAuth account
|
|
42
|
+
# cert: it authorises tunnel create/route, NOT tunnel run — a tunnel created via
|
|
43
|
+
# the API-token path runs from its credentials-file with no cert at all.
|
|
44
|
+
BRAND="${CONFIG_DIR#.}"
|
|
45
|
+
SERVICE_UNIT="cloudflared-${BRAND}.service"
|
|
46
|
+
CERT_PATH="$HOME/$CONFIG_DIR/cloudflared/cert.pem"
|
|
47
|
+
|
|
48
|
+
# A configured tunnel (tunnel.state present) that we decline to spawn is logged
|
|
49
|
+
# loudly, so a non-resumable tunnel is visible at the next service start rather
|
|
50
|
+
# than discovered as an outage. "No state file" stays a silent normal boot.
|
|
51
|
+
resume_refused() {
|
|
52
|
+
log "[tunnel-supervise] op=resume-refused brand=${BRAND} tunnel=${TUNNEL_ID:-unknown} reason=$1"
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
# No state file — no tunnel configured. Normal boot.
|
|
40
56
|
[ -f "$STATE_FILE" ] || exit 0
|
|
41
57
|
|
|
42
58
|
log "reading state from $STATE_FILE (configDir=$CONFIG_DIR)"
|
|
43
59
|
|
|
44
60
|
if ! STATE=$(jq -r '.' "$STATE_FILE" 2>/dev/null); then
|
|
45
|
-
|
|
61
|
+
resume_refused "invalid-state-json"
|
|
46
62
|
exit 0
|
|
47
63
|
fi
|
|
48
64
|
|
|
@@ -51,17 +67,10 @@ DOMAIN=$(echo "$STATE" | jq -r '.domain // empty')
|
|
|
51
67
|
CONFIG_PATH=$(echo "$STATE" | jq -r '.configPath // empty')
|
|
52
68
|
|
|
53
69
|
if [ -z "$TUNNEL_ID" ]; then
|
|
54
|
-
|
|
70
|
+
resume_refused "no-tunnel-id"
|
|
55
71
|
exit 0
|
|
56
72
|
fi
|
|
57
73
|
|
|
58
|
-
# Derive service unit name: strip leading dot from CONFIG_DIR.
|
|
59
|
-
# .maxy-code → cloudflared-maxy-code.service
|
|
60
|
-
BRAND="${CONFIG_DIR#.}"
|
|
61
|
-
SERVICE_UNIT="cloudflared-${BRAND}.service"
|
|
62
|
-
|
|
63
|
-
CERT_PATH="$HOME/$CONFIG_DIR/cloudflared/cert.pem"
|
|
64
|
-
|
|
65
74
|
# Task 833 — single-instance enforcement. cloudflared does not hot-reload
|
|
66
75
|
# config.yml; a config change needs a connector restart. resume-tunnel is the
|
|
67
76
|
# single spawner but historically managed only its OWN connector, so a connector
|
|
@@ -156,12 +165,27 @@ fi
|
|
|
156
165
|
CLOUDFLARED_BIN=$(command -v cloudflared 2>/dev/null || true)
|
|
157
166
|
|
|
158
167
|
if [ -z "$CLOUDFLARED_BIN" ]; then
|
|
159
|
-
|
|
168
|
+
resume_refused "no-cloudflared-binary"
|
|
169
|
+
exit 0
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# tunnel run authenticates with the per-tunnel credentials-file declared in
|
|
173
|
+
# config.yml (written by `tunnel create`, present in both the OAuth and the
|
|
174
|
+
# API-token models). The OAuth account cert (--origincert) is a create/route
|
|
175
|
+
# credential; tunnel run never needs it. So a tunnel is resumable whenever its
|
|
176
|
+
# config exists AND a usable run credential is present — either cert.pem (OAuth)
|
|
177
|
+
# or the credentials-file the config references (API-token). --origincert is
|
|
178
|
+
# added to the spawn only when cert.pem exists, so a token-created tunnel with
|
|
179
|
+
# no cert resumes too.
|
|
180
|
+
if [ -z "$CONFIG_PATH" ] || [ ! -f "$CONFIG_PATH" ]; then
|
|
181
|
+
resume_refused "missing-config"
|
|
160
182
|
exit 0
|
|
161
183
|
fi
|
|
162
184
|
|
|
163
|
-
|
|
164
|
-
|
|
185
|
+
CRED_FILE=$(sed -n 's/^[[:space:]]*credentials-file:[[:space:]]*//p' "$CONFIG_PATH" 2>/dev/null | head -1 | tr -d '[:space:]')
|
|
186
|
+
|
|
187
|
+
if [ ! -f "$CERT_PATH" ] && { [ -z "$CRED_FILE" ] || [ ! -f "$CRED_FILE" ]; }; then
|
|
188
|
+
resume_refused "missing-run-credential"
|
|
165
189
|
exit 0
|
|
166
190
|
fi
|
|
167
191
|
|
|
@@ -184,6 +208,13 @@ LOG_OFFSET=$(wc -c < "$LOG_FILE" 2>/dev/null | awk '{print $1+0}' || echo 0)
|
|
|
184
208
|
# once the unit is active (it does not block for the process lifetime), so there
|
|
185
209
|
# is no shell child to background. --quiet suppresses systemd-run's "Running as
|
|
186
210
|
# unit:" line; the trailing redirect only captures any error output it prints.
|
|
211
|
+
# Build the connector argv. --origincert is included only for an OAuth tunnel
|
|
212
|
+
# (cert.pem present); a token-created tunnel runs from its credentials-file
|
|
213
|
+
# alone. The array is always non-empty, so it expands safely under `set -u`.
|
|
214
|
+
CONNECTOR_ARGS=( "$CLOUDFLARED_BIN" --no-autoupdate )
|
|
215
|
+
[ -f "$CERT_PATH" ] && CONNECTOR_ARGS+=( --origincert "$CERT_PATH" )
|
|
216
|
+
CONNECTOR_ARGS+=( --config "$CONFIG_PATH" tunnel run )
|
|
217
|
+
|
|
187
218
|
systemd-run \
|
|
188
219
|
--user \
|
|
189
220
|
--quiet \
|
|
@@ -197,7 +228,7 @@ systemd-run \
|
|
|
197
228
|
-p "StandardOutput=append:$LOG_FILE" \
|
|
198
229
|
-p "StandardError=append:$LOG_FILE" \
|
|
199
230
|
-- \
|
|
200
|
-
"$
|
|
231
|
+
"${CONNECTOR_ARGS[@]}" \
|
|
201
232
|
>> "$LOG_FILE" 2>&1
|
|
202
233
|
|
|
203
234
|
# Log spawn receipt so the operator can distinguish the supervised path from legacy nohup.
|
|
@@ -18,6 +18,6 @@
|
|
|
18
18
|
account-level AGENTS.md from scratch (or extends an existing one).
|
|
19
19
|
|
|
20
20
|
Example (populated):
|
|
21
|
-
- **specialists:personal-assistant**: Scheduling, platform settings, messaging channels (Telegram, WhatsApp, email, Outlook),
|
|
21
|
+
- **specialists:personal-assistant**: Scheduling, platform settings, messaging channels (Telegram, WhatsApp, email, Outlook), and browser automation. Delegate when the work is on your calendar, configuring the platform, sending or reading messages, or driving a browser.
|
|
22
22
|
- **specialists:research-assistant**: Deep research on the web, knowledge management, and writing structured summaries with citations from multiple sources. Delegate when the operator wants you to research a topic, read web pages, combine findings into a summary, or reorganise stored knowledge.
|
|
23
23
|
-->
|
|
@@ -30,7 +30,7 @@ The operator's machine is the surface — paths admin gives you resolve against
|
|
|
30
30
|
- Graph writes (memory-write, contact-create, work-create, schedule-event) — those route to `database-operator` or `personal-assistant`.
|
|
31
31
|
- Ingestion of documents, transcripts, or archives into the graph — that routes to `librarian`.
|
|
32
32
|
- Web research and external lookups — those route to `research-assistant`.
|
|
33
|
-
- Messaging, scheduling, browser automation
|
|
33
|
+
- Messaging, scheduling, browser automation — those route to `personal-assistant`.
|
|
34
34
|
|
|
35
35
|
When a brief mixes coding work with one of the above (for example, "clone the repo and write a brochure from its README"), do the coding portion and hand the rest back to admin so it can dispatch the right specialist.
|
|
36
36
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: personal-assistant
|
|
3
|
-
description: "Scheduling, platform settings, messaging channels (Telegram, WhatsApp, email, Outlook),
|
|
3
|
+
description: "Scheduling, platform settings, messaging channels (Telegram, WhatsApp, email, Outlook), and browser automation. Delegate when the work is on your calendar, configuring the platform, sending or reading messages, or driving a browser."
|
|
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."
|
|
5
5
|
model: claude-sonnet-4-6
|
|
6
6
|
tools: Skill, mcp__plugin_admin_admin__system-status, mcp__plugin_admin_admin__brand-settings, mcp__plugin_admin_admin__account-manage, mcp__plugin_admin_admin__account-update, mcp__plugin_admin_admin__logs-read, mcp__plugin_admin_admin__plugin-read, mcp__plugin_admin_admin__wifi, mcp__plugin_contacts_contacts__contact-create, mcp__plugin_contacts_contacts__contact-lookup, mcp__plugin_contacts_contacts__contact-update, mcp__plugin_contacts_contacts__contact-delete, mcp__plugin_contacts_contacts__contact-list, mcp__plugin_contacts_contacts__contact-export, mcp__plugin_contacts_contacts__contact-erase, mcp__plugin_contacts_contacts__group-create, mcp__plugin_contacts_contacts__group-manage, mcp__plugin_telegram_telegram__message, mcp__plugin_telegram_telegram__message-history, mcp__plugin_telegram_telegram__telegram-webhook-register, mcp__plugin_whatsapp_whatsapp__whatsapp-login-start, mcp__plugin_whatsapp_whatsapp__whatsapp-login-wait, mcp__plugin_whatsapp_whatsapp__whatsapp-status, mcp__plugin_whatsapp_whatsapp__whatsapp-disconnect, mcp__plugin_whatsapp_whatsapp__whatsapp-config, mcp__plugin_whatsapp_whatsapp__whatsapp-activity, mcp__plugin_whatsapp_whatsapp__whatsapp-conversations, mcp__plugin_whatsapp_whatsapp__whatsapp-messages, mcp__plugin_whatsapp_whatsapp__whatsapp-conversation-graph-state, mcp__plugin_whatsapp_whatsapp__whatsapp-group-info, mcp__plugin_email_email__email-provider-info, mcp__plugin_email_email__email-setup, mcp__plugin_email_email__email-read, mcp__plugin_email_email__email-send, mcp__plugin_email_email__email-draft, mcp__plugin_email_email__email-draft-edit, mcp__plugin_email_email__email-draft-send, mcp__plugin_email_email__email-reply, mcp__plugin_email_email__email-search, mcp__plugin_email_email__email-graph-query, mcp__plugin_email_email__email-otp-extract, mcp__plugin_email_email__email-status, mcp__plugin_email_email__email-fetch, mcp__plugin_email_email__email-ingest, mcp__plugin_outlook_outlook__outlook-account-register, mcp__plugin_outlook_outlook__outlook-mail-list, mcp__plugin_outlook_outlook__outlook-mail-search, mcp__plugin_outlook_outlook__outlook-calendar-list, mcp__plugin_outlook_outlook__outlook-calendar-event, mcp__plugin_outlook_outlook__outlook-contacts-list, mcp__plugin_outlook_outlook__outlook-mailbox-info, mcp__plugin_scheduling_scheduling__schedule-event, mcp__plugin_scheduling_scheduling__schedule-list, mcp__plugin_scheduling_scheduling__schedule-get, mcp__plugin_scheduling_scheduling__schedule-update, mcp__plugin_scheduling_scheduling__schedule-cancel, mcp__plugin_scheduling_scheduling__schedule-export-ics, mcp__plugin_scheduling_scheduling__schedule-import-ics, mcp__plugin_scheduling_scheduling__time-resolve, mcp__plugin_memory_memory__memory-search, mcp__plugin_memory_memory__profile-update, mcp__plugin_url-get_url-get__url-get, mcp__plugin_browser_browser__browser-render, mcp__plugin_browser_browser__browser-navigate, mcp__plugin_browser_browser__browser-snapshot, mcp__plugin_browser_browser__browser-click, mcp__plugin_browser_browser__browser-fill, mcp__plugin_browser_browser__browser-fill-form, mcp__plugin_browser_browser__browser-type, mcp__plugin_browser_browser__browser-press-key, mcp__plugin_browser_browser__browser-hover, mcp__plugin_browser_browser__browser-select-option, mcp__plugin_browser_browser__browser-wait-for, mcp__plugin_browser_browser__browser-handle-dialog, mcp__plugin_browser_browser__browser-evaluate, mcp__plugin_browser_browser__browser-console-messages, mcp__plugin_browser_browser__browser-tabs, mcp__plugin_browser_browser__browser-pdf-save, mcp__plugin_browser_browser__browser-screenshot, mcp__plugin_browser_browser__browser-resize
|
|
@@ -23,9 +23,10 @@ These three rules win when anything else in this prompt conflicts with them.
|
|
|
23
23
|
Each domain has a small set of tools and, where it exists, a skill that drives the multi-step flow. Match the brief to the domain, load the skill if one is named, and run the tools the skill prescribes.
|
|
24
24
|
|
|
25
25
|
- **WhatsApp setup or config:** **After routing, the first tool call MUST be `Skill connect-whatsapp` (for QR pairing and admin-phone setup) or `Skill manage-whatsapp-config` (for DM/group policies). Any channel tool call before the routed skill's content is loaded into context is a contract violation.** Load `skill-load skillName=connect-whatsapp` for QR pairing and admin-phone setup; load `skill-load skillName=manage-whatsapp-config` for DM/group policies and admin-phone management. The skills carry the per-phase flow.
|
|
26
|
-
- **Cloudflare (tunnel, DNS, Pages hosting, D1):** **After routing, the first tool call MUST be `Skill cloudflare`. Any Bash call before the skill content is loaded into context is a contract violation.** Load `skill-load skillName=cloudflare`. The skill routes each operation class to its reference under `plugins/cloudflare/references/` (`manual-setup.md` for the tunnel, `api.md`, `hosting-sites.md`, `d1-data-capture.md`, `reset-guide.md`, `dashboard-guide.md`) and drives `cloudflared` / `wrangler` directly via Bash. There is no shell-script wrapper; PTY stdout is the only surface.
|
|
27
26
|
- **Every other domain** (scheduling, Telegram, email, Outlook, contacts, browser, platform admin) runs through the tool descriptions injected into your system prompt. The rules below apply across these domains regardless of which tool is invoked.
|
|
28
27
|
|
|
28
|
+
Cloudflare and custom-domain setup (tunnel, DNS, Pages, D1) is **not** your work — it needs a shell you do not hold, so the admin session owns it directly. If such a brief reaches you, hand it back to admin.
|
|
29
|
+
|
|
29
30
|
## Cross-domain rules
|
|
30
31
|
|
|
31
32
|
**Brain-first read before action.** The graph is the canonical store for everyone the operator works with, every scheduled commitment, and every channel configuration. Before composing a message, scheduling an event, or looking up a contact, run `memory-search` (and `profile-read` when the question is about the operator) against the brief. Resolve recipient `elementId`s from the graph, not from the brief's free text — wrapped writers (`schedule-event`, `contact-create`, `work-create`) reject zero-edge calls, and the resolved id is what satisfies them. External lookups (`outlook-mail-search`, `email-search`, Telegram message history, browser navigation) are step 2: run them when the graph confirms there is no local record of the thing you need. Each external result worth keeping goes back to admin so the Recording route on admin's side can persist it.
|
|
@@ -38,8 +39,6 @@ Each domain has a small set of tools and, where it exists, a skill that drives t
|
|
|
38
39
|
|
|
39
40
|
**WhatsApp ToS.** Automated broadcast is forbidden. The platform blocks broadcast for WhatsApp regardless of channel config; do not try to design around it.
|
|
40
41
|
|
|
41
|
-
**Cloudflare has two auth paths.** Cloudflare exposes zero MCP tools; load the `cloudflare` skill and follow its references. Tunnel auth is OAuth (`cloudflared tunnel login`) — the operator clicks the linkified URL in their own browser. Everything else (DNS, Pages, D1, Access) is the Cloudflare API, authenticated by a short-lived narrow token the agent mints from the operator's master token (see `plugins/cloudflare/references/api.md`). Do not drive the dashboard with a browser tool, do not browser-automate master-token creation, do not synthesise `cloudflared` flag combinations, and never write or echo a token. Do not edit `cert.pem`, `tunnel.state`, `config.yml`, or `alias-domains.json` outside the runbook. When a sanctioned surface fails, report with evidence (secrets redacted) and cite `plugins/cloudflare/references/reset-guide.md`. After a tunnel setup finishes, verify with `curl -I https://<hostname>`; a non-530 response means the tunnel is live.
|
|
42
|
-
|
|
43
42
|
**Reading JavaScript-rendered pages.** When you only need to *read* a JS page and `url-get` comes back empty or as a shell, use `browser-render` with the URL. It renders the page in the device's Chromium and returns the rendered HTML plus visible text in one stateless call — it does not keep the page open.
|
|
44
43
|
|
|
45
44
|
**Automating a page.** When a task needs to *act* on a page (log in, fill a form, click through a flow, drive a web app), use the persistent automation tools. Start with `browser-navigate` to open the page and keep it alive, then `browser-snapshot` to see what is on the page — it returns each interactive element as a role, a name, and a unique CSS selector. Pass those selectors verbatim to `browser-click`, `browser-fill`, `browser-fill-form`, `browser-hover`, and `browser-select-option`. Use `browser-type` / `browser-press-key` for real keyboard input (e.g. Enter to submit), `browser-wait-for` after anything that loads asynchronously, `browser-evaluate` to read page state, and `browser-console-messages` to debug. Arm `browser-handle-dialog` *before* the action that triggers an alert/confirm/prompt, because dialogs block the page. `browser-tabs` opens or switches to a fresh page so stale state from a prior task does not leak in. If an action returns `session-lost`, the page closed — call `browser-navigate` again. Element targeting is by CSS selector (always from a fresh `browser-snapshot`), not by Playwright-style refs. Report CAPTCHA or bot detection as a blocker; do not try to solve it.
|
|
@@ -3905,33 +3905,6 @@ function isActiveAgentSlug(accountDir, slug) {
|
|
|
3905
3905
|
function getDefaultAccountId() {
|
|
3906
3906
|
return resolveAccount()?.accountId ?? null;
|
|
3907
3907
|
}
|
|
3908
|
-
function resolveUserAccounts(userId) {
|
|
3909
|
-
if (!existsSync5(ACCOUNTS_DIR)) return [];
|
|
3910
|
-
const results = [];
|
|
3911
|
-
const entries = readdirSync(ACCOUNTS_DIR, { withFileTypes: true });
|
|
3912
|
-
for (const entry of entries) {
|
|
3913
|
-
if (!entry.isDirectory()) continue;
|
|
3914
|
-
const configPath = resolve4(ACCOUNTS_DIR, entry.name, "account.json");
|
|
3915
|
-
if (!existsSync5(configPath)) continue;
|
|
3916
|
-
let config;
|
|
3917
|
-
try {
|
|
3918
|
-
config = JSON.parse(readFileSync6(configPath, "utf-8"));
|
|
3919
|
-
} catch {
|
|
3920
|
-
console.error(`[session] account.json corrupt at ${configPath} \u2014 skipping`);
|
|
3921
|
-
continue;
|
|
3922
|
-
}
|
|
3923
|
-
const adminEntry = config.admins?.find((a) => a.userId === userId);
|
|
3924
|
-
if (adminEntry) {
|
|
3925
|
-
results.push({
|
|
3926
|
-
accountId: config.accountId,
|
|
3927
|
-
accountDir: resolve4(ACCOUNTS_DIR, entry.name),
|
|
3928
|
-
config,
|
|
3929
|
-
role: adminEntry.role
|
|
3930
|
-
});
|
|
3931
|
-
}
|
|
3932
|
-
}
|
|
3933
|
-
return results;
|
|
3934
|
-
}
|
|
3935
3908
|
|
|
3936
3909
|
// app/lib/claude-agent/session-store.ts
|
|
3937
3910
|
import { createHash } from "crypto";
|
|
@@ -5781,7 +5754,6 @@ export {
|
|
|
5781
5754
|
resolveAgentConfig,
|
|
5782
5755
|
isActiveAgentSlug,
|
|
5783
5756
|
getDefaultAccountId,
|
|
5784
|
-
resolveUserAccounts,
|
|
5785
5757
|
fingerprintSessionKey,
|
|
5786
5758
|
registerSession,
|
|
5787
5759
|
validateSession,
|
package/payload/server/server.js
CHANGED
|
@@ -85,7 +85,6 @@ import {
|
|
|
85
85
|
resolveCallback,
|
|
86
86
|
resolveClientIp,
|
|
87
87
|
resolveDefaultAgentSlug,
|
|
88
|
-
resolveUserAccounts,
|
|
89
88
|
runAdminUserSelfHeal,
|
|
90
89
|
safeJson,
|
|
91
90
|
sanitizeClientCorrId,
|
|
@@ -106,7 +105,7 @@ import {
|
|
|
106
105
|
vncLog,
|
|
107
106
|
walkPremiumBundles,
|
|
108
107
|
writeAdminUserAndPerson
|
|
109
|
-
} from "./chunk-
|
|
108
|
+
} from "./chunk-NE7G5GT7.js";
|
|
110
109
|
import {
|
|
111
110
|
__commonJS,
|
|
112
111
|
__toESM
|
|
@@ -7030,27 +7029,22 @@ function resolveTitle(body, sessionId, userTitles) {
|
|
|
7030
7029
|
}
|
|
7031
7030
|
app3.get("/", requireAdminSession, async (c) => {
|
|
7032
7031
|
const accountId = process.env.ACCOUNT_ID ?? null;
|
|
7033
|
-
const switcherUserId = (() => {
|
|
7034
|
-
const cacheKey = c.get("cacheKey");
|
|
7035
|
-
return cacheKey ? getUserIdForSession(cacheKey) : void 0;
|
|
7036
|
-
})();
|
|
7037
7032
|
const accounts = [];
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
} catch {
|
|
7045
|
-
}
|
|
7046
|
-
accounts.push({
|
|
7047
|
-
accountId: a.accountId,
|
|
7048
|
-
role: a.config.role ?? "",
|
|
7049
|
-
businessName,
|
|
7050
|
-
isHouse: a.config.role === "house"
|
|
7051
|
-
});
|
|
7033
|
+
for (const a of listValidAccounts()) {
|
|
7034
|
+
let businessName = null;
|
|
7035
|
+
try {
|
|
7036
|
+
const branding = await fetchBranding(a.accountId);
|
|
7037
|
+
businessName = branding?.name || null;
|
|
7038
|
+
} catch {
|
|
7052
7039
|
}
|
|
7040
|
+
accounts.push({
|
|
7041
|
+
accountId: a.accountId,
|
|
7042
|
+
role: a.config.role ?? "",
|
|
7043
|
+
businessName,
|
|
7044
|
+
isHouse: a.config.role === "house"
|
|
7045
|
+
});
|
|
7053
7046
|
}
|
|
7047
|
+
console.log(`[admin-sessions-list] accounts=${accounts.length}`);
|
|
7054
7048
|
const configDir2 = claudeConfigDir();
|
|
7055
7049
|
if (!configDir2) {
|
|
7056
7050
|
console.error("[admin-sessions-list] CLAUDE_CONFIG_DIR not set; returning empty list");
|
|
@@ -7181,38 +7175,6 @@ function validatePin(pin, usersFilePath) {
|
|
|
7181
7175
|
const matched = users.find((u) => u.pin === inputHash);
|
|
7182
7176
|
return matched ? { kind: "match", userId: matched.userId } : { kind: "miss" };
|
|
7183
7177
|
}
|
|
7184
|
-
function scanForOrphanedAccountAdmins(users, accountsDir) {
|
|
7185
|
-
const known = new Set(users.map((u) => u.userId));
|
|
7186
|
-
const orphans = [];
|
|
7187
|
-
if (!existsSync6(accountsDir)) return orphans;
|
|
7188
|
-
let entries;
|
|
7189
|
-
try {
|
|
7190
|
-
entries = readdirSync5(accountsDir);
|
|
7191
|
-
} catch {
|
|
7192
|
-
return orphans;
|
|
7193
|
-
}
|
|
7194
|
-
for (const entry of entries) {
|
|
7195
|
-
if (entry.startsWith(".")) continue;
|
|
7196
|
-
const accountDir = join10(accountsDir, entry);
|
|
7197
|
-
try {
|
|
7198
|
-
if (!statSync3(accountDir).isDirectory()) continue;
|
|
7199
|
-
} catch {
|
|
7200
|
-
continue;
|
|
7201
|
-
}
|
|
7202
|
-
const accountJsonPath = join10(accountDir, "account.json");
|
|
7203
|
-
if (!existsSync6(accountJsonPath)) continue;
|
|
7204
|
-
try {
|
|
7205
|
-
const config = JSON.parse(readFileSync11(accountJsonPath, "utf-8"));
|
|
7206
|
-
const admins = config.admins ?? [];
|
|
7207
|
-
for (const a of admins) {
|
|
7208
|
-
if (typeof a.userId === "string" && !known.has(a.userId)) orphans.push(a.userId);
|
|
7209
|
-
}
|
|
7210
|
-
} catch {
|
|
7211
|
-
continue;
|
|
7212
|
-
}
|
|
7213
|
-
}
|
|
7214
|
-
return orphans;
|
|
7215
|
-
}
|
|
7216
7178
|
|
|
7217
7179
|
// app/lib/whatsapp/inbound/resolve-admin-userid.ts
|
|
7218
7180
|
function classifyAdminUserId(users, admins, senderPhone) {
|
|
@@ -10919,6 +10881,9 @@ async function createAdminSession(accountId, thinkingView, userId, userName, rol
|
|
|
10919
10881
|
sessionId: initialSessionId
|
|
10920
10882
|
};
|
|
10921
10883
|
}
|
|
10884
|
+
function operatorRoleFor(config, userId) {
|
|
10885
|
+
return config.admins?.find((a) => a.userId === userId)?.role ?? "admin";
|
|
10886
|
+
}
|
|
10922
10887
|
var app11 = new Hono();
|
|
10923
10888
|
app11.get("/", async (c) => {
|
|
10924
10889
|
const signedSessionToken = c.req.query("session_key");
|
|
@@ -10983,27 +10948,14 @@ app11.post("/", async (c) => {
|
|
|
10983
10948
|
return c.json({ error: "PIN not configured. Complete onboarding first." }, 503);
|
|
10984
10949
|
}
|
|
10985
10950
|
if (verdict.kind === "miss") {
|
|
10986
|
-
|
|
10987
|
-
try {
|
|
10988
|
-
users = readUsersFile(USERS_FILE) ?? [];
|
|
10989
|
-
} catch {
|
|
10990
|
-
}
|
|
10991
|
-
const orphanUserIds = scanForOrphanedAccountAdmins(users, ACCOUNTS_DIR);
|
|
10992
|
-
if (orphanUserIds.length > 0) {
|
|
10993
|
-
const prefix = orphanUserIds[0].slice(0, 8);
|
|
10994
|
-
console.log(
|
|
10995
|
-
`[session] PIN auth failed: no matching user; account.json admins[] contains userId=${prefix} with no users.json row \u2014 three-store divergence`
|
|
10996
|
-
);
|
|
10997
|
-
} else {
|
|
10998
|
-
console.log(`[session] PIN auth failed: no matching user`);
|
|
10999
|
-
}
|
|
10951
|
+
console.log(`[session] PIN auth failed: no matching user`);
|
|
11000
10952
|
return c.json({ error: "Invalid PIN" }, 401);
|
|
11001
10953
|
}
|
|
11002
10954
|
const { userId } = verdict;
|
|
11003
|
-
const accounts =
|
|
10955
|
+
const accounts = listValidAccounts();
|
|
11004
10956
|
if (accounts.length === 0) {
|
|
11005
|
-
console.log(`[session]
|
|
11006
|
-
return c.json({ error: "No
|
|
10957
|
+
console.log(`[session] no accounts configured on install: userId=${userId}`);
|
|
10958
|
+
return c.json({ error: "No accounts are configured on this install." }, 403);
|
|
11007
10959
|
}
|
|
11008
10960
|
if (accounts.length > 1 && !body.accountId) {
|
|
11009
10961
|
console.log(`[session] multi-account user: userId=${userId} accounts=${accounts.length}`);
|
|
@@ -11015,7 +10967,7 @@ app11.post("/", async (c) => {
|
|
|
11015
10967
|
businessName = branding?.name || void 0;
|
|
11016
10968
|
} catch {
|
|
11017
10969
|
}
|
|
11018
|
-
return { accountId: a.accountId, businessName, role: a.
|
|
10970
|
+
return { accountId: a.accountId, businessName, role: operatorRoleFor(a.config, userId) };
|
|
11019
10971
|
})
|
|
11020
10972
|
);
|
|
11021
10973
|
return c.json({ accounts: accountList, userId });
|
|
@@ -11026,7 +10978,7 @@ app11.post("/", async (c) => {
|
|
|
11026
10978
|
return c.json({ error: "Invalid account selection." }, 403);
|
|
11027
10979
|
}
|
|
11028
10980
|
const { userName, avatar } = await resolveUserIdentity(selected.accountId, userId);
|
|
11029
|
-
const payload = await createAdminSession(selected.accountId, selected.config.thinkingView, userId, userName, selected.
|
|
10981
|
+
const payload = await createAdminSession(selected.accountId, selected.config.thinkingView, userId, userName, operatorRoleFor(selected.config, userId), avatar);
|
|
11030
10982
|
return c.json(payload);
|
|
11031
10983
|
});
|
|
11032
10984
|
var session_default = app11;
|