@rubytech/create-realagent 1.0.816 → 1.0.818
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/graph-search/src/__tests__/fulltext-coverage.test.ts +1 -1
- package/payload/platform/lib/graph-write/dist/index.d.ts +5 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +14 -0
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/index.ts +25 -0
- package/payload/platform/neo4j/edge-annotations.json +0 -8
- package/payload/platform/neo4j/migrations/004-prune-alien-accounts.ts +3 -4
- package/payload/platform/neo4j/migrations/005-removed-review-feature.ts +102 -0
- package/payload/platform/neo4j/schema.cypher +1 -22
- package/payload/platform/plugins/admin/PLUGIN.md +1 -8
- package/payload/platform/plugins/admin/mcp/dist/index.js +6 -44
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +2 -2
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +2 -3
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js.map +1 -1
- package/payload/platform/plugins/docs/references/cloudflare.md +1 -3
- package/payload/platform/plugins/docs/references/memory-guide.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +8 -12
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
- package/payload/platform/scripts/logs-read.sh +8 -38
- package/payload/server/chunk-AJLGI7Y3.js +10067 -0
- package/payload/server/chunk-ON3LBL2Y.js +1114 -0
- package/payload/server/chunk-P3HTEK33.js +10074 -0
- package/payload/server/chunk-PXQA2MA3.js +2518 -0
- package/payload/server/client-pool-GBY5I2KQ.js +31 -0
- package/payload/server/maxy-edge.js +3 -3
- package/payload/server/neo4j-migrations-STCKDWAL.js +364 -0
- package/payload/server/public/assets/{admin-Cxtmv0wo.js → admin-CdVYoqKD.js} +20 -20
- package/payload/server/public/assets/{graph-C4-jEPDE.js → graph-DeH6ulGh.js} +1 -1
- package/payload/server/public/assets/{page-zuI00fuC.js → page-WIAWD2Oi.js} +1 -1
- package/payload/server/public/graph.html +2 -2
- package/payload/server/public/index.html +2 -2
- package/payload/server/server.js +326 -2236
package/payload/server/server.js
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE,
|
|
7
7
|
TELEGRAM_WEBHOOK_SECRET_FILE,
|
|
8
8
|
USERS_FILE,
|
|
9
|
-
actionLogPath,
|
|
10
9
|
autoDeliverPremiumPlugins,
|
|
11
10
|
buildX11Env,
|
|
12
11
|
callOauthLlm,
|
|
@@ -30,7 +29,6 @@ import {
|
|
|
30
29
|
launchAction,
|
|
31
30
|
load,
|
|
32
31
|
logPath,
|
|
33
|
-
reconcileCloudflareSetupFromLog,
|
|
34
32
|
recordFailedAttempt,
|
|
35
33
|
render,
|
|
36
34
|
renderLoginPage,
|
|
@@ -53,7 +51,7 @@ import {
|
|
|
53
51
|
vncLog,
|
|
54
52
|
waitForExit,
|
|
55
53
|
writeChromiumWrapper
|
|
56
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-AJLGI7Y3.js";
|
|
57
55
|
import {
|
|
58
56
|
agentLogStream,
|
|
59
57
|
clearSessionHistory,
|
|
@@ -81,7 +79,7 @@ import {
|
|
|
81
79
|
sigtermFlushStreamLogs,
|
|
82
80
|
unregisterSession,
|
|
83
81
|
validateSession
|
|
84
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-ON3LBL2Y.js";
|
|
85
83
|
import {
|
|
86
84
|
ACCOUNTS_DIR,
|
|
87
85
|
GREETING_DIRECTIVE,
|
|
@@ -121,7 +119,7 @@ import {
|
|
|
121
119
|
verifyAndGetConversationUpdatedAt,
|
|
122
120
|
verifyConversationOwnership,
|
|
123
121
|
writeAdminUserAndPerson
|
|
124
|
-
} from "./chunk-
|
|
122
|
+
} from "./chunk-PXQA2MA3.js";
|
|
125
123
|
|
|
126
124
|
// ../lib/graph-trash/dist/index.js
|
|
127
125
|
var require_dist = __commonJS({
|
|
@@ -620,15 +618,15 @@ var serveStatic = (options = { root: "" }) => {
|
|
|
620
618
|
};
|
|
621
619
|
|
|
622
620
|
// server/index.ts
|
|
623
|
-
import { readFileSync as
|
|
624
|
-
import { resolve as
|
|
621
|
+
import { readFileSync as readFileSync15, existsSync as existsSync21, watchFile } from "fs";
|
|
622
|
+
import { resolve as resolve21, join as join9, basename as basename5 } from "path";
|
|
625
623
|
import { homedir as homedir2 } from "os";
|
|
626
624
|
|
|
627
625
|
// app/lib/agent-slug-pattern.ts
|
|
628
626
|
var AGENT_SLUG_PATTERN = /^\/([a-z][a-z0-9-]{2,49})$/;
|
|
629
627
|
|
|
630
628
|
// server/routes/health.ts
|
|
631
|
-
import { existsSync as
|
|
629
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
632
630
|
import { createConnection } from "net";
|
|
633
631
|
|
|
634
632
|
// app/lib/network.ts
|
|
@@ -649,1608 +647,6 @@ function getLanIp() {
|
|
|
649
647
|
return fallback;
|
|
650
648
|
}
|
|
651
649
|
|
|
652
|
-
// app/lib/review-detector/boot.ts
|
|
653
|
-
import { basename as basename2 } from "path";
|
|
654
|
-
|
|
655
|
-
// app/lib/review-detector/rules.ts
|
|
656
|
-
import { readFileSync, writeFileSync, existsSync as existsSync2, statSync as statSync2, mkdirSync, renameSync } from "fs";
|
|
657
|
-
import { resolve, dirname } from "path";
|
|
658
|
-
var DEFAULT_SCAN_INTERVAL_MS = 5e3;
|
|
659
|
-
var RATE_LIMIT_PATTERN = "rate[- ]?limit(?:ed| reached| hit)|(?:HTTP|status)[^a-z]{0,3}429|too many requests";
|
|
660
|
-
var RATE_LIMIT_PATTERN_V1 = "\\b429\\b|rate.?limit|too.?many.?requests";
|
|
661
|
-
var VALID_TYPES = /* @__PURE__ */ new Set([
|
|
662
|
-
"reconnect-loop",
|
|
663
|
-
"repeated-error",
|
|
664
|
-
"silent-catch",
|
|
665
|
-
"file-write-storm",
|
|
666
|
-
"stale-log",
|
|
667
|
-
"rate-limit",
|
|
668
|
-
"absent-followup"
|
|
669
|
-
]);
|
|
670
|
-
var MAX_FOLLOWUP_WINDOW_MS = 6e5;
|
|
671
|
-
var VALID_SOURCES = /* @__PURE__ */ new Set([
|
|
672
|
-
"any",
|
|
673
|
-
"server",
|
|
674
|
-
"vnc",
|
|
675
|
-
"system",
|
|
676
|
-
"error",
|
|
677
|
-
"session",
|
|
678
|
-
"public",
|
|
679
|
-
"mcp",
|
|
680
|
-
"cloudflared",
|
|
681
|
-
"config-dir"
|
|
682
|
-
]);
|
|
683
|
-
var VALID_SCOPES = /* @__PURE__ */ new Set(["global", "session"]);
|
|
684
|
-
function defaultRules() {
|
|
685
|
-
return [
|
|
686
|
-
{
|
|
687
|
-
id: "whatsapp-reconnect-loop",
|
|
688
|
-
name: "WhatsApp Baileys reconnect loop",
|
|
689
|
-
type: "reconnect-loop",
|
|
690
|
-
logSource: "server",
|
|
691
|
-
pattern: "\\[whatsapp:baileys\\] ERROR",
|
|
692
|
-
thresholdCount: 5,
|
|
693
|
-
thresholdWindowMinutes: 10,
|
|
694
|
-
suggestedAction: "Check Baileys init-queries error. Run `/investigate` on the WhatsApp subsystem, or pause WhatsApp and re-pair the account from chat."
|
|
695
|
-
},
|
|
696
|
-
{
|
|
697
|
-
id: "repeated-error-generic",
|
|
698
|
-
name: "Repeated error signature",
|
|
699
|
-
type: "repeated-error",
|
|
700
|
-
// Match generic ERROR lines with a bracketed prefix. Fingerprinting by
|
|
701
|
-
// first 80 chars happens in the evaluator.
|
|
702
|
-
logSource: "server",
|
|
703
|
-
pattern: "\\] ERROR ",
|
|
704
|
-
thresholdCount: 20,
|
|
705
|
-
thresholdWindowMinutes: 60,
|
|
706
|
-
suggestedAction: "Repeated error burst. Tail the relevant log via `logs-read` and identify the upstream cause."
|
|
707
|
-
},
|
|
708
|
-
{
|
|
709
|
-
id: "silent-catch-fingerprint",
|
|
710
|
-
name: "Silent catch-block fingerprint",
|
|
711
|
-
type: "silent-catch",
|
|
712
|
-
// Patterns that historically indicated a silent catch. Every match is
|
|
713
|
-
// worth an alert — never suppressed by frequency.
|
|
714
|
-
logSource: "any",
|
|
715
|
-
pattern: "(UnhandledPromiseRejection|catch.*ignored|swallowed error|silent failure)",
|
|
716
|
-
thresholdCount: 0,
|
|
717
|
-
thresholdWindowMinutes: 0,
|
|
718
|
-
suggestedAction: "A silent-failure fingerprint appeared in logs. Read the surrounding context and turn the catch into loud failure per CLAUDE.md rule 'Observability Is Non-Negotiable'."
|
|
719
|
-
},
|
|
720
|
-
{
|
|
721
|
-
id: "credentials-write-storm",
|
|
722
|
-
name: "Credentials directory write storm",
|
|
723
|
-
type: "file-write-storm",
|
|
724
|
-
logSource: "config-dir",
|
|
725
|
-
pattern: "",
|
|
726
|
-
watchPath: "credentials",
|
|
727
|
-
thresholdCount: 5,
|
|
728
|
-
thresholdWindowMinutes: 5,
|
|
729
|
-
suggestedAction: "The credentials directory is being rewritten repeatedly. This usually means a subsystem is stuck in an auth-retry loop. Check which file is being rewritten and trace the caller."
|
|
730
|
-
},
|
|
731
|
-
// The stale-log rule type is fully supported by the evaluator but the
|
|
732
|
-
// default seed does not ship an instance of it. Choosing the right file
|
|
733
|
-
// to watch is subsystem-specific (e.g. a plugin-specific log that stops
|
|
734
|
-
// when the plugin dies); server.log is the wrong target because it is
|
|
735
|
-
// written continuously by the detector's own cycle events, so it never
|
|
736
|
-
// goes stale. Users can add a targeted stale-log rule via
|
|
737
|
-
// `review-rules-add` when they know which file matters for their
|
|
738
|
-
// subsystem.
|
|
739
|
-
{
|
|
740
|
-
id: "http-rate-limit-429",
|
|
741
|
-
name: "HTTP 429 rate-limit hit",
|
|
742
|
-
type: "rate-limit",
|
|
743
|
-
logSource: "any",
|
|
744
|
-
pattern: RATE_LIMIT_PATTERN,
|
|
745
|
-
thresholdCount: 0,
|
|
746
|
-
thresholdWindowMinutes: 0,
|
|
747
|
-
suggestedAction: "An external API returned 429 (rate-limited). Identify the caller and add backoff, or check the quota on the relevant API key."
|
|
748
|
-
},
|
|
749
|
-
{
|
|
750
|
-
id: "approval-bypass-detected",
|
|
751
|
-
name: "Approval gating bypass",
|
|
752
|
-
type: "repeated-error",
|
|
753
|
-
logSource: "error",
|
|
754
|
-
pattern: "\\[persist\\].*(?:email-send|email-reply|whatsapp-send|whatsapp-send-document|message|contact-erase).*approval=auto-executed",
|
|
755
|
-
thresholdCount: 0,
|
|
756
|
-
thresholdWindowMinutes: 0,
|
|
757
|
-
suggestedAction: "An external-facing tool executed with auto-executed approval state. Check account.json approvalPolicy \u2014 if this tool should require review, the gating hook may have been bypassed or the policy was changed."
|
|
758
|
-
},
|
|
759
|
-
{
|
|
760
|
-
// Task 530: catches the bridgeai-style class where a single conversation
|
|
761
|
-
// sees the same tool error repeatedly and the agent silently falls back.
|
|
762
|
-
// Session-scoped so cross-conversation coincidence doesn't trigger it.
|
|
763
|
-
id: "tool-result-recurring-errors",
|
|
764
|
-
name: "Tool result errors recurring in a conversation",
|
|
765
|
-
type: "repeated-error",
|
|
766
|
-
logSource: "system",
|
|
767
|
-
pattern: "\\[tool-result\\].*error=true",
|
|
768
|
-
thresholdCount: 2,
|
|
769
|
-
thresholdWindowMinutes: 5,
|
|
770
|
-
scope: "session",
|
|
771
|
-
suggestedAction: 'The same conversation has logged multiple tool failures. Use the admin `logs-read` MCP tool with `type: "system"` (or the `logs-read.sh` script with the conversationId) to retrieve the adjacent [tool-failure-diag] lines and identify whether the cause is DNS, TCP, HTTP, or the tool\'s internal pipeline \u2014 then adapt the next attempt to match. Never retry the same tool against the same target without diagnostic-grounded reasoning.'
|
|
772
|
-
},
|
|
773
|
-
{
|
|
774
|
-
// Task 532: closes the "60-second black-box tool wait" class. Fires when
|
|
775
|
-
// a tool hits the 30-second mark of the mid-flight heartbeat. The
|
|
776
|
-
// pattern anchors on elapsed=30s specifically (the tool-wait tick emits
|
|
777
|
-
// one line per 5s per tool, so matching every tick would be noisy) and
|
|
778
|
-
// excludes tools whose long runtime is expected: `Task`/`Agent` subagent
|
|
779
|
-
// dispatch, `Bash` with explicit long timeouts.
|
|
780
|
-
id: "tool-wait-long-stall",
|
|
781
|
-
name: "Tool wait exceeds 30 seconds (possible stall)",
|
|
782
|
-
type: "repeated-error",
|
|
783
|
-
logSource: "system",
|
|
784
|
-
pattern: "\\[tool-wait\\][^\\n]*name=(?!Task\\b|Agent\\b|Bash\\b)[A-Za-z0-9_]+[^\\n]*elapsed=30s",
|
|
785
|
-
thresholdCount: 0,
|
|
786
|
-
thresholdWindowMinutes: 0,
|
|
787
|
-
scope: "session",
|
|
788
|
-
suggestedAction: "A tool call has been pending for 30 seconds without a result. Read the adjacent [tool-wait-diag] and [tool-wait-proc] lines in the conversation's stream log to determine whether the network remained healthy, the subprocess held active sockets, and the HTTP request reached the wire. If diag shows a healthy network but the subprocess has no [subproc-stderr] UNDICI/HTTP activity during the wait window, the tool's internal pipeline is stalled \u2014 do not retry the same request against the same target without a change in approach."
|
|
789
|
-
},
|
|
790
|
-
{
|
|
791
|
-
// Task 536: detect agents ignoring the WEBFETCH_CANNOT_READ_JS_SPA
|
|
792
|
-
// structured failure. A single SPA short-circuit per conversation is
|
|
793
|
-
// expected — the hook is doing its job. Two or more in the same
|
|
794
|
-
// conversation within 5 minutes means either (a) the agent retried
|
|
795
|
-
// WebFetch on the same SPA URL despite the directive, or (b) the
|
|
796
|
-
// owner is asking about multiple SPA URLs in one session and the
|
|
797
|
-
// pattern needs surfacing as a recurring class. Both signal that the
|
|
798
|
-
// IDENTITY.md "Tool Failure Discipline" guidance is not landing in the
|
|
799
|
-
// prompt — revise the copy rather than add mechanical enforcement.
|
|
800
|
-
id: "webfetch-spa-short-circuit-recurring",
|
|
801
|
-
name: "WebFetch JS-SPA short-circuit fired repeatedly in conversation",
|
|
802
|
-
type: "repeated-error",
|
|
803
|
-
logSource: "system",
|
|
804
|
-
pattern: "WEBFETCH_CANNOT_READ_JS_SPA",
|
|
805
|
-
thresholdCount: 2,
|
|
806
|
-
thresholdWindowMinutes: 5,
|
|
807
|
-
scope: "session",
|
|
808
|
-
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."
|
|
809
|
-
},
|
|
810
|
-
{
|
|
811
|
-
// Task 867 — fires when setup-tunnel.sh emits step=done but no
|
|
812
|
-
// `[persist] role=user … Cloudflare setup completed (actionId: <id>)`
|
|
813
|
-
// line appears within 60s. Covers the three failure modes of the
|
|
814
|
-
// action-relay-queue plumbing: queue-write failed, boot-drain consumer
|
|
815
|
-
// skipped the record, or the agent's hoisted persist threw. The
|
|
816
|
-
// followup pattern is intentionally narrow to the cloudflare-setup
|
|
817
|
-
// relay shape (not a general "any user persist") so a benign user
|
|
818
|
-
// typing in chat does not satisfy the followup. logSource=any so
|
|
819
|
-
// the rule sees both the script tee (in stream logs / server.log)
|
|
820
|
-
// and the [persist] line (server.log).
|
|
821
|
-
id: "cloudflare-setup-relay-not-acknowledged",
|
|
822
|
-
name: "Cloudflare-setup completed but the chat relay never acknowledged",
|
|
823
|
-
type: "absent-followup",
|
|
824
|
-
logSource: "any",
|
|
825
|
-
pattern: "\\[script:setup-tunnel\\] step=done",
|
|
826
|
-
followupPattern: "\\[persist\\] .*role=user.* Cloudflare setup completed \\(actionId:",
|
|
827
|
-
followupWindowMs: 6e4,
|
|
828
|
-
thresholdCount: 0,
|
|
829
|
-
thresholdWindowMinutes: 0,
|
|
830
|
-
suggestedAction: "[Task 867] cloudflare-setup completed but the post-action relay never reached the chat history. Check `[action-relay-queue] phase=enqueued` (queue write), `[action-completion-relay] phase=consumed` (boot-drain ran), and `[persist] role=user \u2026 Cloudflare setup completed` (graph write). One of those is missing; the matching grep recipe is in the Task 867 brief Observability section."
|
|
831
|
-
},
|
|
832
|
-
{
|
|
833
|
-
// Task 879 §C — onboarding turn-completion contract: any assistant
|
|
834
|
-
// turn that narrates a step transition with a non-question-terminated
|
|
835
|
-
// phrase ("Moving to step 9 — operator persona...", "Step 8 done.")
|
|
836
|
-
// must be followed within 30s by either a `render-component` call
|
|
837
|
-
// (the next step's UI surfaces) or `onboarding-complete-step`
|
|
838
|
-
// (deterministic step advance). The trigger pattern matches the
|
|
839
|
-
// `[onboarding-step-narration]` line written by stream-parser at the
|
|
840
|
-
// assistant text block emit site (only for non-question phrases).
|
|
841
|
-
// Operator's symptom in Task 879's evidence: "Moving to step 9..."
|
|
842
|
-
// followed by 4 minutes of silence until the operator nudged with
|
|
843
|
-
// "yeah, so?" — exact failure shape this rule catches.
|
|
844
|
-
id: "assistant-step-advance-deadend",
|
|
845
|
-
name: "Onboarding step-advance narration without follow-up tool call",
|
|
846
|
-
type: "absent-followup",
|
|
847
|
-
logSource: "session",
|
|
848
|
-
pattern: "\\[onboarding-step-narration\\]",
|
|
849
|
-
followupPattern: "\\[render-component\\]|\\[tool-use\\][^\\n]*name=mcp__admin__onboarding-complete-step",
|
|
850
|
-
followupWindowMs: 3e4,
|
|
851
|
-
thresholdCount: 0,
|
|
852
|
-
thresholdWindowMinutes: 0,
|
|
853
|
-
scope: "session",
|
|
854
|
-
suggestedAction: '[Task 879 \xA7C] An onboarding turn narrated a step transition (e.g. "Moving to step N", "Step N done.") but did not render a component or call `onboarding-complete-step` within 30s. The operator is left with a dead-end paragraph and no actionable surface. Check the `platform/plugins/admin/skills/onboarding/SKILL.md` Turn-completion contract section \u2014 the agent must end any step-advance turn with `render-component` or a `?`-terminated question, never bare prose.'
|
|
855
|
-
},
|
|
856
|
-
{
|
|
857
|
-
// Task 538: fires when a [spawn] line appears in a conversation's stream
|
|
858
|
-
// log but no subprocess-lifecycle marker follows within 10s. The three
|
|
859
|
-
// acceptable followups are Task 535's contract — at least one must be
|
|
860
|
-
// emitted immediately at every spawn site. Their absence means
|
|
861
|
-
// `teeProcStderrToStreamLog` regressed, the markers drifted, or the
|
|
862
|
-
// spawn site was added without wiring them up. Session scope so a single
|
|
863
|
-
// broken conversation fires exactly once, not N times for every spawn.
|
|
864
|
-
id: "subproc-tee-silent-spawn",
|
|
865
|
-
name: "Subprocess spawn without a stderr-tee lifecycle marker",
|
|
866
|
-
type: "absent-followup",
|
|
867
|
-
logSource: "system",
|
|
868
|
-
pattern: "\\[spawn\\] pid=\\d+",
|
|
869
|
-
followupPattern: "\\[subproc-stderr-tee-attached\\]|\\[subproc-debug-unavailable\\]|\\[subproc-stderr-skip\\]",
|
|
870
|
-
followupWindowMs: 1e4,
|
|
871
|
-
thresholdCount: 0,
|
|
872
|
-
thresholdWindowMinutes: 0,
|
|
873
|
-
scope: "session",
|
|
874
|
-
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)."
|
|
875
|
-
},
|
|
876
|
-
{
|
|
877
|
-
// Task 533: surface every Cloudflare-plugin refusal. The plugin emits
|
|
878
|
-
// exactly one [cloudflare:refuse] line per refusal with a structured
|
|
879
|
-
// reason field; any single occurrence on a previously-clean device
|
|
880
|
-
// means the bound Cloudflare account does not match the operator's
|
|
881
|
-
// intent (or the post-flight FQDN drifted) and the operator needs to
|
|
882
|
-
// act in the dashboard.
|
|
883
|
-
id: "cloudflare-refuse",
|
|
884
|
-
name: "Cloudflare plugin refusal",
|
|
885
|
-
type: "silent-catch",
|
|
886
|
-
logSource: "any",
|
|
887
|
-
pattern: "\\[cloudflare:refuse\\]|\\[cloudflare:post-flight-mismatch\\]",
|
|
888
|
-
thresholdCount: 0,
|
|
889
|
-
thresholdWindowMinutes: 0,
|
|
890
|
-
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`."
|
|
891
|
-
},
|
|
892
|
-
{
|
|
893
|
-
// Task 540: the single highest-priority refusal — surface it immediately
|
|
894
|
-
// and independently of the generic cloudflare-refuse rule so the admin
|
|
895
|
-
// agent sees it on the very next turn. This is the exact class that
|
|
896
|
-
// burned the operator for 8 days across 9+ sessions (Apr 11–18, 2026):
|
|
897
|
-
// tunnel running locally, dashboard serving the wrong account, nothing
|
|
898
|
-
// from the internet reaches the laptop, and no prior telemetry surfaced
|
|
899
|
-
// it in time for the agent to self-correct.
|
|
900
|
-
id: "cloudflare-bound-account-mismatch",
|
|
901
|
-
name: "Cloudflare bound account does not own the configured hostnames",
|
|
902
|
-
type: "silent-catch",
|
|
903
|
-
logSource: "any",
|
|
904
|
-
pattern: '"reason":"bound-account-does-not-own-hostname"',
|
|
905
|
-
thresholdCount: 0,
|
|
906
|
-
thresholdWindowMinutes: 0,
|
|
907
|
-
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`."
|
|
908
|
-
},
|
|
909
|
-
{
|
|
910
|
-
// Task 545: tunnel-login's terminal-failure class — cloudflared's
|
|
911
|
-
// login process died without writing cert.pem. Covers every reason
|
|
912
|
-
// the handler emits on the `failed` branch: either an unknown exit
|
|
913
|
-
// (`-without-cert`), an exit preceded by the courtesy browser-launch
|
|
914
|
-
// marker (`-with-marker`), auth URL never produced (`-timeout`), or
|
|
915
|
-
// crashed before producing it at all. Task 541's original pattern
|
|
916
|
-
// matched `reason=browser-launch-fetch-error` — Task 545 retired
|
|
917
|
-
// that reason because the marker alone is no longer terminal
|
|
918
|
-
// (cloudflared keeps its OAuth-callback loop alive after emitting
|
|
919
|
-
// it). Use this pattern for any new terminal reason the handler
|
|
920
|
-
// gains: extend the alternation rather than adding parallel rules.
|
|
921
|
-
id: "cloudflare-tunnel-login-failed",
|
|
922
|
-
name: "Cloudflare tunnel-login process terminated without writing cert",
|
|
923
|
-
type: "silent-catch",
|
|
924
|
-
logSource: "any",
|
|
925
|
-
pattern: "\\[cloudflare:tunnel-login:failed\\] reason=(login-process-exited-without-cert|login-process-exited-with-marker|auth-url-timeout|process-exited-before-auth-url)",
|
|
926
|
-
thresholdCount: 0,
|
|
927
|
-
thresholdWindowMinutes: 0,
|
|
928
|
-
suggestedAction: "The cloudflared login process died before cert.pem landed on disk. For `login-process-exited-*` reasons the tunnel-login tool detected the dead process on the next call and has already respawned it \u2014 relay the new sign-in URL and wait for the operator to authorize in the VNC browser. For `auth-url-timeout` / `process-exited-before-auth-url`, the tool's most recent call returned an error and did not respawn \u2014 call `tunnel-login` again to spawn a fresh attempt. Never open the Cloudflare dashboard in any other surface; the only auth path is the sign-in URL the tool produces."
|
|
929
|
-
},
|
|
930
|
-
{
|
|
931
|
-
// Task 545: non-terminal advisory. cloudflared's browser-launch
|
|
932
|
-
// subcommand failed (DISPLAY unreachable, xdg-open absent, etc.) but
|
|
933
|
-
// its OAuth-callback listener is still running — the login can still
|
|
934
|
-
// complete if a human opens the URL. This rule fires so the admin
|
|
935
|
-
// agent can relay "open the URL yourself" to the operator the moment
|
|
936
|
-
// the condition appears, rather than waiting for the operator to
|
|
937
|
-
// notice nothing is happening in their browser. Task 546 will
|
|
938
|
-
// obsolete the advisory by rendering auth URLs that auto-open in
|
|
939
|
-
// the VNC browser.
|
|
940
|
-
id: "cloudflare-tunnel-login-browser-launch-failed",
|
|
941
|
-
name: "cloudflared couldn't open the sign-in URL (login still live)",
|
|
942
|
-
type: "silent-catch",
|
|
943
|
-
logSource: "any",
|
|
944
|
-
pattern: "\\[cloudflare:tunnel-login:browser-launch-failed\\]",
|
|
945
|
-
thresholdCount: 0,
|
|
946
|
-
thresholdWindowMinutes: 0,
|
|
947
|
-
suggestedAction: "cloudflared's browser-launch courtesy failed on this laptop, but the sign-in URL is still live \u2014 the process is waiting for the OAuth callback. Tell the operator to open the sign-in URL in the VNC browser themselves and complete the authorization. The tunnel-login tool already includes the URL and the advisory in its most recent response; do not restart the login (that would invalidate the URL the operator is about to open)."
|
|
948
|
-
},
|
|
949
|
-
{
|
|
950
|
-
// Task 545: raw-log surface. cloudflared-login.log is now read by
|
|
951
|
-
// the review-detector's `cloudflared` source (see sources.ts). The
|
|
952
|
-
// literal "Failed to fetch resource" is emitted by cloudflared
|
|
953
|
-
// itself when its browser-launch subcommand can't reach a display.
|
|
954
|
-
// This rule catches the cloudflared-side event even if the MCP
|
|
955
|
-
// handler's classification drifts or the platform is restarting
|
|
956
|
-
// mid-login and the advisory log line is not yet written. Keeping
|
|
957
|
-
// this rule on a different source (log-line, not MCP stderr) makes
|
|
958
|
-
// the detection redundant in the "defence in depth" sense — if the
|
|
959
|
-
// MCP classification ever regresses, this still fires.
|
|
960
|
-
id: "cloudflared-login-browser-launch-failed-raw",
|
|
961
|
-
name: "cloudflared login \u2014 browser-launch fetch error in log",
|
|
962
|
-
type: "silent-catch",
|
|
963
|
-
logSource: "cloudflared",
|
|
964
|
-
pattern: "Failed to fetch resource",
|
|
965
|
-
thresholdCount: 0,
|
|
966
|
-
thresholdWindowMinutes: 0,
|
|
967
|
-
suggestedAction: "cloudflared-login.log shows the browser-launch courtesy failed. The sign-in URL from the last tunnel-login call is still live \u2014 the cloudflared process waits for the OAuth callback regardless of whether it could open the browser itself. Tell the operator to open the sign-in URL manually in the VNC browser on the device."
|
|
968
|
-
},
|
|
969
|
-
{
|
|
970
|
-
// Task 540: cloudflared.log is the one file most likely to carry the
|
|
971
|
-
// "tunnel is having real-world connectivity problems" signal — QUIC
|
|
972
|
-
// connection failures, connector drops, edge unreachability. Prior to
|
|
973
|
-
// this rule it was written but never read (the review-detector had no
|
|
974
|
-
// rule coverage for it). A single ERR line is worth surfacing; the
|
|
975
|
-
// tee'd output is typically noise-free.
|
|
976
|
-
// Task 862: setup-tunnel.sh emits `[script:setup-tunnel]
|
|
977
|
-
// step=onboarding-persist result=skipped reason=no-account-dir` via
|
|
978
|
-
// phase_line when ACCOUNT_DIR is unset. Pre-Task-862, the form-driven
|
|
979
|
-
// action runner threaded STREAM_LOG_PATH but not ACCOUNT_DIR, so the
|
|
980
|
-
// line landed in the agent's stream log (system source) and the user
|
|
981
|
-
// looped on currentStep=6 indefinitely.
|
|
982
|
-
// The agent-via-Bash path also threads STREAM_LOG_PATH; operator-SSH
|
|
983
|
-
// does not — that's the disambiguator. If this pattern reappears in
|
|
984
|
-
// a system log, a future invocation surface forgot to declare
|
|
985
|
-
// ACCOUNT_DIR. Fix at action-runner.ts WHITELIST['cloudflare-setup'],
|
|
986
|
-
// not in the script.
|
|
987
|
-
id: "cloudflare-setup-account-dir-missing",
|
|
988
|
-
name: "cloudflare-setup ran without ACCOUNT_DIR \u2014 onboarding step-7 will not persist",
|
|
989
|
-
type: "silent-catch",
|
|
990
|
-
logSource: "system",
|
|
991
|
-
pattern: "\\[script:setup-tunnel\\] step=onboarding-persist result=skipped reason=no-account-dir",
|
|
992
|
-
thresholdCount: 0,
|
|
993
|
-
thresholdWindowMinutes: 0,
|
|
994
|
-
suggestedAction: "An invocation surface for setup-tunnel.sh failed to declare ACCOUNT_DIR. Without it the script's step-7 persist block (Task 562) skips, leaving OnboardingState.currentStep=6 forever. Inspect platform/ui/server/lib/action-runner.ts WHITELIST['cloudflare-setup'].build (Task 862 thread) and platform/ui/app/lib/claude-agent/spawn-env.ts buildSpawnEnv (Task 562 thread) \u2014 confirm ACCOUNT_DIR is in the env map for whichever surface the action-id prefix indicates. Do NOT change setup-tunnel.sh; the skipped branch is correct for operator-SSH reconfigure flows."
|
|
995
|
-
},
|
|
996
|
-
{
|
|
997
|
-
id: "cloudflared-edge-errors",
|
|
998
|
-
name: "cloudflared edge connectivity errors",
|
|
999
|
-
type: "silent-catch",
|
|
1000
|
-
logSource: "cloudflared",
|
|
1001
|
-
pattern: "^\\S+ ERR (Failed to refresh protocol|no more connections active|Failed to dial a quic connection)",
|
|
1002
|
-
thresholdCount: 0,
|
|
1003
|
-
thresholdWindowMinutes: 0,
|
|
1004
|
-
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."
|
|
1005
|
-
},
|
|
1006
|
-
{
|
|
1007
|
-
// Task 543: fires when an agent turn contains an opposing-axis choice-fork
|
|
1008
|
-
// question ("Want me to X, or Y?"). Every occurrence is a violation of the
|
|
1009
|
-
// IDENTITY § Questions rule — Rule A (one-sided questions) catches them
|
|
1010
|
-
// all, and the sharper sub-class (Rule B — no menu when a tool returned a
|
|
1011
|
-
// deterministic signal) is isolated by Task 544's `preceded-by` tightening.
|
|
1012
|
-
// logSource is "any" so the rule catches violations on both the admin
|
|
1013
|
-
// stream (claude-agent-stream-*) and the public agent stream
|
|
1014
|
-
// (public-agent-stream-*); the regex is agent-phrasing-specific and has
|
|
1015
|
-
// not been observed in other log contexts. Session scope groups matches
|
|
1016
|
-
// by conversationId so a single offending turn fires exactly once.
|
|
1017
|
-
id: "agent-choice-fork",
|
|
1018
|
-
name: "Agent emitted a choice-fork question",
|
|
1019
|
-
type: "repeated-error",
|
|
1020
|
-
logSource: "any",
|
|
1021
|
-
pattern: "Want me to [^\\n]+, or [^\\n]+\\?",
|
|
1022
|
-
thresholdCount: 1,
|
|
1023
|
-
thresholdWindowMinutes: 60,
|
|
1024
|
-
scope: "session",
|
|
1025
|
-
suggestedAction: 'Agent emitted a choice-fork question ("Want me to X, or Y?") instead of a one-sided question or the prescribed action. Review Task 543 IDENTITY \xA7 Questions \u2014 the agent asked an opposing-axis question, or degraded a deterministic tool signal into a menu. The log sample shows the offending turn verbatim.'
|
|
1026
|
-
},
|
|
1027
|
-
{
|
|
1028
|
-
// Task 546: fires when the operator clicks a device-bound URL affordance
|
|
1029
|
-
// and the chat UI cannot drive the device browser — either CDP is
|
|
1030
|
-
// unreachable, the navigation timed out, or CDP returned an error. The
|
|
1031
|
-
// log line carries intent, hostname, and navigateResult so the admin
|
|
1032
|
-
// agent can name the affected flow and hostname verbatim on its next
|
|
1033
|
-
// turn. Every occurrence is worth surfacing (thresholdCount: 0) because
|
|
1034
|
-
// this is the exact class of silent failure Task 546 exists to close:
|
|
1035
|
-
// the operator clicked, nothing happened on the device, and if we don't
|
|
1036
|
-
// review the click telemetry the agent has no way to know the flow is
|
|
1037
|
-
// stuck.
|
|
1038
|
-
id: "device-url-click-failed",
|
|
1039
|
-
name: "Device-bound URL click failed to drive the VNC browser",
|
|
1040
|
-
type: "silent-catch",
|
|
1041
|
-
logSource: "server",
|
|
1042
|
-
// Enumerate the NavigateResult union explicitly rather than relying
|
|
1043
|
-
// on a (?!ok) negative lookahead anchored to a specific token order.
|
|
1044
|
-
// If a new member is added to the union in cdp-client.ts, this rule
|
|
1045
|
-
// must be updated in the same commit — the pattern is order-agnostic
|
|
1046
|
-
// (browser= and navigateResult= can appear in either order) and the
|
|
1047
|
-
// enumerated list compile-fails the source if it ever drifts from
|
|
1048
|
-
// the shared type in device-url-schema.ts.
|
|
1049
|
-
pattern: "\\[device-url:click\\][^\\n]*(?:browser=fallback|navigateResult=(?:timeout|cdp-unreachable|error))",
|
|
1050
|
-
thresholdCount: 0,
|
|
1051
|
-
thresholdWindowMinutes: 0,
|
|
1052
|
-
suggestedAction: "A device-bound URL click failed to drive Chromium on the device's VNC display. Identify the `intent` and `hostname` from the log line, then check the VNC surface: read `vnc-boot.log` and confirm Chromium on :99 is responding on CDP port 9222. If CDP is unreachable, the operator needs to restart the VNC stack; if CDP is reachable but navigation errored, the URL itself may be malformed upstream \u2014 grep the stream log for the originating `[device-url:render]` line."
|
|
1053
|
-
},
|
|
1054
|
-
{
|
|
1055
|
-
// Task 554: fires when an agent turn emits a synthesized localhost/127.0.0.1
|
|
1056
|
-
// `__remote-auth/setup` URL. The only legitimate source for that URL is
|
|
1057
|
-
// `remote-auth-status`, which emits it inside a `maxy-device-url` affordance
|
|
1058
|
-
// block with the device's real hostname. A synthesized localhost variant is
|
|
1059
|
-
// the exact failure mode Task 554 closed: the removed "direct the user to
|
|
1060
|
-
// /__remote-auth/setup" clause in the password-setter's tool description
|
|
1061
|
-
// invited URL synthesis from a partial path. Session scope groups matches
|
|
1062
|
-
// by conversationId so a single offending turn fires exactly once, even if
|
|
1063
|
-
// the URL appears multiple times in the streamed response.
|
|
1064
|
-
id: "invented-remote-auth-url",
|
|
1065
|
-
name: "Invented localhost/127.0.0.1 remote-auth setup URL",
|
|
1066
|
-
type: "silent-catch",
|
|
1067
|
-
logSource: "any",
|
|
1068
|
-
pattern: "http(s)?://(localhost|127\\.0\\.0\\.1)[:\\w/.-]*__remote-auth/setup",
|
|
1069
|
-
thresholdCount: 0,
|
|
1070
|
-
thresholdWindowMinutes: 0,
|
|
1071
|
-
scope: "session",
|
|
1072
|
-
suggestedAction: "[Task 554 regression] An assistant turn emitted a synthesized localhost/127.0.0.1 remote-auth setup URL. The only authority for the browser-setup URL is `remote-auth-status`, which emits a `maxy-device-url` affordance block with the device's real hostname. Review the offending turn, confirm the `remote-auth-set-password` tool description and onboarding SKILL.md step 3 still lack URL-synthesis language, and patch whichever prose surface invited the synthesis."
|
|
1073
|
-
},
|
|
1074
|
-
{
|
|
1075
|
-
// Task 553: fires when `anthropic-setup` auto-resets a revoked API key.
|
|
1076
|
-
// The auto-reset is silent to the agent (the tool falls through to
|
|
1077
|
-
// awaiting_signin in the same call), but it is operator-visible — the
|
|
1078
|
-
// user's previously-stored key was just deleted because Anthropic
|
|
1079
|
-
// rejected it. Surfacing the event lets the admin agent explain on the
|
|
1080
|
-
// next turn why the user is being asked to sign in again instead of
|
|
1081
|
-
// continuing normally. The matching string is the exact stable prefix
|
|
1082
|
-
// emitted by the state machine immediately before `deleteKey()` is
|
|
1083
|
-
// invoked.
|
|
1084
|
-
id: "anthropic-setup-auth-error-auto-reset",
|
|
1085
|
-
name: "Anthropic API key auto-reset on auth_error",
|
|
1086
|
-
type: "silent-catch",
|
|
1087
|
-
logSource: "any",
|
|
1088
|
-
pattern: "\\[anthropic-setup\\] auth_error",
|
|
1089
|
-
thresholdCount: 0,
|
|
1090
|
-
thresholdWindowMinutes: 0,
|
|
1091
|
-
suggestedAction: "The stored Anthropic API key was rejected by console.anthropic.com (invalid, revoked, or expired) and `anthropic-setup` auto-deleted it. On the next operator interaction, explain that the key was cleared and walk them through sign-in again via the onboarding skill \u2014 the tool already returned `awaiting_signin` with the correct `browser_evaluate` action on the same call."
|
|
1092
|
-
},
|
|
1093
|
-
{
|
|
1094
|
-
// Task 562: setup-tunnel.sh persists step-7 completion to a filesystem
|
|
1095
|
-
// flag before arming the service restart. The flag is consumed by the
|
|
1096
|
-
// next session's `loadOnboardingStep` and `getOnboardingState` calls,
|
|
1097
|
-
// so any runtime failure to write it means the next admin session
|
|
1098
|
-
// will re-ask the Cloudflare question the user just answered. Every
|
|
1099
|
-
// occurrence is loud-surface-worthy: the cause is either a permission
|
|
1100
|
-
// issue on the onboarding directory or a filesystem problem on the
|
|
1101
|
-
// device, and the recovery is deterministic (the operator can re-run
|
|
1102
|
-
// setup-tunnel.sh or call `onboarding-complete-step` explicitly).
|
|
1103
|
-
id: "setup-tunnel-onboarding-persist-failed",
|
|
1104
|
-
name: "setup-tunnel.sh failed to persist step-7 completion",
|
|
1105
|
-
type: "silent-catch",
|
|
1106
|
-
logSource: "any",
|
|
1107
|
-
pattern: "\\[script:setup-tunnel\\] step=onboarding-persist result=error",
|
|
1108
|
-
thresholdCount: 0,
|
|
1109
|
-
thresholdWindowMinutes: 0,
|
|
1110
|
-
suggestedAction: "[Task 562] setup-tunnel.sh could not persist step-7 completion before arming the service restart. Read the `reason` field on the failing phase line: `fs-flag-dir-failed` means `${ACCOUNT_DIR}/onboarding/` is not writable (chmod issue or disk full); `fs-flag-write-failed` means the flag file itself could not be written. Without the flag, the next admin session will re-ask the Cloudflare question the user just answered. Recovery: fix the permission/disk issue, then either re-run `~/setup-tunnel.sh` (the flag-write is idempotent) or call `onboarding-complete-step` with step 7 from the admin chat to mark step 7 complete explicitly."
|
|
1111
|
-
},
|
|
1112
|
-
{
|
|
1113
|
-
// Task 570: every `[llm-call] adminModel resolution FAILED:` line
|
|
1114
|
-
// is a workflow LLM step that could not resolve a model because
|
|
1115
|
-
// readAdminModel returned null. The stderr line carries `reason=`
|
|
1116
|
-
// (enoent/eacces/fs_error/parse_error/field_missing/
|
|
1117
|
-
// field_wrong_type/field_empty) so the admin agent can act on the
|
|
1118
|
-
// specific failure mode rather than reading the generic persisted
|
|
1119
|
-
// error and misdiagnosing. Every occurrence is worth surfacing —
|
|
1120
|
-
// the workflow already failed when this line was emitted.
|
|
1121
|
-
id: "workflow-admin-model-resolution-failed",
|
|
1122
|
-
name: "Workflow readAdminModel returned null (reason code in log)",
|
|
1123
|
-
type: "silent-catch",
|
|
1124
|
-
logSource: "any",
|
|
1125
|
-
pattern: "\\[llm-call\\] adminModel resolution FAILED:",
|
|
1126
|
-
thresholdCount: 0,
|
|
1127
|
-
thresholdWindowMinutes: 0,
|
|
1128
|
-
suggestedAction: "A workflow LLM step could not resolve the account's adminModel. Read the log line's `reason=` field: `enoent` means the account.json path is wrong or the account is unprovisioned \u2014 inspect `path=` and confirm it exists. `eacces` means permission drift \u2014 check file ownership. `parse_error` means the file is malformed, likely mid-write or truncated \u2014 read the file and repair. `field_missing` / `field_wrong_type` / `field_empty` mean the `adminModel` field needs to be set to a valid model ID. Never edit the persisted WorkflowRun.error string; fix the underlying file or permissions and re-run the workflow."
|
|
1129
|
-
},
|
|
1130
|
-
{
|
|
1131
|
-
// Task 561: fires when an admin-agent Bash tool call installs
|
|
1132
|
-
// `bind9-dnsutils` at runtime. Post-Task-561 the Maxy installer
|
|
1133
|
-
// (platform/scripts/setup.sh) provisions the package on every fresh
|
|
1134
|
-
// and upgrade install, so `dig` is always in PATH before
|
|
1135
|
-
// setup-tunnel.sh runs. An admin apt-install of bind9-dnsutils is
|
|
1136
|
-
// therefore evidence the installer regressed (or the host is on a
|
|
1137
|
-
// pre-Task-561 image); in either case the fix is to re-run the
|
|
1138
|
-
// installer, not to patch apt-state from chat. Session scope so a
|
|
1139
|
-
// single offending turn fires exactly once.
|
|
1140
|
-
id: "admin-agent-apt-bind9-install",
|
|
1141
|
-
name: "Admin agent installed bind9-dnsutils at runtime (installer regression)",
|
|
1142
|
-
type: "repeated-error",
|
|
1143
|
-
logSource: "any",
|
|
1144
|
-
pattern: "\\[tool-use\\][^\\n]*name=Bash[^\\n]*apt-get install[^\\n]*bind9-dnsutils",
|
|
1145
|
-
thresholdCount: 1,
|
|
1146
|
-
thresholdWindowMinutes: 60,
|
|
1147
|
-
scope: "session",
|
|
1148
|
-
suggestedAction: "[Task 561 regression] The admin agent ran `apt-get install bind9-dnsutils` \u2014 the exact workaround Task 561 eliminated. The Maxy installer provisions `bind9-dnsutils` in `platform/scripts/setup.sh` so `dig` is always in PATH before `setup-tunnel.sh` runs. Do not patch apt-state from chat \u2014 re-run the installer (`npx -y @rubytech/create-maxy`) and verify the installer's apt-get line still includes `bind9-dnsutils`. If the package is present in setup.sh but missing on the device, the installer did not re-run step 1 on the current image."
|
|
1149
|
-
}
|
|
1150
|
-
];
|
|
1151
|
-
}
|
|
1152
|
-
function rulesFilePath(configDir2) {
|
|
1153
|
-
return resolve(configDir2, "review-rules.json");
|
|
1154
|
-
}
|
|
1155
|
-
function ensureRulesFile(configDir2) {
|
|
1156
|
-
const path2 = rulesFilePath(configDir2);
|
|
1157
|
-
if (existsSync2(path2)) return { created: false, path: path2 };
|
|
1158
|
-
mkdirSync(dirname(path2), { recursive: true });
|
|
1159
|
-
const body = {
|
|
1160
|
-
scanIntervalMs: DEFAULT_SCAN_INTERVAL_MS,
|
|
1161
|
-
rules: defaultRules()
|
|
1162
|
-
};
|
|
1163
|
-
atomicWriteJson(path2, body);
|
|
1164
|
-
return { created: true, path: path2 };
|
|
1165
|
-
}
|
|
1166
|
-
function loadRules(configDir2) {
|
|
1167
|
-
const path2 = rulesFilePath(configDir2);
|
|
1168
|
-
if (!existsSync2(path2)) {
|
|
1169
|
-
throw new Error(`rules file missing at ${path2}`);
|
|
1170
|
-
}
|
|
1171
|
-
const raw = readFileSync(path2, "utf-8");
|
|
1172
|
-
let parsed;
|
|
1173
|
-
try {
|
|
1174
|
-
parsed = JSON.parse(raw);
|
|
1175
|
-
} catch (err) {
|
|
1176
|
-
throw new Error(`rules file ${path2} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
1177
|
-
}
|
|
1178
|
-
return validateRulesFile(parsed, path2);
|
|
1179
|
-
}
|
|
1180
|
-
function rulesFileMtime(configDir2) {
|
|
1181
|
-
const path2 = rulesFilePath(configDir2);
|
|
1182
|
-
try {
|
|
1183
|
-
return statSync2(path2).mtimeMs;
|
|
1184
|
-
} catch {
|
|
1185
|
-
return null;
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
function saveRules(configDir2, file) {
|
|
1189
|
-
validateRulesFile(file, rulesFilePath(configDir2));
|
|
1190
|
-
atomicWriteJson(rulesFilePath(configDir2), file);
|
|
1191
|
-
}
|
|
1192
|
-
function atomicWriteJson(path2, body) {
|
|
1193
|
-
const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
|
|
1194
|
-
writeFileSync(tmp, JSON.stringify(body, null, 2) + "\n", "utf-8");
|
|
1195
|
-
renameSync(tmp, path2);
|
|
1196
|
-
}
|
|
1197
|
-
function validateRulesFile(input, sourceLabel) {
|
|
1198
|
-
if (!input || typeof input !== "object") {
|
|
1199
|
-
throw new Error(`${sourceLabel}: top-level must be an object`);
|
|
1200
|
-
}
|
|
1201
|
-
const obj = input;
|
|
1202
|
-
const scanIntervalMs = obj.scanIntervalMs;
|
|
1203
|
-
if (typeof scanIntervalMs !== "number" || scanIntervalMs < 500 || scanIntervalMs > 3e5) {
|
|
1204
|
-
throw new Error(`${sourceLabel}: scanIntervalMs must be a number between 500 and 300000`);
|
|
1205
|
-
}
|
|
1206
|
-
const rulesRaw = obj.rules;
|
|
1207
|
-
if (!Array.isArray(rulesRaw)) {
|
|
1208
|
-
throw new Error(`${sourceLabel}: rules must be an array`);
|
|
1209
|
-
}
|
|
1210
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1211
|
-
const rules = rulesRaw.map((r, i) => validateRule(r, `${sourceLabel}#rules[${i}]`, ids));
|
|
1212
|
-
return { scanIntervalMs, rules };
|
|
1213
|
-
}
|
|
1214
|
-
function addMissingDefaultRules(rulesFile) {
|
|
1215
|
-
const existingIds = new Set(rulesFile.rules.map((r) => r.id));
|
|
1216
|
-
const defaults = defaultRules();
|
|
1217
|
-
let mutated = false;
|
|
1218
|
-
for (const rule of defaults) {
|
|
1219
|
-
if (!existingIds.has(rule.id)) {
|
|
1220
|
-
rulesFile.rules.push(rule);
|
|
1221
|
-
mutated = true;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
return mutated;
|
|
1225
|
-
}
|
|
1226
|
-
function migrateRateLimitPattern(rulesFile) {
|
|
1227
|
-
const rule = rulesFile.rules.find((r) => r.id === "http-rate-limit-429");
|
|
1228
|
-
if (!rule) return false;
|
|
1229
|
-
if (rule.pattern !== RATE_LIMIT_PATTERN_V1) return false;
|
|
1230
|
-
rule.pattern = RATE_LIMIT_PATTERN;
|
|
1231
|
-
return true;
|
|
1232
|
-
}
|
|
1233
|
-
function validateRule(input, label, seenIds) {
|
|
1234
|
-
if (!input || typeof input !== "object") {
|
|
1235
|
-
throw new Error(`${label}: rule must be an object`);
|
|
1236
|
-
}
|
|
1237
|
-
const r = input;
|
|
1238
|
-
const id = r.id;
|
|
1239
|
-
if (typeof id !== "string" || id.length === 0) {
|
|
1240
|
-
throw new Error(`${label}: id must be a non-empty string`);
|
|
1241
|
-
}
|
|
1242
|
-
if (seenIds.has(id)) {
|
|
1243
|
-
throw new Error(`${label}: duplicate rule id "${id}"`);
|
|
1244
|
-
}
|
|
1245
|
-
seenIds.add(id);
|
|
1246
|
-
const name = r.name;
|
|
1247
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
1248
|
-
throw new Error(`${label}: name must be a non-empty string`);
|
|
1249
|
-
}
|
|
1250
|
-
const type = r.type;
|
|
1251
|
-
if (typeof type !== "string" || !VALID_TYPES.has(type)) {
|
|
1252
|
-
throw new Error(`${label}: type must be one of ${[...VALID_TYPES].join(", ")}`);
|
|
1253
|
-
}
|
|
1254
|
-
const logSource = r.logSource;
|
|
1255
|
-
if (typeof logSource !== "string" || !VALID_SOURCES.has(logSource)) {
|
|
1256
|
-
throw new Error(`${label}: logSource must be one of ${[...VALID_SOURCES].join(", ")}`);
|
|
1257
|
-
}
|
|
1258
|
-
const pattern = r.pattern;
|
|
1259
|
-
if (typeof pattern !== "string") {
|
|
1260
|
-
throw new Error(`${label}: pattern must be a string (may be empty for stale-log/file-write-storm)`);
|
|
1261
|
-
}
|
|
1262
|
-
if (pattern.length > 0) {
|
|
1263
|
-
try {
|
|
1264
|
-
new RegExp(pattern);
|
|
1265
|
-
} catch (err) {
|
|
1266
|
-
throw new Error(`${label}: pattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
const thresholdCount = r.thresholdCount;
|
|
1270
|
-
if (typeof thresholdCount !== "number" || thresholdCount < 0) {
|
|
1271
|
-
throw new Error(`${label}: thresholdCount must be a non-negative number`);
|
|
1272
|
-
}
|
|
1273
|
-
const thresholdWindowMinutes = r.thresholdWindowMinutes;
|
|
1274
|
-
if (typeof thresholdWindowMinutes !== "number" || thresholdWindowMinutes < 0) {
|
|
1275
|
-
throw new Error(`${label}: thresholdWindowMinutes must be a non-negative number`);
|
|
1276
|
-
}
|
|
1277
|
-
const suggestedAction = r.suggestedAction;
|
|
1278
|
-
if (typeof suggestedAction !== "string" || suggestedAction.length === 0) {
|
|
1279
|
-
throw new Error(`${label}: suggestedAction must be a non-empty string`);
|
|
1280
|
-
}
|
|
1281
|
-
const rule = {
|
|
1282
|
-
id,
|
|
1283
|
-
name,
|
|
1284
|
-
type,
|
|
1285
|
-
logSource,
|
|
1286
|
-
pattern,
|
|
1287
|
-
thresholdCount,
|
|
1288
|
-
thresholdWindowMinutes,
|
|
1289
|
-
suggestedAction
|
|
1290
|
-
};
|
|
1291
|
-
if (typeof r.watchPath === "string") rule.watchPath = r.watchPath;
|
|
1292
|
-
if (typeof r.staleHours === "number") rule.staleHours = r.staleHours;
|
|
1293
|
-
if (typeof r.followupPattern === "string") {
|
|
1294
|
-
if (r.followupPattern.length > 0) {
|
|
1295
|
-
try {
|
|
1296
|
-
new RegExp(r.followupPattern);
|
|
1297
|
-
} catch (err) {
|
|
1298
|
-
throw new Error(`${label}: followupPattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
rule.followupPattern = r.followupPattern;
|
|
1302
|
-
}
|
|
1303
|
-
if (typeof r.followupWindowMs === "number") rule.followupWindowMs = r.followupWindowMs;
|
|
1304
|
-
if (typeof r.suppressedUntil === "string") rule.suppressedUntil = r.suppressedUntil;
|
|
1305
|
-
if (r.scope !== void 0) {
|
|
1306
|
-
if (typeof r.scope !== "string" || !VALID_SCOPES.has(r.scope)) {
|
|
1307
|
-
throw new Error(`${label}: scope must be one of ${[...VALID_SCOPES].join(", ")}`);
|
|
1308
|
-
}
|
|
1309
|
-
rule.scope = r.scope;
|
|
1310
|
-
}
|
|
1311
|
-
if (rule.type === "file-write-storm" || rule.type === "stale-log") {
|
|
1312
|
-
if (!rule.watchPath) {
|
|
1313
|
-
throw new Error(`${label}: ${rule.type} rules require watchPath`);
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
if (rule.type === "stale-log") {
|
|
1317
|
-
if (typeof rule.staleHours !== "number" || rule.staleHours <= 0) {
|
|
1318
|
-
throw new Error(`${label}: stale-log rules require a positive staleHours`);
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
if (rule.type === "absent-followup") {
|
|
1322
|
-
if (rule.pattern.length === 0) {
|
|
1323
|
-
throw new Error(`${label}: absent-followup rules require a non-empty pattern`);
|
|
1324
|
-
}
|
|
1325
|
-
if (typeof rule.followupPattern !== "string" || rule.followupPattern.length === 0) {
|
|
1326
|
-
throw new Error(`${label}: absent-followup rules require a non-empty followupPattern`);
|
|
1327
|
-
}
|
|
1328
|
-
if (typeof rule.followupWindowMs !== "number" || rule.followupWindowMs <= 0 || rule.followupWindowMs > MAX_FOLLOWUP_WINDOW_MS) {
|
|
1329
|
-
throw new Error(`${label}: absent-followup rules require followupWindowMs in (0, ${MAX_FOLLOWUP_WINDOW_MS}]`);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
return rule;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
// app/lib/review-detector/sources.ts
|
|
1336
|
-
import { existsSync as existsSync3, readdirSync, statSync as statSync3, writeFileSync as writeFileSync2, renameSync as renameSync2, mkdirSync as mkdirSync2, openSync, readSync, closeSync, readFileSync as readFileSync2 } from "fs";
|
|
1337
|
-
import { resolve as resolve2, join as join2, basename, dirname as dirname2 } from "path";
|
|
1338
|
-
function tailStatePath(configDir2) {
|
|
1339
|
-
return resolve2(configDir2, "review-state.json");
|
|
1340
|
-
}
|
|
1341
|
-
function loadTailState(configDir2) {
|
|
1342
|
-
const path2 = tailStatePath(configDir2);
|
|
1343
|
-
if (!existsSync3(path2)) return {};
|
|
1344
|
-
try {
|
|
1345
|
-
const raw = readFileSync2(path2, "utf-8");
|
|
1346
|
-
const parsed = JSON.parse(raw);
|
|
1347
|
-
if (!parsed || typeof parsed !== "object") return {};
|
|
1348
|
-
const clean = {};
|
|
1349
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
1350
|
-
const entry = value;
|
|
1351
|
-
if (entry && typeof entry.offset === "number" && typeof entry.size === "number" && typeof entry.inode === "number") {
|
|
1352
|
-
clean[key] = entry;
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
return clean;
|
|
1356
|
-
} catch (err) {
|
|
1357
|
-
console.error(`[review] tail state corrupt at ${path2}, starting fresh: ${err instanceof Error ? err.message : String(err)}`);
|
|
1358
|
-
return {};
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
function saveTailState(configDir2, state) {
|
|
1362
|
-
const path2 = tailStatePath(configDir2);
|
|
1363
|
-
mkdirSync2(dirname2(path2), { recursive: true });
|
|
1364
|
-
const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
|
|
1365
|
-
writeFileSync2(tmp, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
1366
|
-
renameSync2(tmp, path2);
|
|
1367
|
-
}
|
|
1368
|
-
function discoverSourceFiles(configDir2, accountLogDir2, logicalSource) {
|
|
1369
|
-
if (logicalSource === "server") {
|
|
1370
|
-
const p = resolve2(configDir2, "logs", "server.log");
|
|
1371
|
-
return existsSync3(p) ? [{ logicalSource: "server", filepath: p }] : [];
|
|
1372
|
-
}
|
|
1373
|
-
if (logicalSource === "vnc") {
|
|
1374
|
-
const p = resolve2(configDir2, "logs", "vnc-boot.log");
|
|
1375
|
-
return existsSync3(p) ? [{ logicalSource: "vnc", filepath: p }] : [];
|
|
1376
|
-
}
|
|
1377
|
-
if (logicalSource === "cloudflared") {
|
|
1378
|
-
const files2 = [];
|
|
1379
|
-
const daemon = resolve2(configDir2, "logs", "cloudflared.log");
|
|
1380
|
-
if (existsSync3(daemon)) files2.push({ logicalSource: "cloudflared", filepath: daemon });
|
|
1381
|
-
const login = resolve2(configDir2, "logs", "cloudflared-login.log");
|
|
1382
|
-
if (existsSync3(login)) files2.push({ logicalSource: "cloudflared", filepath: login });
|
|
1383
|
-
return files2;
|
|
1384
|
-
}
|
|
1385
|
-
const prefix = {
|
|
1386
|
-
system: "claude-agent-stream-",
|
|
1387
|
-
error: "claude-agent-stderr-",
|
|
1388
|
-
session: "sse-events-",
|
|
1389
|
-
public: "public-agent-stream-",
|
|
1390
|
-
mcp: "mcp-"
|
|
1391
|
-
}[logicalSource];
|
|
1392
|
-
if (!existsSync3(accountLogDir2)) return [];
|
|
1393
|
-
const files = [];
|
|
1394
|
-
let scanned = 0;
|
|
1395
|
-
let skippedPrefixMismatch = 0;
|
|
1396
|
-
let skippedNotLog = 0;
|
|
1397
|
-
for (const entry of readdirSync(accountLogDir2)) {
|
|
1398
|
-
scanned += 1;
|
|
1399
|
-
const matchesPrefix = entry.startsWith(prefix);
|
|
1400
|
-
const isLog = entry.endsWith(".log");
|
|
1401
|
-
if (matchesPrefix && isLog) {
|
|
1402
|
-
files.push({ logicalSource, filepath: join2(accountLogDir2, entry) });
|
|
1403
|
-
} else if (!matchesPrefix) {
|
|
1404
|
-
skippedPrefixMismatch += 1;
|
|
1405
|
-
} else {
|
|
1406
|
-
skippedNotLog += 1;
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
files.sort((a, b) => {
|
|
1410
|
-
try {
|
|
1411
|
-
return statSync3(b.filepath).mtimeMs - statSync3(a.filepath).mtimeMs;
|
|
1412
|
-
} catch {
|
|
1413
|
-
return a.filepath.localeCompare(b.filepath);
|
|
1414
|
-
}
|
|
1415
|
-
});
|
|
1416
|
-
if (skippedPrefixMismatch > 0 || skippedNotLog > 0) {
|
|
1417
|
-
console.error(`[review-scan-skip] dir=${accountLogDir2} source=${logicalSource} prefix=${prefix} scanned=${scanned} matched=${files.length} skipped_prefix=${skippedPrefixMismatch} skipped_non_log=${skippedNotLog}`);
|
|
1418
|
-
}
|
|
1419
|
-
return files;
|
|
1420
|
-
}
|
|
1421
|
-
function discoverAllSources(configDir2, accountLogDir2) {
|
|
1422
|
-
return [
|
|
1423
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "server"),
|
|
1424
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "vnc"),
|
|
1425
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "system"),
|
|
1426
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "error"),
|
|
1427
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "session"),
|
|
1428
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "public"),
|
|
1429
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "mcp"),
|
|
1430
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "cloudflared")
|
|
1431
|
-
];
|
|
1432
|
-
}
|
|
1433
|
-
function readNewLines(filepath, prev) {
|
|
1434
|
-
if (!existsSync3(filepath)) return null;
|
|
1435
|
-
const stat7 = statSync3(filepath);
|
|
1436
|
-
const size = stat7.size;
|
|
1437
|
-
const inode = stat7.ino;
|
|
1438
|
-
let startOffset = 0;
|
|
1439
|
-
let rotated = false;
|
|
1440
|
-
let truncated = false;
|
|
1441
|
-
if (prev) {
|
|
1442
|
-
if (prev.inode !== inode) {
|
|
1443
|
-
rotated = true;
|
|
1444
|
-
startOffset = 0;
|
|
1445
|
-
} else if (size < prev.offset) {
|
|
1446
|
-
truncated = true;
|
|
1447
|
-
startOffset = 0;
|
|
1448
|
-
} else {
|
|
1449
|
-
startOffset = prev.offset;
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
if (startOffset >= size) {
|
|
1453
|
-
return {
|
|
1454
|
-
lines: [],
|
|
1455
|
-
entry: { offset: size, size, inode },
|
|
1456
|
-
rotated,
|
|
1457
|
-
truncated
|
|
1458
|
-
};
|
|
1459
|
-
}
|
|
1460
|
-
const fd = openSync(filepath, "r");
|
|
1461
|
-
try {
|
|
1462
|
-
const bufSize = Math.max(0, size - startOffset);
|
|
1463
|
-
const buf = Buffer.alloc(bufSize);
|
|
1464
|
-
if (bufSize > 0) {
|
|
1465
|
-
readSync(fd, buf, 0, bufSize, startOffset);
|
|
1466
|
-
}
|
|
1467
|
-
const text = buf.toString("utf-8");
|
|
1468
|
-
const lines = text.length > 0 ? text.split("\n") : [];
|
|
1469
|
-
let newOffset = size;
|
|
1470
|
-
if (lines.length > 0 && !text.endsWith("\n")) {
|
|
1471
|
-
const partial = lines.pop();
|
|
1472
|
-
newOffset = size - Buffer.byteLength(partial, "utf-8");
|
|
1473
|
-
} else if (lines.length > 0 && text.endsWith("\n")) {
|
|
1474
|
-
if (lines[lines.length - 1] === "") lines.pop();
|
|
1475
|
-
}
|
|
1476
|
-
return {
|
|
1477
|
-
lines,
|
|
1478
|
-
entry: { offset: newOffset, size, inode },
|
|
1479
|
-
rotated,
|
|
1480
|
-
truncated
|
|
1481
|
-
};
|
|
1482
|
-
} finally {
|
|
1483
|
-
closeSync(fd);
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
function countRecentWrites(dir, sinceMs) {
|
|
1487
|
-
if (!existsSync3(dir)) return 0;
|
|
1488
|
-
let count = 0;
|
|
1489
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1490
|
-
if (!entry.isFile()) continue;
|
|
1491
|
-
try {
|
|
1492
|
-
const st = statSync3(join2(dir, entry.name));
|
|
1493
|
-
if (st.mtimeMs >= sinceMs) count += 1;
|
|
1494
|
-
} catch {
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
return count;
|
|
1498
|
-
}
|
|
1499
|
-
function fileLastWriteMs(path2) {
|
|
1500
|
-
if (!existsSync3(path2)) return null;
|
|
1501
|
-
try {
|
|
1502
|
-
return statSync3(path2).mtimeMs;
|
|
1503
|
-
} catch {
|
|
1504
|
-
return null;
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
function accountLogDir(accountDir) {
|
|
1508
|
-
return resolve2(accountDir, "logs");
|
|
1509
|
-
}
|
|
1510
|
-
function sourceKey(file) {
|
|
1511
|
-
return `${file.logicalSource}:${basename(file.filepath)}`;
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
// app/lib/review-detector/writer.ts
|
|
1515
|
-
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, statSync as statSync4 } from "fs";
|
|
1516
|
-
import { resolve as resolve3, dirname as dirname3 } from "path";
|
|
1517
|
-
import { randomUUID } from "crypto";
|
|
1518
|
-
function reviewLogPath(configDir2) {
|
|
1519
|
-
return resolve3(configDir2, "logs", "review.log");
|
|
1520
|
-
}
|
|
1521
|
-
function pendingAlertsPath(configDir2) {
|
|
1522
|
-
return resolve3(configDir2, "review-pending-alerts.jsonl");
|
|
1523
|
-
}
|
|
1524
|
-
function reviewLog(configDir2, event) {
|
|
1525
|
-
const path2 = reviewLogPath(configDir2);
|
|
1526
|
-
try {
|
|
1527
|
-
mkdirSync3(dirname3(path2), { recursive: true });
|
|
1528
|
-
const line = `${new Date(
|
|
1529
|
-
typeof event.ts === "number" ? event.ts : Date.now()
|
|
1530
|
-
).toISOString()} [review] ${JSON.stringify(event)}
|
|
1531
|
-
`;
|
|
1532
|
-
appendFileSync(path2, line, "utf-8");
|
|
1533
|
-
} catch (err) {
|
|
1534
|
-
console.error(`[review] failed to write review log at ${path2}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
async function ensureReviewAlertIndex() {
|
|
1538
|
-
const session = getSession();
|
|
1539
|
-
try {
|
|
1540
|
-
await session.run(
|
|
1541
|
-
`CREATE INDEX review_alert_lookup IF NOT EXISTS
|
|
1542
|
-
FOR (a:ReviewAlert)
|
|
1543
|
-
ON (a.accountId, a.resolvedAt, a.lastMatchAt)`
|
|
1544
|
-
);
|
|
1545
|
-
} finally {
|
|
1546
|
-
await session.close();
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
async function ensureReviewDigestSchedule(accountId) {
|
|
1550
|
-
const eventId = `review-digest-${accountId}`;
|
|
1551
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1552
|
-
const next = /* @__PURE__ */ new Date();
|
|
1553
|
-
if (next.getHours() >= 8) {
|
|
1554
|
-
next.setDate(next.getDate() + 1);
|
|
1555
|
-
}
|
|
1556
|
-
next.setHours(8, 0, 0, 0);
|
|
1557
|
-
const nextRun = next.toISOString();
|
|
1558
|
-
const session = getSession();
|
|
1559
|
-
try {
|
|
1560
|
-
await session.run(
|
|
1561
|
-
`MERGE (e:Event { eventId: $eventId })
|
|
1562
|
-
ON CREATE SET
|
|
1563
|
-
e.accountId = $accountId,
|
|
1564
|
-
e.name = 'Daily review digest',
|
|
1565
|
-
e.description = 'Task 385 \u2014 composes the review cadence digest from the last 24 hours of review.log and active ReviewAlert records. Scheduled via check-due-events.',
|
|
1566
|
-
e.startDate = $now,
|
|
1567
|
-
e.eventStatus = 'scheduled',
|
|
1568
|
-
e.recurrence = '0 8 * * *',
|
|
1569
|
-
e.nextRun = datetime($nextRun),
|
|
1570
|
-
e.sourcePlugin = 'admin',
|
|
1571
|
-
e.actionPlugin = 'admin',
|
|
1572
|
-
e.actionTool = 'review-digest-compose',
|
|
1573
|
-
e.actionArgs = '{}',
|
|
1574
|
-
e.createdAt = $now,
|
|
1575
|
-
e.updatedAt = $now
|
|
1576
|
-
ON MATCH SET
|
|
1577
|
-
e.actionPlugin = 'admin',
|
|
1578
|
-
e.actionTool = 'review-digest-compose',
|
|
1579
|
-
e.updatedAt = $now`,
|
|
1580
|
-
{ eventId, accountId, now, nextRun }
|
|
1581
|
-
);
|
|
1582
|
-
} finally {
|
|
1583
|
-
await session.close();
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
async function countActiveReviewAlerts(accountId) {
|
|
1587
|
-
const session = getSession();
|
|
1588
|
-
try {
|
|
1589
|
-
const result = await session.run(
|
|
1590
|
-
`MATCH (a:ReviewAlert {accountId: $accountId})
|
|
1591
|
-
WHERE a.resolvedAt IS NULL
|
|
1592
|
-
AND (a.suppressedUntil IS NULL OR a.suppressedUntil < datetime())
|
|
1593
|
-
RETURN count(a) AS n`,
|
|
1594
|
-
{ accountId }
|
|
1595
|
-
);
|
|
1596
|
-
const n = result.records[0]?.get("n");
|
|
1597
|
-
if (typeof n === "number") return n;
|
|
1598
|
-
if (n && typeof n.toNumber === "function") {
|
|
1599
|
-
return n.toNumber();
|
|
1600
|
-
}
|
|
1601
|
-
return 0;
|
|
1602
|
-
} finally {
|
|
1603
|
-
await session.close();
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
async function upsertReviewAlert(accountId, match) {
|
|
1607
|
-
const session = getSession();
|
|
1608
|
-
try {
|
|
1609
|
-
await session.run(
|
|
1610
|
-
`MERGE (a:ReviewAlert { ruleId: $ruleId, accountId: $accountId })
|
|
1611
|
-
ON CREATE SET
|
|
1612
|
-
a.alertId = $alertId,
|
|
1613
|
-
a.ruleName = $ruleName,
|
|
1614
|
-
a.firstMatchAt = datetime($matchedAt),
|
|
1615
|
-
a.lastMatchAt = datetime($matchedAt),
|
|
1616
|
-
a.cumulativeMatchCount = 1,
|
|
1617
|
-
a.sampleEvidence = $sampleEvidence,
|
|
1618
|
-
a.suggestedAction = $suggestedAction,
|
|
1619
|
-
a.suppressedUntil = null,
|
|
1620
|
-
a.resolvedAt = null
|
|
1621
|
-
ON MATCH SET
|
|
1622
|
-
a.lastMatchAt = datetime($matchedAt),
|
|
1623
|
-
a.cumulativeMatchCount = a.cumulativeMatchCount + 1,
|
|
1624
|
-
a.sampleEvidence = $sampleEvidence,
|
|
1625
|
-
a.suggestedAction = $suggestedAction,
|
|
1626
|
-
a.resolvedAt = null`,
|
|
1627
|
-
{
|
|
1628
|
-
ruleId: match.ruleId,
|
|
1629
|
-
accountId,
|
|
1630
|
-
alertId: randomUUID(),
|
|
1631
|
-
ruleName: match.ruleName,
|
|
1632
|
-
matchedAt: new Date(match.matchedAt).toISOString(),
|
|
1633
|
-
sampleEvidence: match.sampleEvidence,
|
|
1634
|
-
suggestedAction: match.suggestedAction
|
|
1635
|
-
}
|
|
1636
|
-
);
|
|
1637
|
-
} finally {
|
|
1638
|
-
await session.close();
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
function queueAlert(configDir2, accountId, match) {
|
|
1642
|
-
const path2 = pendingAlertsPath(configDir2);
|
|
1643
|
-
try {
|
|
1644
|
-
mkdirSync3(dirname3(path2), { recursive: true });
|
|
1645
|
-
const line = JSON.stringify({ accountId, match }) + "\n";
|
|
1646
|
-
appendFileSync(path2, line, "utf-8");
|
|
1647
|
-
} catch (err) {
|
|
1648
|
-
console.error(`[review] failed to queue alert at ${path2}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
async function drainPendingAlerts(configDir2) {
|
|
1652
|
-
const path2 = pendingAlertsPath(configDir2);
|
|
1653
|
-
if (!existsSync4(path2)) return { drained: 0, remaining: 0 };
|
|
1654
|
-
const raw = readFileSync3(path2, "utf-8");
|
|
1655
|
-
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
1656
|
-
if (lines.length === 0) return { drained: 0, remaining: 0 };
|
|
1657
|
-
const remaining = [];
|
|
1658
|
-
let drained = 0;
|
|
1659
|
-
for (const line of lines) {
|
|
1660
|
-
let entry = null;
|
|
1661
|
-
try {
|
|
1662
|
-
entry = JSON.parse(line);
|
|
1663
|
-
} catch {
|
|
1664
|
-
continue;
|
|
1665
|
-
}
|
|
1666
|
-
try {
|
|
1667
|
-
await upsertReviewAlert(entry.accountId, entry.match);
|
|
1668
|
-
drained += 1;
|
|
1669
|
-
} catch {
|
|
1670
|
-
remaining.push(line);
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
|
|
1674
|
-
if (remaining.length > 0) {
|
|
1675
|
-
writeFileSync3(tmp, remaining.join("\n") + "\n", "utf-8");
|
|
1676
|
-
} else {
|
|
1677
|
-
writeFileSync3(tmp, "", "utf-8");
|
|
1678
|
-
}
|
|
1679
|
-
renameSync3(tmp, path2);
|
|
1680
|
-
return { drained, remaining: remaining.length };
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
// app/lib/review-detector/boot.ts
|
|
1684
|
-
async function bootDetector() {
|
|
1685
|
-
const account = resolveAccount();
|
|
1686
|
-
if (!account) {
|
|
1687
|
-
console.error("[review] boot: no account resolved \u2014 detector not starting (Phase 0 expects exactly one account)");
|
|
1688
|
-
return null;
|
|
1689
|
-
}
|
|
1690
|
-
const configDir2 = MAXY_DIR;
|
|
1691
|
-
const accountId = account.accountId;
|
|
1692
|
-
const accountDir = account.accountDir;
|
|
1693
|
-
const ensured = ensureRulesFile(configDir2);
|
|
1694
|
-
if (ensured.created) {
|
|
1695
|
-
reviewLog(configDir2, { event: "rules-defaults-created", path: ensured.path });
|
|
1696
|
-
}
|
|
1697
|
-
let rulesFile;
|
|
1698
|
-
try {
|
|
1699
|
-
rulesFile = loadRules(configDir2);
|
|
1700
|
-
} catch (err) {
|
|
1701
|
-
reviewLog(configDir2, {
|
|
1702
|
-
event: "boot-failed",
|
|
1703
|
-
reason: "rules-invalid",
|
|
1704
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1705
|
-
});
|
|
1706
|
-
console.error(`[review] boot: rules file invalid \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
1707
|
-
return null;
|
|
1708
|
-
}
|
|
1709
|
-
if (addMissingDefaultRules(rulesFile)) {
|
|
1710
|
-
saveRules(configDir2, rulesFile);
|
|
1711
|
-
reviewLog(configDir2, { event: "rules-updated", update: "added-missing-defaults" });
|
|
1712
|
-
console.error("[review] boot: added missing default rules to existing rules file");
|
|
1713
|
-
}
|
|
1714
|
-
if (migrateRateLimitPattern(rulesFile)) {
|
|
1715
|
-
saveRules(configDir2, rulesFile);
|
|
1716
|
-
reviewLog(configDir2, { event: "rules-migrated", migration: "rate-limit-pattern-v2" });
|
|
1717
|
-
console.error("[review] boot: migrated http-rate-limit-429 pattern to v2 (Task 408)");
|
|
1718
|
-
}
|
|
1719
|
-
try {
|
|
1720
|
-
await ensureReviewAlertIndex();
|
|
1721
|
-
reviewLog(configDir2, { event: "neo4j-index-ensured" });
|
|
1722
|
-
} catch (err) {
|
|
1723
|
-
reviewLog(configDir2, {
|
|
1724
|
-
event: "neo4j-index-failed",
|
|
1725
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1726
|
-
});
|
|
1727
|
-
console.error(`[review] boot: ensureReviewAlertIndex failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
1728
|
-
}
|
|
1729
|
-
try {
|
|
1730
|
-
await ensureReviewDigestSchedule(accountId);
|
|
1731
|
-
reviewLog(configDir2, { event: "digest-schedule-ensured" });
|
|
1732
|
-
} catch (err) {
|
|
1733
|
-
reviewLog(configDir2, {
|
|
1734
|
-
event: "digest-schedule-failed",
|
|
1735
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1736
|
-
});
|
|
1737
|
-
console.error(`[review] boot: ensureReviewDigestSchedule failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
1738
|
-
}
|
|
1739
|
-
const tailState = loadTailState(configDir2);
|
|
1740
|
-
const logDir = accountLogDir(accountDir);
|
|
1741
|
-
const initialSources = discoverAllSources(configDir2, logDir);
|
|
1742
|
-
const snapshot = {
|
|
1743
|
-
state: "running",
|
|
1744
|
-
startedAt: Date.now(),
|
|
1745
|
-
lastScanAt: null,
|
|
1746
|
-
lastScanDurationMs: null,
|
|
1747
|
-
scanCycles: 0,
|
|
1748
|
-
rulesLoaded: rulesFile.rules.length,
|
|
1749
|
-
sourcesTracked: initialSources.length,
|
|
1750
|
-
activeAlerts: 0,
|
|
1751
|
-
lastError: null
|
|
1752
|
-
};
|
|
1753
|
-
reviewLog(configDir2, {
|
|
1754
|
-
event: "detector-started",
|
|
1755
|
-
configDir: basename2(configDir2),
|
|
1756
|
-
accountId: accountId.slice(0, 8),
|
|
1757
|
-
rulesLoaded: rulesFile.rules.length,
|
|
1758
|
-
sourcesDiscovered: initialSources.length,
|
|
1759
|
-
tailStateEntries: Object.keys(tailState).length,
|
|
1760
|
-
scanIntervalMs: rulesFile.scanIntervalMs
|
|
1761
|
-
});
|
|
1762
|
-
const runtime = {
|
|
1763
|
-
configDir: configDir2,
|
|
1764
|
-
accountId,
|
|
1765
|
-
accountDir,
|
|
1766
|
-
rulesFile,
|
|
1767
|
-
rulesFileMtimeMs: rulesFileMtime(configDir2) ?? Date.now(),
|
|
1768
|
-
ruleState: /* @__PURE__ */ new Map(),
|
|
1769
|
-
tailState,
|
|
1770
|
-
snapshot,
|
|
1771
|
-
stopped: false
|
|
1772
|
-
};
|
|
1773
|
-
return runtime;
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
// app/lib/review-detector/scan-loop.ts
|
|
1777
|
-
import { resolve as resolve4 } from "path";
|
|
1778
|
-
|
|
1779
|
-
// app/lib/review-detector/evaluator.ts
|
|
1780
|
-
var SAMPLE_MAX_CHARS = 500;
|
|
1781
|
-
var CONV_ID_REGEX = /conversationId=([a-f0-9]{8})/;
|
|
1782
|
-
function scopeKeyFor(rule, line) {
|
|
1783
|
-
if (rule.scope !== "session") return "";
|
|
1784
|
-
const m = CONV_ID_REGEX.exec(line);
|
|
1785
|
-
return m ? m[1] : "";
|
|
1786
|
-
}
|
|
1787
|
-
var compiledRegexCache = /* @__PURE__ */ new Map();
|
|
1788
|
-
function compileRegex(pattern) {
|
|
1789
|
-
let re = compiledRegexCache.get(pattern);
|
|
1790
|
-
if (re === void 0) {
|
|
1791
|
-
re = new RegExp(pattern, "i");
|
|
1792
|
-
compiledRegexCache.set(pattern, re);
|
|
1793
|
-
}
|
|
1794
|
-
return re;
|
|
1795
|
-
}
|
|
1796
|
-
function isSuppressed(rule, nowMs) {
|
|
1797
|
-
if (!rule.suppressedUntil) return false;
|
|
1798
|
-
const until = Date.parse(rule.suppressedUntil);
|
|
1799
|
-
if (Number.isNaN(until)) return false;
|
|
1800
|
-
return nowMs < until;
|
|
1801
|
-
}
|
|
1802
|
-
function toSample(line) {
|
|
1803
|
-
if (line.length <= SAMPLE_MAX_CHARS) return line;
|
|
1804
|
-
return line.slice(0, SAMPLE_MAX_CHARS) + "\u2026";
|
|
1805
|
-
}
|
|
1806
|
-
function newRuleState() {
|
|
1807
|
-
return {
|
|
1808
|
-
matchTimestamps: [],
|
|
1809
|
-
matchTimestampsByScope: /* @__PURE__ */ new Map(),
|
|
1810
|
-
lastAlertAt: null,
|
|
1811
|
-
cumulativeSinceLastAlert: 0,
|
|
1812
|
-
lastSeenAt: null,
|
|
1813
|
-
pendingFollowups: []
|
|
1814
|
-
};
|
|
1815
|
-
}
|
|
1816
|
-
function evaluateTextRule(rule, lines, state, nowMs) {
|
|
1817
|
-
if (isSuppressed(rule, nowMs)) return { match: null, state };
|
|
1818
|
-
if (rule.pattern.length === 0) return { match: null, state };
|
|
1819
|
-
const regex = compileRegex(rule.pattern);
|
|
1820
|
-
const matchesByScope = /* @__PURE__ */ new Map();
|
|
1821
|
-
let firstSample = null;
|
|
1822
|
-
for (const line of lines) {
|
|
1823
|
-
if (!regex.test(line)) continue;
|
|
1824
|
-
if (!firstSample) firstSample = line;
|
|
1825
|
-
const key = scopeKeyFor(rule, line);
|
|
1826
|
-
const existing = matchesByScope.get(key) ?? [];
|
|
1827
|
-
existing.push(line);
|
|
1828
|
-
matchesByScope.set(key, existing);
|
|
1829
|
-
}
|
|
1830
|
-
if (matchesByScope.size === 0) return { match: null, state };
|
|
1831
|
-
const windowStart = rule.thresholdWindowMinutes > 0 ? nowMs - rule.thresholdWindowMinutes * 6e4 : -Infinity;
|
|
1832
|
-
const updated = {
|
|
1833
|
-
...state,
|
|
1834
|
-
matchTimestamps: [...state.matchTimestamps],
|
|
1835
|
-
matchTimestampsByScope: new Map(state.matchTimestampsByScope ?? [])
|
|
1836
|
-
};
|
|
1837
|
-
let firingSample = null;
|
|
1838
|
-
let fires = false;
|
|
1839
|
-
const isCountZero = rule.thresholdCount === 0;
|
|
1840
|
-
if (rule.scope === "session") {
|
|
1841
|
-
for (const [key, hits] of matchesByScope) {
|
|
1842
|
-
const prior = updated.matchTimestampsByScope.get(key) ?? [];
|
|
1843
|
-
const merged = [...prior, ...hits.map(() => nowMs)].filter((t) => t >= windowStart);
|
|
1844
|
-
if (merged.length === 0) {
|
|
1845
|
-
updated.matchTimestampsByScope.delete(key);
|
|
1846
|
-
} else {
|
|
1847
|
-
updated.matchTimestampsByScope.set(key, merged);
|
|
1848
|
-
}
|
|
1849
|
-
if (!fires && (isCountZero || merged.length >= rule.thresholdCount)) {
|
|
1850
|
-
fires = true;
|
|
1851
|
-
firingSample = hits[0];
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
} else {
|
|
1855
|
-
for (const hits of matchesByScope.values()) {
|
|
1856
|
-
for (const _ of hits) updated.matchTimestamps.push(nowMs);
|
|
1857
|
-
}
|
|
1858
|
-
updated.matchTimestamps = updated.matchTimestamps.filter((t) => t >= windowStart);
|
|
1859
|
-
fires = isCountZero || updated.matchTimestamps.length >= rule.thresholdCount;
|
|
1860
|
-
if (fires) firingSample = firstSample;
|
|
1861
|
-
}
|
|
1862
|
-
if (!fires) {
|
|
1863
|
-
return { match: null, state: updated };
|
|
1864
|
-
}
|
|
1865
|
-
const match = {
|
|
1866
|
-
ruleId: rule.id,
|
|
1867
|
-
ruleName: rule.name,
|
|
1868
|
-
matchedAt: nowMs,
|
|
1869
|
-
sampleEvidence: toSample(firingSample ?? firstSample ?? lines[0] ?? ""),
|
|
1870
|
-
suggestedAction: rule.suggestedAction
|
|
1871
|
-
};
|
|
1872
|
-
return { match, state: updated };
|
|
1873
|
-
}
|
|
1874
|
-
function evaluateFileWriteStormRule(rule, recentWriteCount, state, nowMs) {
|
|
1875
|
-
if (isSuppressed(rule, nowMs)) return { match: null, state };
|
|
1876
|
-
if (recentWriteCount < rule.thresholdCount) {
|
|
1877
|
-
return { match: null, state };
|
|
1878
|
-
}
|
|
1879
|
-
const match = {
|
|
1880
|
-
ruleId: rule.id,
|
|
1881
|
-
ruleName: rule.name,
|
|
1882
|
-
matchedAt: nowMs,
|
|
1883
|
-
sampleEvidence: `${recentWriteCount} file writes in ${rule.thresholdWindowMinutes}m at ${rule.watchPath}`,
|
|
1884
|
-
suggestedAction: rule.suggestedAction
|
|
1885
|
-
};
|
|
1886
|
-
return { match, state };
|
|
1887
|
-
}
|
|
1888
|
-
function evaluateStaleLogRule(rule, lastMtimeMs, state, nowMs) {
|
|
1889
|
-
if (isSuppressed(rule, nowMs)) return { match: null, state };
|
|
1890
|
-
let lastSeenAt = state.lastSeenAt;
|
|
1891
|
-
if (lastMtimeMs !== null) {
|
|
1892
|
-
lastSeenAt = Math.max(lastSeenAt ?? 0, lastMtimeMs);
|
|
1893
|
-
}
|
|
1894
|
-
const updated = { ...state, lastSeenAt };
|
|
1895
|
-
if (lastMtimeMs === null || lastSeenAt === null) {
|
|
1896
|
-
return { match: null, state: updated };
|
|
1897
|
-
}
|
|
1898
|
-
const staleMs = (rule.staleHours ?? 24) * 60 * 60 * 1e3;
|
|
1899
|
-
if (nowMs - lastSeenAt < staleMs) {
|
|
1900
|
-
return { match: null, state: updated };
|
|
1901
|
-
}
|
|
1902
|
-
const match = {
|
|
1903
|
-
ruleId: rule.id,
|
|
1904
|
-
ruleName: rule.name,
|
|
1905
|
-
matchedAt: nowMs,
|
|
1906
|
-
sampleEvidence: `last write at ${new Date(lastSeenAt).toISOString()} (${rule.watchPath})`,
|
|
1907
|
-
suggestedAction: rule.suggestedAction
|
|
1908
|
-
};
|
|
1909
|
-
return { match, state: updated };
|
|
1910
|
-
}
|
|
1911
|
-
function evaluateAbsentFollowupRule(rule, lines, state, nowMs) {
|
|
1912
|
-
if (isSuppressed(rule, nowMs)) return { matches: [], state };
|
|
1913
|
-
if (!rule.pattern || !rule.followupPattern || !rule.followupWindowMs) {
|
|
1914
|
-
return { matches: [], state };
|
|
1915
|
-
}
|
|
1916
|
-
const triggerRegex = compileRegex(rule.pattern);
|
|
1917
|
-
const followupRegex = compileRegex(rule.followupPattern);
|
|
1918
|
-
const pending = [...state.pendingFollowups ?? []];
|
|
1919
|
-
for (const line of lines) {
|
|
1920
|
-
if (triggerRegex.test(line)) {
|
|
1921
|
-
pending.push({
|
|
1922
|
-
scope: scopeKeyFor(rule, line),
|
|
1923
|
-
timestamp: nowMs,
|
|
1924
|
-
line,
|
|
1925
|
-
fulfilled: false
|
|
1926
|
-
});
|
|
1927
|
-
continue;
|
|
1928
|
-
}
|
|
1929
|
-
if (followupRegex.test(line)) {
|
|
1930
|
-
const scope = scopeKeyFor(rule, line);
|
|
1931
|
-
for (const entry of pending) {
|
|
1932
|
-
if (!entry.fulfilled && entry.scope === scope) {
|
|
1933
|
-
entry.fulfilled = true;
|
|
1934
|
-
break;
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
const matches = [];
|
|
1940
|
-
const kept = [];
|
|
1941
|
-
for (const entry of pending) {
|
|
1942
|
-
const age = nowMs - entry.timestamp;
|
|
1943
|
-
if (age >= rule.followupWindowMs) {
|
|
1944
|
-
if (!entry.fulfilled) {
|
|
1945
|
-
matches.push({
|
|
1946
|
-
ruleId: rule.id,
|
|
1947
|
-
ruleName: rule.name,
|
|
1948
|
-
matchedAt: nowMs,
|
|
1949
|
-
sampleEvidence: toSample(entry.line),
|
|
1950
|
-
suggestedAction: rule.suggestedAction,
|
|
1951
|
-
missedForMs: age
|
|
1952
|
-
});
|
|
1953
|
-
}
|
|
1954
|
-
continue;
|
|
1955
|
-
}
|
|
1956
|
-
kept.push(entry);
|
|
1957
|
-
}
|
|
1958
|
-
return {
|
|
1959
|
-
matches,
|
|
1960
|
-
state: { ...state, pendingFollowups: kept }
|
|
1961
|
-
};
|
|
1962
|
-
}
|
|
1963
|
-
var ALERT_WINDOW_MS = 60 * 60 * 1e3;
|
|
1964
|
-
function rateLimitDecision(state, nowMs) {
|
|
1965
|
-
const since = state.lastAlertAt === null ? Infinity : nowMs - state.lastAlertAt;
|
|
1966
|
-
if (since >= ALERT_WINDOW_MS) {
|
|
1967
|
-
return {
|
|
1968
|
-
surface: true,
|
|
1969
|
-
state: { ...state, lastAlertAt: nowMs, cumulativeSinceLastAlert: 0 }
|
|
1970
|
-
};
|
|
1971
|
-
}
|
|
1972
|
-
return {
|
|
1973
|
-
surface: false,
|
|
1974
|
-
state: { ...state, cumulativeSinceLastAlert: state.cumulativeSinceLastAlert + 1 }
|
|
1975
|
-
};
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
// app/lib/review-detector/scan-loop.ts
|
|
1979
|
-
async function runScanCycle(runtime) {
|
|
1980
|
-
const cycleStart = Date.now();
|
|
1981
|
-
runtime.snapshot.scanCycles += 1;
|
|
1982
|
-
try {
|
|
1983
|
-
const currentMtime = rulesFileMtime(runtime.configDir);
|
|
1984
|
-
if (currentMtime !== null && currentMtime !== runtime.rulesFileMtimeMs) {
|
|
1985
|
-
try {
|
|
1986
|
-
runtime.rulesFile = loadRules(runtime.configDir);
|
|
1987
|
-
runtime.rulesFileMtimeMs = currentMtime;
|
|
1988
|
-
runtime.snapshot.rulesLoaded = runtime.rulesFile.rules.length;
|
|
1989
|
-
reviewLog(runtime.configDir, {
|
|
1990
|
-
event: "rules-reloaded",
|
|
1991
|
-
rulesLoaded: runtime.rulesFile.rules.length,
|
|
1992
|
-
mtime: new Date(currentMtime).toISOString()
|
|
1993
|
-
});
|
|
1994
|
-
} catch (err) {
|
|
1995
|
-
reviewLog(runtime.configDir, {
|
|
1996
|
-
event: "rules-reload-failed",
|
|
1997
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1998
|
-
});
|
|
1999
|
-
runtime.snapshot.state = "degraded";
|
|
2000
|
-
runtime.snapshot.lastError = err instanceof Error ? err.message : String(err);
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
const logDir = accountLogDir(runtime.accountDir);
|
|
2004
|
-
const files = discoverAllSources(runtime.configDir, logDir);
|
|
2005
|
-
runtime.snapshot.sourcesTracked = files.length;
|
|
2006
|
-
const linesBySource = /* @__PURE__ */ new Map();
|
|
2007
|
-
for (const file of files) {
|
|
2008
|
-
const key = sourceKey(file);
|
|
2009
|
-
const prev = runtime.tailState[key];
|
|
2010
|
-
const result = readNewLines(file.filepath, prev);
|
|
2011
|
-
if (result === null) {
|
|
2012
|
-
if (prev) {
|
|
2013
|
-
reviewLog(runtime.configDir, {
|
|
2014
|
-
event: "source-vanished",
|
|
2015
|
-
source: file.logicalSource,
|
|
2016
|
-
filepath: file.filepath
|
|
2017
|
-
});
|
|
2018
|
-
delete runtime.tailState[key];
|
|
2019
|
-
}
|
|
2020
|
-
continue;
|
|
2021
|
-
}
|
|
2022
|
-
if (result.rotated) {
|
|
2023
|
-
reviewLog(runtime.configDir, {
|
|
2024
|
-
event: "source-rotated",
|
|
2025
|
-
source: file.logicalSource,
|
|
2026
|
-
filepath: file.filepath
|
|
2027
|
-
});
|
|
2028
|
-
}
|
|
2029
|
-
if (result.truncated) {
|
|
2030
|
-
reviewLog(runtime.configDir, {
|
|
2031
|
-
event: "source-truncated",
|
|
2032
|
-
source: file.logicalSource,
|
|
2033
|
-
filepath: file.filepath
|
|
2034
|
-
});
|
|
2035
|
-
}
|
|
2036
|
-
runtime.tailState[key] = result.entry;
|
|
2037
|
-
if (result.lines.length > 0) {
|
|
2038
|
-
const bucket = linesBySource.get(file.logicalSource) ?? [];
|
|
2039
|
-
bucket.push(...result.lines);
|
|
2040
|
-
linesBySource.set(file.logicalSource, bucket);
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
const matches = [];
|
|
2044
|
-
for (const rule of runtime.rulesFile.rules) {
|
|
2045
|
-
let state = runtime.ruleState.get(rule.id) ?? newRuleState();
|
|
2046
|
-
if (isSuppressed(rule, cycleStart)) {
|
|
2047
|
-
reviewLog(runtime.configDir, {
|
|
2048
|
-
event: "rule-suppressed",
|
|
2049
|
-
ruleId: rule.id,
|
|
2050
|
-
suppressedUntil: rule.suppressedUntil
|
|
2051
|
-
});
|
|
2052
|
-
runtime.ruleState.set(rule.id, state);
|
|
2053
|
-
continue;
|
|
2054
|
-
}
|
|
2055
|
-
let match = null;
|
|
2056
|
-
if (rule.type === "reconnect-loop" || rule.type === "repeated-error" || rule.type === "silent-catch" || rule.type === "rate-limit") {
|
|
2057
|
-
let inputLines = [];
|
|
2058
|
-
if (rule.logSource === "any") {
|
|
2059
|
-
for (const [src, lines] of linesBySource.entries()) {
|
|
2060
|
-
if (src !== "config-dir") inputLines.push(...lines);
|
|
2061
|
-
}
|
|
2062
|
-
} else {
|
|
2063
|
-
inputLines = linesBySource.get(rule.logSource) ?? [];
|
|
2064
|
-
}
|
|
2065
|
-
if (inputLines.length > 0) {
|
|
2066
|
-
const result = evaluateTextRule(rule, inputLines, state, cycleStart);
|
|
2067
|
-
state = result.state;
|
|
2068
|
-
match = result.match;
|
|
2069
|
-
}
|
|
2070
|
-
} else if (rule.type === "file-write-storm") {
|
|
2071
|
-
const dir = resolve4(runtime.configDir, rule.watchPath ?? "");
|
|
2072
|
-
const sinceMs = cycleStart - rule.thresholdWindowMinutes * 6e4;
|
|
2073
|
-
const count = countRecentWrites(dir, sinceMs);
|
|
2074
|
-
const result = evaluateFileWriteStormRule(rule, count, state, cycleStart);
|
|
2075
|
-
state = result.state;
|
|
2076
|
-
match = result.match;
|
|
2077
|
-
} else if (rule.type === "stale-log") {
|
|
2078
|
-
const trackedPath = resolve4(runtime.configDir, rule.watchPath ?? "");
|
|
2079
|
-
const lastMs = fileLastWriteMs(trackedPath);
|
|
2080
|
-
const result = evaluateStaleLogRule(rule, lastMs, state, cycleStart);
|
|
2081
|
-
state = result.state;
|
|
2082
|
-
match = result.match;
|
|
2083
|
-
} else if (rule.type === "absent-followup") {
|
|
2084
|
-
let inputLines = [];
|
|
2085
|
-
if (rule.logSource === "any") {
|
|
2086
|
-
for (const [src, lines] of linesBySource.entries()) {
|
|
2087
|
-
if (src !== "config-dir") inputLines.push(...lines);
|
|
2088
|
-
}
|
|
2089
|
-
} else {
|
|
2090
|
-
inputLines = linesBySource.get(rule.logSource) ?? [];
|
|
2091
|
-
}
|
|
2092
|
-
const result = evaluateAbsentFollowupRule(rule, inputLines, state, cycleStart);
|
|
2093
|
-
state = result.state;
|
|
2094
|
-
for (const m of result.matches) {
|
|
2095
|
-
const safeTrigger = m.sampleEvidence.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2096
|
-
console.error(
|
|
2097
|
-
`[review-detector] absent-followup rule=${rule.id} trigger="${safeTrigger}" missed_for_ms=${m.missedForMs ?? ""}`
|
|
2098
|
-
);
|
|
2099
|
-
matches.push(m);
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
runtime.ruleState.set(rule.id, state);
|
|
2103
|
-
if (match) matches.push(match);
|
|
2104
|
-
}
|
|
2105
|
-
for (const match of matches) {
|
|
2106
|
-
const state = runtime.ruleState.get(match.ruleId) ?? newRuleState();
|
|
2107
|
-
const decision = rateLimitDecision(state, cycleStart);
|
|
2108
|
-
runtime.ruleState.set(match.ruleId, decision.state);
|
|
2109
|
-
if (!decision.surface) {
|
|
2110
|
-
reviewLog(runtime.configDir, {
|
|
2111
|
-
event: "rate-limit-deferred",
|
|
2112
|
-
ruleId: match.ruleId,
|
|
2113
|
-
cumulativeSinceLastAlert: decision.state.cumulativeSinceLastAlert
|
|
2114
|
-
});
|
|
2115
|
-
continue;
|
|
2116
|
-
}
|
|
2117
|
-
reviewLog(runtime.configDir, {
|
|
2118
|
-
event: "match",
|
|
2119
|
-
ruleId: match.ruleId,
|
|
2120
|
-
ruleName: match.ruleName,
|
|
2121
|
-
sampleEvidence: match.sampleEvidence,
|
|
2122
|
-
suggestedAction: match.suggestedAction
|
|
2123
|
-
});
|
|
2124
|
-
try {
|
|
2125
|
-
await upsertReviewAlert(runtime.accountId, match);
|
|
2126
|
-
reviewLog(runtime.configDir, { event: "alert-persisted", ruleId: match.ruleId });
|
|
2127
|
-
} catch (err) {
|
|
2128
|
-
queueAlert(runtime.configDir, runtime.accountId, match);
|
|
2129
|
-
reviewLog(runtime.configDir, {
|
|
2130
|
-
event: "alert-queued",
|
|
2131
|
-
ruleId: match.ruleId,
|
|
2132
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2133
|
-
});
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
try {
|
|
2137
|
-
const drain = await drainPendingAlerts(runtime.configDir);
|
|
2138
|
-
if (drain.drained > 0 || drain.remaining > 0) {
|
|
2139
|
-
reviewLog(runtime.configDir, {
|
|
2140
|
-
event: "queue-drain",
|
|
2141
|
-
drained: drain.drained,
|
|
2142
|
-
remaining: drain.remaining
|
|
2143
|
-
});
|
|
2144
|
-
}
|
|
2145
|
-
} catch (err) {
|
|
2146
|
-
reviewLog(runtime.configDir, {
|
|
2147
|
-
event: "queue-drain-failed",
|
|
2148
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2149
|
-
});
|
|
2150
|
-
}
|
|
2151
|
-
try {
|
|
2152
|
-
runtime.snapshot.activeAlerts = await countActiveReviewAlerts(runtime.accountId);
|
|
2153
|
-
} catch (err) {
|
|
2154
|
-
reviewLog(runtime.configDir, {
|
|
2155
|
-
event: "active-alerts-count-failed",
|
|
2156
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2157
|
-
});
|
|
2158
|
-
}
|
|
2159
|
-
saveTailState(runtime.configDir, runtime.tailState);
|
|
2160
|
-
const cycleDuration = Date.now() - cycleStart;
|
|
2161
|
-
runtime.snapshot.lastScanAt = cycleStart;
|
|
2162
|
-
runtime.snapshot.lastScanDurationMs = cycleDuration;
|
|
2163
|
-
if (runtime.snapshot.state !== "degraded" && runtime.snapshot.state !== "failed") {
|
|
2164
|
-
runtime.snapshot.state = "running";
|
|
2165
|
-
}
|
|
2166
|
-
runtime.snapshot.lastError = null;
|
|
2167
|
-
reviewLog(runtime.configDir, {
|
|
2168
|
-
event: "cycle",
|
|
2169
|
-
cycles: runtime.snapshot.scanCycles,
|
|
2170
|
-
sources: files.length,
|
|
2171
|
-
rules: runtime.rulesFile.rules.length,
|
|
2172
|
-
matches: matches.length,
|
|
2173
|
-
durationMs: cycleDuration
|
|
2174
|
-
});
|
|
2175
|
-
} catch (err) {
|
|
2176
|
-
runtime.snapshot.state = "degraded";
|
|
2177
|
-
runtime.snapshot.lastError = err instanceof Error ? err.message : String(err);
|
|
2178
|
-
reviewLog(runtime.configDir, {
|
|
2179
|
-
event: "cycle-failed",
|
|
2180
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2181
|
-
});
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
function startScanLoop(runtime) {
|
|
2185
|
-
let inFlight = null;
|
|
2186
|
-
const interval = setInterval(async () => {
|
|
2187
|
-
if (runtime.stopped) return;
|
|
2188
|
-
if (inFlight) return;
|
|
2189
|
-
inFlight = runScanCycle(runtime);
|
|
2190
|
-
try {
|
|
2191
|
-
await inFlight;
|
|
2192
|
-
} finally {
|
|
2193
|
-
inFlight = null;
|
|
2194
|
-
}
|
|
2195
|
-
}, runtime.rulesFile.scanIntervalMs);
|
|
2196
|
-
inFlight = runScanCycle(runtime);
|
|
2197
|
-
return async () => {
|
|
2198
|
-
runtime.stopped = true;
|
|
2199
|
-
clearInterval(interval);
|
|
2200
|
-
if (inFlight) {
|
|
2201
|
-
try {
|
|
2202
|
-
await inFlight;
|
|
2203
|
-
} catch {
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
runtime.snapshot.state = "stopped";
|
|
2207
|
-
reviewLog(runtime.configDir, { event: "detector-stopped", cycles: runtime.snapshot.scanCycles });
|
|
2208
|
-
};
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
// app/lib/review-detector/index.ts
|
|
2212
|
-
var activeRuntime = null;
|
|
2213
|
-
var stopFn = null;
|
|
2214
|
-
async function startReviewDetector() {
|
|
2215
|
-
if (stopFn) {
|
|
2216
|
-
console.error("[review] startReviewDetector called twice \u2014 ignoring second call");
|
|
2217
|
-
return;
|
|
2218
|
-
}
|
|
2219
|
-
try {
|
|
2220
|
-
activeRuntime = await bootDetector();
|
|
2221
|
-
if (!activeRuntime) return;
|
|
2222
|
-
stopFn = startScanLoop(activeRuntime);
|
|
2223
|
-
} catch (err) {
|
|
2224
|
-
console.error(`[review] detector start failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
async function shutdownReviewDetector() {
|
|
2228
|
-
if (!stopFn) return;
|
|
2229
|
-
try {
|
|
2230
|
-
await stopFn();
|
|
2231
|
-
} catch (err) {
|
|
2232
|
-
console.error(`[review] detector shutdown error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2233
|
-
}
|
|
2234
|
-
stopFn = null;
|
|
2235
|
-
activeRuntime = null;
|
|
2236
|
-
}
|
|
2237
|
-
function getReviewDetectorSnapshot() {
|
|
2238
|
-
if (!activeRuntime) {
|
|
2239
|
-
return {
|
|
2240
|
-
state: "stopped",
|
|
2241
|
-
startedAt: null,
|
|
2242
|
-
lastScanAt: null,
|
|
2243
|
-
lastScanDurationMs: null,
|
|
2244
|
-
scanCycles: 0,
|
|
2245
|
-
rulesLoaded: 0,
|
|
2246
|
-
sourcesTracked: 0,
|
|
2247
|
-
activeAlerts: 0,
|
|
2248
|
-
lastError: null
|
|
2249
|
-
};
|
|
2250
|
-
}
|
|
2251
|
-
return activeRuntime.snapshot;
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
650
|
// app/lib/whatsapp/schema.ts
|
|
2255
651
|
import { z } from "zod";
|
|
2256
652
|
var DmPolicySchema = z.enum(["open", "allowlist", "disabled"]);
|
|
@@ -2315,20 +711,20 @@ var WhatsAppConfigSchema = z.object({
|
|
|
2315
711
|
});
|
|
2316
712
|
|
|
2317
713
|
// app/lib/whatsapp/config-persist.ts
|
|
2318
|
-
import { readFileSync
|
|
2319
|
-
import { resolve
|
|
714
|
+
import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
715
|
+
import { resolve, join as join2 } from "path";
|
|
2320
716
|
var TAG = "[whatsapp:config]";
|
|
2321
717
|
function configPath(accountDir) {
|
|
2322
|
-
return
|
|
718
|
+
return resolve(accountDir, "account.json");
|
|
2323
719
|
}
|
|
2324
720
|
function readConfig(accountDir) {
|
|
2325
721
|
const path2 = configPath(accountDir);
|
|
2326
|
-
if (!
|
|
2327
|
-
return JSON.parse(
|
|
722
|
+
if (!existsSync2(path2)) throw new Error(`account.json not found at ${path2}`);
|
|
723
|
+
return JSON.parse(readFileSync(path2, "utf-8"));
|
|
2328
724
|
}
|
|
2329
725
|
function writeConfig(accountDir, config) {
|
|
2330
726
|
const path2 = configPath(accountDir);
|
|
2331
|
-
|
|
727
|
+
writeFileSync(path2, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2332
728
|
}
|
|
2333
729
|
function reloadManagerConfig(accountDir) {
|
|
2334
730
|
try {
|
|
@@ -2498,8 +894,8 @@ function setPublicAgent(accountDir, slug) {
|
|
|
2498
894
|
if (!trimmed) {
|
|
2499
895
|
return { ok: false, error: "Agent slug cannot be empty." };
|
|
2500
896
|
}
|
|
2501
|
-
const agentConfigPath =
|
|
2502
|
-
if (!
|
|
897
|
+
const agentConfigPath = join2(accountDir, "agents", trimmed, "config.json");
|
|
898
|
+
if (!existsSync2(agentConfigPath)) {
|
|
2503
899
|
return { ok: false, error: `Agent "${trimmed}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
|
|
2504
900
|
}
|
|
2505
901
|
try {
|
|
@@ -2562,8 +958,8 @@ function setGroupPublicAgent(accountDir, accountId, groupJid, slug) {
|
|
|
2562
958
|
if (!trimmedSlug) return { ok: false, error: "Agent slug cannot be empty." };
|
|
2563
959
|
if (!trimmedGroup) return { ok: false, error: "Group JID cannot be empty." };
|
|
2564
960
|
if (!trimmedAccount) return { ok: false, error: "Account ID cannot be empty." };
|
|
2565
|
-
const agentConfigPath =
|
|
2566
|
-
if (!
|
|
961
|
+
const agentConfigPath = join2(accountDir, "agents", trimmedSlug, "config.json");
|
|
962
|
+
if (!existsSync2(agentConfigPath)) {
|
|
2567
963
|
return { ok: false, error: `Agent "${trimmedSlug}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
|
|
2568
964
|
}
|
|
2569
965
|
try {
|
|
@@ -2792,7 +1188,7 @@ function listCredentialAccountIds(configDir2) {
|
|
|
2792
1188
|
}
|
|
2793
1189
|
|
|
2794
1190
|
// app/lib/whatsapp/session.ts
|
|
2795
|
-
import { randomUUID
|
|
1191
|
+
import { randomUUID } from "crypto";
|
|
2796
1192
|
import fsSync2 from "fs";
|
|
2797
1193
|
import fs2 from "fs/promises";
|
|
2798
1194
|
import { inspect } from "util";
|
|
@@ -2867,7 +1263,7 @@ var credsSaveQueue = Promise.resolve();
|
|
|
2867
1263
|
async function drainCredsSaveQueue(timeoutMs = 5e3) {
|
|
2868
1264
|
console.error(`${TAG3} draining credential save queue\u2026`);
|
|
2869
1265
|
const timer2 = new Promise(
|
|
2870
|
-
(
|
|
1266
|
+
(resolve22) => setTimeout(() => resolve22("timeout"), timeoutMs)
|
|
2871
1267
|
);
|
|
2872
1268
|
const result = await Promise.race([
|
|
2873
1269
|
credsSaveQueue.then(() => "drained"),
|
|
@@ -2995,11 +1391,11 @@ async function createWaSocket(opts) {
|
|
|
2995
1391
|
return sock;
|
|
2996
1392
|
}
|
|
2997
1393
|
async function waitForConnection(sock) {
|
|
2998
|
-
return new Promise((
|
|
1394
|
+
return new Promise((resolve22, reject) => {
|
|
2999
1395
|
const handler = (update) => {
|
|
3000
1396
|
if (update.connection === "open") {
|
|
3001
1397
|
sock.ev.off("connection.update", handler);
|
|
3002
|
-
|
|
1398
|
+
resolve22();
|
|
3003
1399
|
}
|
|
3004
1400
|
if (update.connection === "close") {
|
|
3005
1401
|
sock.ev.off("connection.update", handler);
|
|
@@ -3113,14 +1509,14 @@ ${inspected}`;
|
|
|
3113
1509
|
return inspect2(err, INSPECT_OPTS2);
|
|
3114
1510
|
}
|
|
3115
1511
|
function withTimeout(label, promise, timeoutMs) {
|
|
3116
|
-
return new Promise((
|
|
1512
|
+
return new Promise((resolve22, reject) => {
|
|
3117
1513
|
const timer2 = setTimeout(() => {
|
|
3118
1514
|
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
3119
1515
|
}, timeoutMs);
|
|
3120
1516
|
promise.then(
|
|
3121
1517
|
(value) => {
|
|
3122
1518
|
clearTimeout(timer2);
|
|
3123
|
-
|
|
1519
|
+
resolve22(value);
|
|
3124
1520
|
},
|
|
3125
1521
|
(err) => {
|
|
3126
1522
|
clearTimeout(timer2);
|
|
@@ -3655,8 +2051,8 @@ async function persistWhatsAppMessage(input) {
|
|
|
3655
2051
|
const { givenName, familyName } = splitName(input.pushName);
|
|
3656
2052
|
const prev = sessionWriteLocks.get(input.sessionKey);
|
|
3657
2053
|
let release;
|
|
3658
|
-
const mine = new Promise((
|
|
3659
|
-
release =
|
|
2054
|
+
const mine = new Promise((resolve22) => {
|
|
2055
|
+
release = resolve22;
|
|
3660
2056
|
});
|
|
3661
2057
|
const chained = (prev ?? Promise.resolve()).then(() => mine);
|
|
3662
2058
|
sessionWriteLocks.set(input.sessionKey, chained);
|
|
@@ -3841,8 +2237,8 @@ async function ensureWhatsAppConversation(input) {
|
|
|
3841
2237
|
}
|
|
3842
2238
|
|
|
3843
2239
|
// app/lib/whatsapp/platform-account-id.ts
|
|
3844
|
-
import { readdirSync
|
|
3845
|
-
import { resolve as
|
|
2240
|
+
import { readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
2241
|
+
import { resolve as resolve2 } from "path";
|
|
3846
2242
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3847
2243
|
var cached = null;
|
|
3848
2244
|
var cachedAccountsDir = null;
|
|
@@ -3866,7 +2262,7 @@ function resolvePlatformAccountId(accountsDir = ACCOUNTS_DIR) {
|
|
|
3866
2262
|
function enumerateValidAccountIds(accountsDir) {
|
|
3867
2263
|
let names;
|
|
3868
2264
|
try {
|
|
3869
|
-
names =
|
|
2265
|
+
names = readdirSync(accountsDir);
|
|
3870
2266
|
} catch (err) {
|
|
3871
2267
|
if (err.code === "ENOENT") return [];
|
|
3872
2268
|
throw err;
|
|
@@ -3874,9 +2270,9 @@ function enumerateValidAccountIds(accountsDir) {
|
|
|
3874
2270
|
const valid = [];
|
|
3875
2271
|
for (const name of names) {
|
|
3876
2272
|
if (!UUID_RE.test(name)) continue;
|
|
3877
|
-
const configPath2 =
|
|
2273
|
+
const configPath2 = resolve2(accountsDir, name, "account.json");
|
|
3878
2274
|
try {
|
|
3879
|
-
JSON.parse(
|
|
2275
|
+
JSON.parse(readFileSync2(configPath2, "utf-8"));
|
|
3880
2276
|
valid.push(name);
|
|
3881
2277
|
} catch (err) {
|
|
3882
2278
|
const code = err.code;
|
|
@@ -3887,9 +2283,9 @@ function enumerateValidAccountIds(accountsDir) {
|
|
|
3887
2283
|
}
|
|
3888
2284
|
|
|
3889
2285
|
// app/lib/whatsapp/inbound/media.ts
|
|
3890
|
-
import { randomUUID as
|
|
2286
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3891
2287
|
import { writeFile, mkdir } from "fs/promises";
|
|
3892
|
-
import { join as
|
|
2288
|
+
import { join as join3 } from "path";
|
|
3893
2289
|
import {
|
|
3894
2290
|
downloadMediaMessage,
|
|
3895
2291
|
downloadContentFromMessage,
|
|
@@ -3974,8 +2370,8 @@ async function downloadInboundMedia(msg, sock, opts) {
|
|
|
3974
2370
|
}
|
|
3975
2371
|
await mkdir(MEDIA_DIR, { recursive: true });
|
|
3976
2372
|
const ext = mimeToExt(mimetype ?? "application/octet-stream");
|
|
3977
|
-
const filename = `${
|
|
3978
|
-
const filePath =
|
|
2373
|
+
const filename = `${randomUUID2()}.${ext}`;
|
|
2374
|
+
const filePath = join3(MEDIA_DIR, filename);
|
|
3979
2375
|
await writeFile(filePath, buffer);
|
|
3980
2376
|
const sizeKB = (buffer.length / 1024).toFixed(0);
|
|
3981
2377
|
console.error(`${TAG9} media downloaded type=${mimetype ?? "unknown"} size=${sizeKB}KB path=${filePath}`);
|
|
@@ -4663,11 +3059,11 @@ async function connectWithReconnect(conn) {
|
|
|
4663
3059
|
console.error(
|
|
4664
3060
|
`${TAG13} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
|
|
4665
3061
|
);
|
|
4666
|
-
await new Promise((
|
|
4667
|
-
const timer2 = setTimeout(
|
|
3062
|
+
await new Promise((resolve22) => {
|
|
3063
|
+
const timer2 = setTimeout(resolve22, delay);
|
|
4668
3064
|
conn.abortController.signal.addEventListener("abort", () => {
|
|
4669
3065
|
clearTimeout(timer2);
|
|
4670
|
-
|
|
3066
|
+
resolve22();
|
|
4671
3067
|
}, { once: true });
|
|
4672
3068
|
});
|
|
4673
3069
|
}
|
|
@@ -4675,16 +3071,16 @@ async function connectWithReconnect(conn) {
|
|
|
4675
3071
|
}
|
|
4676
3072
|
}
|
|
4677
3073
|
function waitForDisconnectEvent(conn) {
|
|
4678
|
-
return new Promise((
|
|
3074
|
+
return new Promise((resolve22) => {
|
|
4679
3075
|
if (!conn.sock) {
|
|
4680
|
-
|
|
3076
|
+
resolve22();
|
|
4681
3077
|
return;
|
|
4682
3078
|
}
|
|
4683
3079
|
const sock = conn.sock;
|
|
4684
3080
|
const handler = (update) => {
|
|
4685
3081
|
if (update.connection === "close") {
|
|
4686
3082
|
sock.ev.off("connection.update", handler);
|
|
4687
|
-
|
|
3083
|
+
resolve22();
|
|
4688
3084
|
}
|
|
4689
3085
|
};
|
|
4690
3086
|
sock.ev.on("connection.update", handler);
|
|
@@ -4945,8 +3341,8 @@ async function handleInboundMessage(conn, msg) {
|
|
|
4945
3341
|
const conversationKey = isGroup ? remoteJid : senderPhone;
|
|
4946
3342
|
const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
|
|
4947
3343
|
let resolvePending;
|
|
4948
|
-
const sttPending = new Promise((
|
|
4949
|
-
resolvePending =
|
|
3344
|
+
const sttPending = new Promise((resolve22) => {
|
|
3345
|
+
resolvePending = resolve22;
|
|
4950
3346
|
});
|
|
4951
3347
|
if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
|
|
4952
3348
|
try {
|
|
@@ -5067,20 +3463,20 @@ async function probeApiKey() {
|
|
|
5067
3463
|
return result.status;
|
|
5068
3464
|
}
|
|
5069
3465
|
function checkPort(port2, timeoutMs = 500) {
|
|
5070
|
-
return new Promise((
|
|
3466
|
+
return new Promise((resolve22) => {
|
|
5071
3467
|
const socket = createConnection(port2, "127.0.0.1");
|
|
5072
3468
|
socket.setTimeout(timeoutMs);
|
|
5073
3469
|
socket.once("connect", () => {
|
|
5074
3470
|
socket.destroy();
|
|
5075
|
-
|
|
3471
|
+
resolve22(true);
|
|
5076
3472
|
});
|
|
5077
3473
|
socket.once("error", () => {
|
|
5078
3474
|
socket.destroy();
|
|
5079
|
-
|
|
3475
|
+
resolve22(false);
|
|
5080
3476
|
});
|
|
5081
3477
|
socket.once("timeout", () => {
|
|
5082
3478
|
socket.destroy();
|
|
5083
|
-
|
|
3479
|
+
resolve22(false);
|
|
5084
3480
|
});
|
|
5085
3481
|
});
|
|
5086
3482
|
}
|
|
@@ -5089,8 +3485,8 @@ app.get("/", async (c) => {
|
|
|
5089
3485
|
const browserTransport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
|
|
5090
3486
|
let pinConfigured = false;
|
|
5091
3487
|
try {
|
|
5092
|
-
if (
|
|
5093
|
-
const raw =
|
|
3488
|
+
if (existsSync3(USERS_FILE)) {
|
|
3489
|
+
const raw = readFileSync3(USERS_FILE, "utf-8").trim();
|
|
5094
3490
|
if (raw) {
|
|
5095
3491
|
const users = JSON.parse(raw);
|
|
5096
3492
|
pinConfigured = Array.isArray(users) && users.length > 0;
|
|
@@ -5109,7 +3505,7 @@ app.get("/", async (c) => {
|
|
|
5109
3505
|
const vncRunning = await checkPort(6080);
|
|
5110
3506
|
let apiKeyConfigured = false;
|
|
5111
3507
|
try {
|
|
5112
|
-
apiKeyConfigured =
|
|
3508
|
+
apiKeyConfigured = existsSync3(keyFilePath());
|
|
5113
3509
|
} catch {
|
|
5114
3510
|
}
|
|
5115
3511
|
let apiKeyStatus = "missing";
|
|
@@ -5142,7 +3538,6 @@ app.get("/", async (c) => {
|
|
|
5142
3538
|
const step = await loadOnboardingStep(account.accountId);
|
|
5143
3539
|
if (step !== null) onboardingComplete = step >= 6;
|
|
5144
3540
|
}
|
|
5145
|
-
const reviewDetector = getReviewDetectorSnapshot();
|
|
5146
3541
|
return c.json({
|
|
5147
3542
|
pin_configured: pinConfigured,
|
|
5148
3543
|
claude_authenticated: claudeAuthenticated,
|
|
@@ -5158,31 +3553,20 @@ app.get("/", async (c) => {
|
|
|
5158
3553
|
any_connected: whatsappAnyConnected,
|
|
5159
3554
|
any_stuck: whatsappAnyStuck,
|
|
5160
3555
|
accounts: whatsappAccounts
|
|
5161
|
-
},
|
|
5162
|
-
review_detector: {
|
|
5163
|
-
state: reviewDetector.state,
|
|
5164
|
-
started_at: reviewDetector.startedAt,
|
|
5165
|
-
last_scan_at: reviewDetector.lastScanAt,
|
|
5166
|
-
last_scan_duration_ms: reviewDetector.lastScanDurationMs,
|
|
5167
|
-
scan_cycles: reviewDetector.scanCycles,
|
|
5168
|
-
rules_loaded: reviewDetector.rulesLoaded,
|
|
5169
|
-
sources_tracked: reviewDetector.sourcesTracked,
|
|
5170
|
-
active_alerts: reviewDetector.activeAlerts,
|
|
5171
|
-
last_error: reviewDetector.lastError
|
|
5172
3556
|
}
|
|
5173
3557
|
});
|
|
5174
3558
|
});
|
|
5175
3559
|
var health_default = app;
|
|
5176
3560
|
|
|
5177
3561
|
// server/routes/session.ts
|
|
5178
|
-
import { resolve as
|
|
5179
|
-
import { existsSync as
|
|
3562
|
+
import { resolve as resolve3 } from "path";
|
|
3563
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
5180
3564
|
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5181
3565
|
function writeBrandingCache(accountId, agentSlug, branding) {
|
|
5182
3566
|
try {
|
|
5183
|
-
const cacheDir =
|
|
5184
|
-
|
|
5185
|
-
|
|
3567
|
+
const cacheDir = resolve3(MAXY_DIR, "branding-cache", accountId);
|
|
3568
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
3569
|
+
writeFileSync2(resolve3(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
|
|
5186
3570
|
} catch (err) {
|
|
5187
3571
|
console.error(`[branding] cache write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5188
3572
|
}
|
|
@@ -5252,9 +3636,9 @@ app2.post("/", async (c) => {
|
|
|
5252
3636
|
}
|
|
5253
3637
|
let agentConfig = null;
|
|
5254
3638
|
if (account) {
|
|
5255
|
-
const agentDir =
|
|
5256
|
-
const agentConfigPath =
|
|
5257
|
-
if (!
|
|
3639
|
+
const agentDir = resolve3(account.accountDir, "agents", agentSlug);
|
|
3640
|
+
const agentConfigPath = resolve3(agentDir, "config.json");
|
|
3641
|
+
if (!existsSync4(agentDir) || !existsSync4(agentConfigPath)) {
|
|
5258
3642
|
return c.json({ error: "Agent not found" }, 404);
|
|
5259
3643
|
}
|
|
5260
3644
|
agentConfig = resolveAgentConfig(account.accountDir, agentSlug);
|
|
@@ -5504,12 +3888,12 @@ ${raw}`;
|
|
|
5504
3888
|
}
|
|
5505
3889
|
|
|
5506
3890
|
// app/lib/attachments.ts
|
|
5507
|
-
import { randomUUID as
|
|
3891
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
5508
3892
|
import { mkdir as mkdir2, readFile, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
5509
3893
|
import { realpathSync } from "fs";
|
|
5510
|
-
import { resolve as
|
|
5511
|
-
var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ??
|
|
5512
|
-
var ATTACHMENTS_ROOT =
|
|
3894
|
+
import { resolve as resolve4, extname, basename } from "path";
|
|
3895
|
+
var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "../platform");
|
|
3896
|
+
var ATTACHMENTS_ROOT = resolve4(PLATFORM_ROOT2, "..", "data/uploads");
|
|
5513
3897
|
var SUPPORTED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
5514
3898
|
"image/jpeg",
|
|
5515
3899
|
"image/png",
|
|
@@ -5535,12 +3919,12 @@ function assertSupportedMime(mimeType) {
|
|
|
5535
3919
|
}
|
|
5536
3920
|
}
|
|
5537
3921
|
async function writeAttachment(scope, filename, mimeType, sizeBytes, buffer) {
|
|
5538
|
-
const attachmentId =
|
|
5539
|
-
const dir =
|
|
3922
|
+
const attachmentId = randomUUID3();
|
|
3923
|
+
const dir = resolve4(ATTACHMENTS_ROOT, scope, attachmentId);
|
|
5540
3924
|
await mkdir2(dir, { recursive: true });
|
|
5541
3925
|
const ext = extname(filename) || "";
|
|
5542
|
-
const storagePath =
|
|
5543
|
-
const metaPath =
|
|
3926
|
+
const storagePath = resolve4(dir, `${attachmentId}${ext}`);
|
|
3927
|
+
const metaPath = resolve4(dir, `${attachmentId}.meta.json`);
|
|
5544
3928
|
const meta = {
|
|
5545
3929
|
attachmentId,
|
|
5546
3930
|
scope,
|
|
@@ -5614,7 +3998,7 @@ async function storeGeneratedFile(accountId, filePath) {
|
|
|
5614
3998
|
`File exceeds the 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB).`
|
|
5615
3999
|
);
|
|
5616
4000
|
}
|
|
5617
|
-
const filename =
|
|
4001
|
+
const filename = basename(filePath);
|
|
5618
4002
|
const mimeType = detectMimeType(filePath);
|
|
5619
4003
|
const buffer = await readFile(filePath);
|
|
5620
4004
|
return writeAttachment(accountId, filename, mimeType, fileStat.size, buffer);
|
|
@@ -5623,7 +4007,7 @@ async function storeGeneratedFile(accountId, filePath) {
|
|
|
5623
4007
|
// app/lib/stt/voice-note.ts
|
|
5624
4008
|
import { writeFile as writeFile3, mkdtemp, rm } from "fs/promises";
|
|
5625
4009
|
import { tmpdir } from "os";
|
|
5626
|
-
import { join as
|
|
4010
|
+
import { join as join4 } from "path";
|
|
5627
4011
|
var TAG14 = "[voice]";
|
|
5628
4012
|
var AUDIO_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
5629
4013
|
"audio/ogg",
|
|
@@ -5661,9 +4045,9 @@ async function transcribeVoiceNote(file, source) {
|
|
|
5661
4045
|
let tempDir;
|
|
5662
4046
|
let tempPath;
|
|
5663
4047
|
try {
|
|
5664
|
-
tempDir = await mkdtemp(
|
|
4048
|
+
tempDir = await mkdtemp(join4(tmpdir(), "voice-"));
|
|
5665
4049
|
const ext = audioExtension(mimeType);
|
|
5666
|
-
tempPath =
|
|
4050
|
+
tempPath = join4(tempDir, `recording${ext}`);
|
|
5667
4051
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
5668
4052
|
await writeFile3(tempPath, buffer);
|
|
5669
4053
|
} catch (err) {
|
|
@@ -6218,16 +4602,16 @@ var group_default = app4;
|
|
|
6218
4602
|
|
|
6219
4603
|
// app/lib/access-gate.ts
|
|
6220
4604
|
import neo4j from "neo4j-driver";
|
|
6221
|
-
import { readFileSync as
|
|
6222
|
-
import { resolve as
|
|
6223
|
-
import { randomUUID as
|
|
6224
|
-
var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ??
|
|
4605
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
4606
|
+
import { resolve as resolve5 } from "path";
|
|
4607
|
+
import { randomUUID as randomUUID4, randomInt } from "crypto";
|
|
4608
|
+
var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "..");
|
|
6225
4609
|
var driver = null;
|
|
6226
4610
|
function readPassword() {
|
|
6227
4611
|
if (process.env.NEO4J_PASSWORD) return process.env.NEO4J_PASSWORD;
|
|
6228
|
-
const passwordFile =
|
|
4612
|
+
const passwordFile = resolve5(PLATFORM_ROOT3, "config/.neo4j-password");
|
|
6229
4613
|
try {
|
|
6230
|
-
return
|
|
4614
|
+
return readFileSync4(passwordFile, "utf-8").trim();
|
|
6231
4615
|
} catch {
|
|
6232
4616
|
throw new Error(
|
|
6233
4617
|
`Neo4j password not found. Expected at ${passwordFile} or in NEO4J_PASSWORD env var.`
|
|
@@ -6476,7 +4860,7 @@ async function setGrantPassword(grantId, passwordHash) {
|
|
|
6476
4860
|
}
|
|
6477
4861
|
}
|
|
6478
4862
|
async function generateNewMagicToken(grantId) {
|
|
6479
|
-
const token =
|
|
4863
|
+
const token = randomUUID4();
|
|
6480
4864
|
const session = getSession2();
|
|
6481
4865
|
try {
|
|
6482
4866
|
await session.run(
|
|
@@ -6538,19 +4922,19 @@ async function findActiveGrantByContact(contactValue, agentSlug, accountId) {
|
|
|
6538
4922
|
}
|
|
6539
4923
|
|
|
6540
4924
|
// app/lib/brevo-sms.ts
|
|
6541
|
-
import { readFileSync as
|
|
6542
|
-
import { dirname
|
|
6543
|
-
import { resolve as
|
|
6544
|
-
var BREVO_API_KEY_FILE =
|
|
4925
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync5, chmodSync } from "fs";
|
|
4926
|
+
import { dirname } from "path";
|
|
4927
|
+
import { resolve as resolve6 } from "path";
|
|
4928
|
+
var BREVO_API_KEY_FILE = resolve6(MAXY_DIR, ".brevo-api-key");
|
|
6545
4929
|
var BREVO_API_URL = "https://api.brevo.com/v3/transactionalSMS/sms";
|
|
6546
4930
|
var BREVO_TIMEOUT_MS = 1e4;
|
|
6547
4931
|
var BREVO_SENDER = "Maxy";
|
|
6548
4932
|
var platformRoot = process.env.MAXY_PLATFORM_ROOT;
|
|
6549
4933
|
if (platformRoot) {
|
|
6550
4934
|
try {
|
|
6551
|
-
const brandPath =
|
|
6552
|
-
if (
|
|
6553
|
-
const brand = JSON.parse(
|
|
4935
|
+
const brandPath = resolve6(platformRoot, "config", "brand.json");
|
|
4936
|
+
if (existsSync5(brandPath)) {
|
|
4937
|
+
const brand = JSON.parse(readFileSync5(brandPath, "utf-8"));
|
|
6554
4938
|
if (brand.productName) BREVO_SENDER = brand.productName;
|
|
6555
4939
|
}
|
|
6556
4940
|
} catch {
|
|
@@ -6558,7 +4942,7 @@ if (platformRoot) {
|
|
|
6558
4942
|
}
|
|
6559
4943
|
function readBrevoApiKey() {
|
|
6560
4944
|
try {
|
|
6561
|
-
const key =
|
|
4945
|
+
const key = readFileSync5(BREVO_API_KEY_FILE, "utf-8").trim();
|
|
6562
4946
|
if (!key) {
|
|
6563
4947
|
throw new Error(`Brevo API key file is empty: ${BREVO_API_KEY_FILE}`);
|
|
6564
4948
|
}
|
|
@@ -6573,7 +4957,7 @@ function readBrevoApiKey() {
|
|
|
6573
4957
|
}
|
|
6574
4958
|
}
|
|
6575
4959
|
function hasBrevoApiKey() {
|
|
6576
|
-
return
|
|
4960
|
+
return existsSync5(BREVO_API_KEY_FILE);
|
|
6577
4961
|
}
|
|
6578
4962
|
async function sendSms(recipient, content, opts) {
|
|
6579
4963
|
let apiKey;
|
|
@@ -6989,7 +5373,7 @@ app5.post("/send-otp", async (c) => {
|
|
|
6989
5373
|
var access_default = app5;
|
|
6990
5374
|
|
|
6991
5375
|
// server/routes/telegram.ts
|
|
6992
|
-
import { existsSync as
|
|
5376
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
6993
5377
|
import { timingSafeEqual } from "crypto";
|
|
6994
5378
|
|
|
6995
5379
|
// app/lib/telegram/access-control.ts
|
|
@@ -7026,8 +5410,8 @@ var TELEGRAM_API = "https://api.telegram.org";
|
|
|
7026
5410
|
function getWebhookSecret(botType) {
|
|
7027
5411
|
const filePath = botType === "admin" ? TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE : TELEGRAM_WEBHOOK_SECRET_FILE;
|
|
7028
5412
|
try {
|
|
7029
|
-
if (!
|
|
7030
|
-
const secret =
|
|
5413
|
+
if (!existsSync6(filePath)) return null;
|
|
5414
|
+
const secret = readFileSync6(filePath, "utf-8").trim();
|
|
7031
5415
|
return secret || null;
|
|
7032
5416
|
} catch {
|
|
7033
5417
|
return null;
|
|
@@ -7185,12 +5569,12 @@ app6.post("/webhook", async (c) => {
|
|
|
7185
5569
|
var telegram_default = app6;
|
|
7186
5570
|
|
|
7187
5571
|
// server/routes/whatsapp.ts
|
|
7188
|
-
import { join as
|
|
5572
|
+
import { join as join5, resolve as resolve7, basename as basename2 } from "path";
|
|
7189
5573
|
import { readFile as readFile2, stat as stat3 } from "fs/promises";
|
|
7190
|
-
import { realpathSync as realpathSync2, readdirSync as
|
|
5574
|
+
import { realpathSync as realpathSync2, readdirSync as readdirSync2, readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
7191
5575
|
|
|
7192
5576
|
// app/lib/whatsapp/login.ts
|
|
7193
|
-
import { randomUUID as
|
|
5577
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
7194
5578
|
var TAG17 = "[whatsapp:login]";
|
|
7195
5579
|
var ACTIVE_LOGIN_TTL_MS = 3 * 6e4;
|
|
7196
5580
|
var activeLogins = /* @__PURE__ */ new Map();
|
|
@@ -7293,8 +5677,8 @@ async function startLogin(opts) {
|
|
|
7293
5677
|
resetActiveLogin(accountId);
|
|
7294
5678
|
let resolveQr = null;
|
|
7295
5679
|
let rejectQr = null;
|
|
7296
|
-
const qrPromise = new Promise((
|
|
7297
|
-
resolveQr =
|
|
5680
|
+
const qrPromise = new Promise((resolve22, reject) => {
|
|
5681
|
+
resolveQr = resolve22;
|
|
7298
5682
|
rejectQr = reject;
|
|
7299
5683
|
});
|
|
7300
5684
|
const qrTimer = setTimeout(
|
|
@@ -7329,7 +5713,7 @@ async function startLogin(opts) {
|
|
|
7329
5713
|
const login = {
|
|
7330
5714
|
accountId,
|
|
7331
5715
|
authDir,
|
|
7332
|
-
id:
|
|
5716
|
+
id: randomUUID5(),
|
|
7333
5717
|
sock,
|
|
7334
5718
|
startedAt: Date.now(),
|
|
7335
5719
|
connected: false
|
|
@@ -7528,7 +5912,7 @@ app7.post("/login/start", async (c) => {
|
|
|
7528
5912
|
const body = await c.req.json().catch(() => ({}));
|
|
7529
5913
|
const accountId = validateAccountId(body.accountId);
|
|
7530
5914
|
const force = body.force ?? false;
|
|
7531
|
-
const authDir =
|
|
5915
|
+
const authDir = join5(MAXY_DIR, "credentials", "whatsapp", accountId);
|
|
7532
5916
|
const result = await startLogin({ accountId, authDir, force });
|
|
7533
5917
|
console.error(`${TAG18} login/start result account=${accountId} hasQr=${!!result.qrRaw}${result.selfPhone ? ` phone=${result.selfPhone}` : ""}`);
|
|
7534
5918
|
return c.json(result);
|
|
@@ -7688,17 +6072,17 @@ app7.post("/config", async (c) => {
|
|
|
7688
6072
|
return c.json(result, result.ok ? 200 : 400);
|
|
7689
6073
|
}
|
|
7690
6074
|
case "list-public-agents": {
|
|
7691
|
-
const agentsDir =
|
|
6075
|
+
const agentsDir = resolve7(account.accountDir, "agents");
|
|
7692
6076
|
const agents = [];
|
|
7693
|
-
if (
|
|
6077
|
+
if (existsSync7(agentsDir)) {
|
|
7694
6078
|
try {
|
|
7695
|
-
const entries =
|
|
6079
|
+
const entries = readdirSync2(agentsDir, { withFileTypes: true });
|
|
7696
6080
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
7697
6081
|
if (!entry.isDirectory() || entry.name === "admin") continue;
|
|
7698
|
-
const configPath2 =
|
|
7699
|
-
if (!
|
|
6082
|
+
const configPath2 = resolve7(agentsDir, entry.name, "config.json");
|
|
6083
|
+
if (!existsSync7(configPath2)) continue;
|
|
7700
6084
|
try {
|
|
7701
|
-
const config = JSON.parse(
|
|
6085
|
+
const config = JSON.parse(readFileSync7(configPath2, "utf-8"));
|
|
7702
6086
|
agents.push({ slug: entry.name, displayName: config.displayName ?? entry.name });
|
|
7703
6087
|
} catch {
|
|
7704
6088
|
console.error(`${TAG18} config action=list-public-agents error="failed to parse config.json for agent ${entry.name}" \u2014 skipping`);
|
|
@@ -7773,7 +6157,7 @@ app7.post("/send-document", async (c) => {
|
|
|
7773
6157
|
if (!maxyAccountId || !PLATFORM_ROOT4) {
|
|
7774
6158
|
return c.json({ error: "Cannot validate file path: missing account or platform context" }, 400);
|
|
7775
6159
|
}
|
|
7776
|
-
const accountDir =
|
|
6160
|
+
const accountDir = resolve7(PLATFORM_ROOT4, "..", "data/accounts", maxyAccountId);
|
|
7777
6161
|
let resolvedPath;
|
|
7778
6162
|
try {
|
|
7779
6163
|
resolvedPath = realpathSync2(filePath);
|
|
@@ -7797,7 +6181,7 @@ app7.post("/send-document", async (c) => {
|
|
|
7797
6181
|
return c.json({ error: `File exceeds 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)` }, 400);
|
|
7798
6182
|
}
|
|
7799
6183
|
const buffer = Buffer.from(await readFile2(resolvedPath));
|
|
7800
|
-
const filename =
|
|
6184
|
+
const filename = basename2(resolvedPath);
|
|
7801
6185
|
const mimetype = detectMimeType(resolvedPath);
|
|
7802
6186
|
const sock = getSocket(accountId);
|
|
7803
6187
|
if (!sock) {
|
|
@@ -7997,16 +6381,16 @@ var whatsapp_default = app7;
|
|
|
7997
6381
|
|
|
7998
6382
|
// server/routes/onboarding.ts
|
|
7999
6383
|
import { spawn, execFileSync } from "child_process";
|
|
8000
|
-
import { openSync
|
|
8001
|
-
import { resolve as
|
|
8002
|
-
import { createHash, randomUUID as
|
|
6384
|
+
import { openSync, closeSync, writeFileSync as writeFileSync4, writeSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync } from "fs";
|
|
6385
|
+
import { resolve as resolve8, dirname as dirname2 } from "path";
|
|
6386
|
+
import { createHash, randomUUID as randomUUID6 } from "crypto";
|
|
8003
6387
|
var PLATFORM_ROOT5 = process.env.MAXY_PLATFORM_ROOT || "";
|
|
8004
6388
|
function hashPin(pin) {
|
|
8005
6389
|
return createHash("sha256").update(pin).digest("hex");
|
|
8006
6390
|
}
|
|
8007
6391
|
function readUsersFile() {
|
|
8008
|
-
if (!
|
|
8009
|
-
const raw =
|
|
6392
|
+
if (!existsSync8(USERS_FILE)) return null;
|
|
6393
|
+
const raw = readFileSync8(USERS_FILE, "utf-8").trim();
|
|
8010
6394
|
if (!raw) return [];
|
|
8011
6395
|
return JSON.parse(raw);
|
|
8012
6396
|
}
|
|
@@ -8072,11 +6456,11 @@ app8.post("/claude-auth", async (c) => {
|
|
|
8072
6456
|
if (!vncReady) return c.json({ error: "VNC display failed to start" }, 500);
|
|
8073
6457
|
}
|
|
8074
6458
|
await ensureCdp(transport);
|
|
8075
|
-
|
|
6459
|
+
writeFileSync4(logPath("claude-auth"), "");
|
|
8076
6460
|
const chromiumWrapper = writeChromiumWrapper();
|
|
8077
6461
|
const x11Env = buildX11Env(chromiumWrapper, transport);
|
|
8078
6462
|
vncLog("claude-auth", { action: "start", transport });
|
|
8079
|
-
const claudeAuthLogFd =
|
|
6463
|
+
const claudeAuthLogFd = openSync(logPath("claude-auth"), "a");
|
|
8080
6464
|
const claudeProc = spawn("claude", ["auth", "login"], {
|
|
8081
6465
|
env: x11Env,
|
|
8082
6466
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -8085,7 +6469,7 @@ app8.post("/claude-auth", async (c) => {
|
|
|
8085
6469
|
const onClaudeOutput = (chunk) => writeSync(claudeAuthLogFd, chunk);
|
|
8086
6470
|
claudeProc.stdout?.on("data", onClaudeOutput);
|
|
8087
6471
|
claudeProc.stderr?.on("data", onClaudeOutput);
|
|
8088
|
-
claudeProc.once("close", () =>
|
|
6472
|
+
claudeProc.once("close", () => closeSync(claudeAuthLogFd));
|
|
8089
6473
|
await waitForAuthPage(2e4);
|
|
8090
6474
|
return c.json({ started: true, transport });
|
|
8091
6475
|
});
|
|
@@ -8116,22 +6500,22 @@ app8.post("/set-pin", async (c) => {
|
|
|
8116
6500
|
const hash = hashPin(body.pin);
|
|
8117
6501
|
const account = resolveAccount();
|
|
8118
6502
|
const existingOwnerUserId = account?.config.admins?.find((a) => a.role === "owner")?.userId;
|
|
8119
|
-
const userId = existingOwnerUserId ??
|
|
6503
|
+
const userId = existingOwnerUserId ?? randomUUID6();
|
|
8120
6504
|
if (existingOwnerUserId) {
|
|
8121
6505
|
console.log(`[set-pin] reusing existing owner userId=${userId.slice(0, 8)}\u2026 (change-PIN preserves identity)`);
|
|
8122
6506
|
} else {
|
|
8123
6507
|
console.log(`[set-pin] minted new userId=${userId.slice(0, 8)}\u2026 (first-time install)`);
|
|
8124
6508
|
}
|
|
8125
|
-
|
|
8126
|
-
|
|
6509
|
+
mkdirSync3(dirname2(USERS_FILE), { recursive: true });
|
|
6510
|
+
writeFileSync4(USERS_FILE, JSON.stringify([{ userId, pin: hash }]), { mode: 384 });
|
|
8127
6511
|
console.log(`[set-pin] wrote users.json: userId=${userId.slice(0, 8)}\u2026 hash=${hash.slice(0, 8)}\u2026`);
|
|
8128
6512
|
if (account) {
|
|
8129
6513
|
try {
|
|
8130
|
-
const config = JSON.parse(
|
|
6514
|
+
const config = JSON.parse(readFileSync8(`${account.accountDir}/account.json`, "utf-8"));
|
|
8131
6515
|
if (!config.admins) config.admins = [];
|
|
8132
6516
|
if (!config.admins.some((a) => a.userId === userId)) {
|
|
8133
6517
|
config.admins.push({ userId, role: "owner" });
|
|
8134
|
-
|
|
6518
|
+
writeFileSync4(`${account.accountDir}/account.json`, JSON.stringify(config, null, 2) + "\n");
|
|
8135
6519
|
console.log(`[set-pin] added userId=${userId.slice(0, 8)}\u2026 to account.json admins`);
|
|
8136
6520
|
}
|
|
8137
6521
|
} catch (err) {
|
|
@@ -8184,7 +6568,7 @@ app8.delete("/set-pin", async (c) => {
|
|
|
8184
6568
|
unlinkSync(USERS_FILE);
|
|
8185
6569
|
console.log(`[set-pin] cleared users.json (last entry removed): userId=${matchedUser.userId.slice(0, 8)}\u2026`);
|
|
8186
6570
|
} else {
|
|
8187
|
-
|
|
6571
|
+
writeFileSync4(USERS_FILE, JSON.stringify(remaining), { mode: 384 });
|
|
8188
6572
|
console.log(`[set-pin] removed entry from users.json: userId=${matchedUser.userId.slice(0, 8)}\u2026 remaining=${remaining.length}`);
|
|
8189
6573
|
}
|
|
8190
6574
|
return c.json({ ok: true });
|
|
@@ -8203,19 +6587,19 @@ app8.post("/skip", async (c) => {
|
|
|
8203
6587
|
}
|
|
8204
6588
|
const { accountId, accountDir } = account;
|
|
8205
6589
|
let agentName = "Maxy";
|
|
8206
|
-
const brandPath = PLATFORM_ROOT5 ?
|
|
8207
|
-
if (brandPath &&
|
|
6590
|
+
const brandPath = PLATFORM_ROOT5 ? resolve8(PLATFORM_ROOT5, "config", "brand.json") : "";
|
|
6591
|
+
if (brandPath && existsSync8(brandPath)) {
|
|
8208
6592
|
try {
|
|
8209
|
-
const brand = JSON.parse(
|
|
6593
|
+
const brand = JSON.parse(readFileSync8(brandPath, "utf-8"));
|
|
8210
6594
|
if (brand.productName) agentName = brand.productName;
|
|
8211
6595
|
} catch (err) {
|
|
8212
6596
|
console.error(`[onboarding-skip] brand.json read failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8213
6597
|
}
|
|
8214
6598
|
}
|
|
8215
|
-
const soulPath =
|
|
6599
|
+
const soulPath = resolve8(accountDir, "agents", "admin", "SOUL.md");
|
|
8216
6600
|
try {
|
|
8217
|
-
|
|
8218
|
-
|
|
6601
|
+
mkdirSync3(dirname2(soulPath), { recursive: true });
|
|
6602
|
+
writeFileSync4(soulPath, `You are ${agentName}, an AI operations manager.
|
|
8219
6603
|
`);
|
|
8220
6604
|
console.log(`[onboarding-skip] wrote SOUL.md: ${soulPath}`);
|
|
8221
6605
|
} catch (err) {
|
|
@@ -8259,9 +6643,9 @@ app8.post("/skip", async (c) => {
|
|
|
8259
6643
|
var onboarding_default = app8;
|
|
8260
6644
|
|
|
8261
6645
|
// server/routes/client-error.ts
|
|
8262
|
-
import { appendFileSync
|
|
8263
|
-
import { join as
|
|
8264
|
-
var CLIENT_ERRORS_LOG =
|
|
6646
|
+
import { appendFileSync, existsSync as existsSync9, renameSync, statSync as statSync2 } from "fs";
|
|
6647
|
+
import { join as join6 } from "path";
|
|
6648
|
+
var CLIENT_ERRORS_LOG = join6(LOG_DIR, "client-errors.log");
|
|
8265
6649
|
var MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
8266
6650
|
var MAX_BODY_SIZE = 8 * 1024;
|
|
8267
6651
|
var MAX_STACK_LEN = 2e3;
|
|
@@ -8304,10 +6688,10 @@ function stackHead(stack) {
|
|
|
8304
6688
|
}
|
|
8305
6689
|
function rotateIfNeeded() {
|
|
8306
6690
|
try {
|
|
8307
|
-
if (!
|
|
8308
|
-
const stats =
|
|
6691
|
+
if (!existsSync9(CLIENT_ERRORS_LOG)) return;
|
|
6692
|
+
const stats = statSync2(CLIENT_ERRORS_LOG);
|
|
8309
6693
|
if (stats.size < MAX_LOG_SIZE) return;
|
|
8310
|
-
|
|
6694
|
+
renameSync(CLIENT_ERRORS_LOG, CLIENT_ERRORS_LOG + ".1");
|
|
8311
6695
|
} catch (err) {
|
|
8312
6696
|
console.error(`[client-error] log rotation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8313
6697
|
}
|
|
@@ -8375,7 +6759,7 @@ app9.post("/", async (c) => {
|
|
|
8375
6759
|
const safe = typeof v === "string" ? truncate(v, 200) : typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(v).slice(0, 200);
|
|
8376
6760
|
return `${k}=${safe}`;
|
|
8377
6761
|
}).join(" ");
|
|
8378
|
-
const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set([
|
|
6762
|
+
const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set([]);
|
|
8379
6763
|
if (TAGGED_PREFIX_SOURCES.has(source)) {
|
|
8380
6764
|
console.log(
|
|
8381
6765
|
`[${source}] ts=${ts} ip=${ip} version=${version || "unknown"}${extra ? " " + extra : ""}`
|
|
@@ -8408,7 +6792,7 @@ app9.post("/", async (c) => {
|
|
|
8408
6792
|
tag: typeof body.tag === "string" ? truncate(body.tag, 32) : void 0,
|
|
8409
6793
|
status: typeof body.status === "number" ? body.status : void 0
|
|
8410
6794
|
};
|
|
8411
|
-
|
|
6795
|
+
appendFileSync(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
|
|
8412
6796
|
} catch (err) {
|
|
8413
6797
|
console.error(`[client-error] append failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8414
6798
|
}
|
|
@@ -8418,15 +6802,15 @@ app9.post("/", async (c) => {
|
|
|
8418
6802
|
var client_error_default = app9;
|
|
8419
6803
|
|
|
8420
6804
|
// server/routes/admin/session.ts
|
|
8421
|
-
import { readFileSync as
|
|
6805
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
|
|
8422
6806
|
import { createHash as createHash2 } from "crypto";
|
|
8423
6807
|
var deprecationLogged = /* @__PURE__ */ new Set();
|
|
8424
6808
|
function hashPin2(pin) {
|
|
8425
6809
|
return createHash2("sha256").update(pin).digest("hex");
|
|
8426
6810
|
}
|
|
8427
6811
|
function readUsersFile2() {
|
|
8428
|
-
if (!
|
|
8429
|
-
const raw =
|
|
6812
|
+
if (!existsSync10(USERS_FILE)) return null;
|
|
6813
|
+
const raw = readFileSync9(USERS_FILE, "utf-8").trim();
|
|
8430
6814
|
if (!raw) return [];
|
|
8431
6815
|
return JSON.parse(raw);
|
|
8432
6816
|
}
|
|
@@ -8443,7 +6827,7 @@ function stripLegacyNameField(users) {
|
|
|
8443
6827
|
}
|
|
8444
6828
|
}
|
|
8445
6829
|
try {
|
|
8446
|
-
|
|
6830
|
+
writeFileSync5(USERS_FILE, JSON.stringify(users), { mode: 384 });
|
|
8447
6831
|
} catch (err) {
|
|
8448
6832
|
console.error(`[admin-identity] users-json strip failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8449
6833
|
}
|
|
@@ -8597,49 +6981,16 @@ app10.post("/", async (c) => {
|
|
|
8597
6981
|
const payload = await createAdminSession(selected.accountId, selected.config.thinkingView, userId, userName, selected.role, avatar);
|
|
8598
6982
|
return c.json(payload);
|
|
8599
6983
|
});
|
|
8600
|
-
app10.post("/rebind", async (c) => {
|
|
8601
|
-
let body;
|
|
8602
|
-
try {
|
|
8603
|
-
body = await c.req.json();
|
|
8604
|
-
} catch {
|
|
8605
|
-
return c.json({ ok: false, error: "invalid_json" }, 400);
|
|
8606
|
-
}
|
|
8607
|
-
const sessionKey = typeof body.session_key === "string" ? body.session_key : "";
|
|
8608
|
-
const conversationId = typeof body.lastKnownConversationId === "string" ? body.lastKnownConversationId : "";
|
|
8609
|
-
if (!sessionKey || !conversationId) {
|
|
8610
|
-
return c.json({ ok: false, error: "session_key and lastKnownConversationId required" }, 400);
|
|
8611
|
-
}
|
|
8612
|
-
const sk8 = sessionKey.slice(0, 8);
|
|
8613
|
-
const cid8 = conversationId.slice(0, 8);
|
|
8614
|
-
const bridge = await tryCookieBridgeForConversation(c, sessionKey, conversationId);
|
|
8615
|
-
if (!bridge.ok) {
|
|
8616
|
-
console.log(`[admin/session/rebind] sessionKey=${sk8}\u2026 result=bridge-rejected reason=${bridge.reason} conversationId=${cid8}\u2026`);
|
|
8617
|
-
const status = bridge.reason === "conversation-not-found" ? 404 : bridge.reason === "account-mismatch" ? 403 : 401;
|
|
8618
|
-
return c.json({ ok: false, error: bridge.reason }, status);
|
|
8619
|
-
}
|
|
8620
|
-
const existing = getConversationIdForSession(sessionKey);
|
|
8621
|
-
if (existing && existing !== conversationId) {
|
|
8622
|
-
console.log(`[admin/session/rebind] sessionKey=${sk8}\u2026 result=conflict conversationId=${existing.slice(0, 8)}\u2026 requested=${cid8}\u2026`);
|
|
8623
|
-
return c.json({ ok: false, error: "conflict", conversationId: existing }, 409);
|
|
8624
|
-
}
|
|
8625
|
-
const bound = setConversationIdForSession(sessionKey, conversationId);
|
|
8626
|
-
if (!bound) {
|
|
8627
|
-
console.error(`[admin/session/rebind] sessionKey=${sk8}\u2026 result=bind-failed conversationId=${cid8}\u2026 (post-bridge session-store missing \u2014 programmer error)`);
|
|
8628
|
-
return c.json({ ok: false, error: "bind_failed" }, 500);
|
|
8629
|
-
}
|
|
8630
|
-
console.log(`[admin/session/rebind] sessionKey=${sk8}\u2026 result=ok conversationId=${cid8}\u2026`);
|
|
8631
|
-
return c.json({ ok: true, conversationId });
|
|
8632
|
-
});
|
|
8633
6984
|
var session_default2 = app10;
|
|
8634
6985
|
|
|
8635
6986
|
// server/routes/admin/chat.ts
|
|
8636
|
-
import { resolve as
|
|
8637
|
-
import { appendFileSync as
|
|
6987
|
+
import { resolve as resolve9 } from "path";
|
|
6988
|
+
import { appendFileSync as appendFileSync3 } from "fs";
|
|
8638
6989
|
|
|
8639
6990
|
// app/lib/script-stream-tailer.ts
|
|
8640
6991
|
import * as childProcess from "child_process";
|
|
8641
|
-
import { appendFileSync as
|
|
8642
|
-
import { dirname as
|
|
6992
|
+
import { appendFileSync as appendFileSync2, createReadStream as createReadStream2, mkdirSync as mkdirSync4, statSync as statSync3 } from "fs";
|
|
6993
|
+
import { dirname as dirname3 } from "path";
|
|
8643
6994
|
import { StringDecoder } from "string_decoder";
|
|
8644
6995
|
var SCRIPT_STREAM_RE = /^\[([^\]]+)\] \[script:([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] (.*)$/;
|
|
8645
6996
|
function parseLine(line) {
|
|
@@ -8657,7 +7008,7 @@ function startScriptStreamTailer(opts) {
|
|
|
8657
7008
|
const { path: path2, onEvent, onError } = opts;
|
|
8658
7009
|
let offset;
|
|
8659
7010
|
try {
|
|
8660
|
-
offset =
|
|
7011
|
+
offset = statSync3(path2).size;
|
|
8661
7012
|
} catch {
|
|
8662
7013
|
offset = 0;
|
|
8663
7014
|
}
|
|
@@ -8676,7 +7027,7 @@ function startScriptStreamTailer(opts) {
|
|
|
8676
7027
|
try {
|
|
8677
7028
|
let size;
|
|
8678
7029
|
try {
|
|
8679
|
-
size =
|
|
7030
|
+
size = statSync3(path2).size;
|
|
8680
7031
|
} catch {
|
|
8681
7032
|
return;
|
|
8682
7033
|
}
|
|
@@ -8748,8 +7099,8 @@ function writeRouteMilestone(streamLogPath, scope, line) {
|
|
|
8748
7099
|
}
|
|
8749
7100
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
8750
7101
|
try {
|
|
8751
|
-
|
|
8752
|
-
|
|
7102
|
+
mkdirSync4(dirname3(streamLogPath), { recursive: true });
|
|
7103
|
+
appendFileSync2(streamLogPath, `[${ts}] [script:${scope}] ${line}
|
|
8753
7104
|
`);
|
|
8754
7105
|
} catch (err) {
|
|
8755
7106
|
console.error(
|
|
@@ -8924,7 +7275,7 @@ var app11 = new Hono();
|
|
|
8924
7275
|
app11.post("/cancel", requireAdminSession, async (c) => {
|
|
8925
7276
|
const session_key = c.var.sessionKey;
|
|
8926
7277
|
try {
|
|
8927
|
-
const { interruptClient: interruptClient2 } = await import("./client-pool-
|
|
7278
|
+
const { interruptClient: interruptClient2 } = await import("./client-pool-GBY5I2KQ.js");
|
|
8928
7279
|
await interruptClient2(session_key);
|
|
8929
7280
|
return c.json({ ok: true });
|
|
8930
7281
|
} catch (err) {
|
|
@@ -9088,10 +7439,10 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9088
7439
|
function resolveTeeStreamLogPath() {
|
|
9089
7440
|
const liveConvId = getConversationIdForSession(session_key);
|
|
9090
7441
|
const key = liveConvId ?? preflushStreamLogKey(session_key);
|
|
9091
|
-
return
|
|
7442
|
+
return resolve9(account.accountDir, "logs", `claude-agent-stream-${key}.log`);
|
|
9092
7443
|
}
|
|
9093
7444
|
try {
|
|
9094
|
-
|
|
7445
|
+
appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [chat-route-version=task606-tee-path-resolve] sessionKey=${session_key.slice(0, 12)}\u2026
|
|
9095
7446
|
`);
|
|
9096
7447
|
} catch {
|
|
9097
7448
|
}
|
|
@@ -9100,7 +7451,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9100
7451
|
incoming.on("close", () => {
|
|
9101
7452
|
const tsClose = (/* @__PURE__ */ new Date()).toISOString();
|
|
9102
7453
|
try {
|
|
9103
|
-
|
|
7454
|
+
appendFileSync3(resolveTeeStreamLogPath(), `[${tsClose}] [incoming-close] sessionKey=${session_key.slice(0, 12)}\u2026 complete=${incoming.complete}
|
|
9104
7455
|
`);
|
|
9105
7456
|
} catch {
|
|
9106
7457
|
}
|
|
@@ -9108,7 +7459,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9108
7459
|
console.error(`[${tsClose}] [incoming-close] DISCONNECT sessionKey=${session_key.slice(0, 12)}\u2026`);
|
|
9109
7460
|
interruptClient(session_key).catch((err) => {
|
|
9110
7461
|
try {
|
|
9111
|
-
|
|
7462
|
+
appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] interrupt-failed: ${err instanceof Error ? err.message : String(err)}
|
|
9112
7463
|
`);
|
|
9113
7464
|
} catch {
|
|
9114
7465
|
}
|
|
@@ -9117,7 +7468,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9117
7468
|
});
|
|
9118
7469
|
} else {
|
|
9119
7470
|
try {
|
|
9120
|
-
|
|
7471
|
+
appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] UNAVAILABLE \u2014 c.env.incoming is not an EventEmitter
|
|
9121
7472
|
`);
|
|
9122
7473
|
} catch {
|
|
9123
7474
|
}
|
|
@@ -9129,7 +7480,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9129
7480
|
} catch {
|
|
9130
7481
|
}
|
|
9131
7482
|
try {
|
|
9132
|
-
|
|
7483
|
+
appendFileSync3(resolveTeeStreamLogPath(), line);
|
|
9133
7484
|
} catch {
|
|
9134
7485
|
}
|
|
9135
7486
|
return true;
|
|
@@ -9190,7 +7541,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9190
7541
|
try {
|
|
9191
7542
|
registerAdminSSE(sseEntry);
|
|
9192
7543
|
if (sseConvId) {
|
|
9193
|
-
const streamLogPath =
|
|
7544
|
+
const streamLogPath = resolve9(account.accountDir, "logs", `claude-agent-stream-${sseConvId}.log`);
|
|
9194
7545
|
tailer = startScriptStreamTailer({
|
|
9195
7546
|
path: streamLogPath,
|
|
9196
7547
|
onEvent: (event) => {
|
|
@@ -9319,22 +7670,22 @@ app12.post("/", requireAdminSession, async (c) => {
|
|
|
9319
7670
|
var compact_default = app12;
|
|
9320
7671
|
|
|
9321
7672
|
// server/routes/admin/logs.ts
|
|
9322
|
-
import { existsSync as
|
|
9323
|
-
import { resolve as
|
|
7673
|
+
import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync4 } from "fs";
|
|
7674
|
+
import { resolve as resolve10, basename as basename3 } from "path";
|
|
9324
7675
|
|
|
9325
7676
|
// app/lib/logs-read-resolve.ts
|
|
9326
|
-
import { existsSync as
|
|
9327
|
-
import { join as
|
|
7677
|
+
import { existsSync as existsSync11 } from "fs";
|
|
7678
|
+
import { join as join7 } from "path";
|
|
9328
7679
|
function resolveConversationLogPaths(fullFilename, preflushFilename, logDirs) {
|
|
9329
7680
|
const tried = [fullFilename, preflushFilename];
|
|
9330
7681
|
const hits = [];
|
|
9331
7682
|
const stalePreflushPaths = [];
|
|
9332
7683
|
for (const dir of logDirs) {
|
|
9333
|
-
const fullPath =
|
|
9334
|
-
if (
|
|
7684
|
+
const fullPath = join7(dir, fullFilename);
|
|
7685
|
+
if (existsSync11(fullPath)) {
|
|
9335
7686
|
hits.push({ path: fullPath, shape: "full", dir });
|
|
9336
|
-
const preflushSibling =
|
|
9337
|
-
if (
|
|
7687
|
+
const preflushSibling = join7(dir, preflushFilename);
|
|
7688
|
+
if (existsSync11(preflushSibling)) {
|
|
9338
7689
|
stalePreflushPaths.push(preflushSibling);
|
|
9339
7690
|
}
|
|
9340
7691
|
}
|
|
@@ -9343,8 +7694,8 @@ function resolveConversationLogPaths(fullFilename, preflushFilename, logDirs) {
|
|
|
9343
7694
|
return { hits, stalePreflushPaths, tried };
|
|
9344
7695
|
}
|
|
9345
7696
|
for (const dir of logDirs) {
|
|
9346
|
-
const preflushPath =
|
|
9347
|
-
if (
|
|
7697
|
+
const preflushPath = join7(dir, preflushFilename);
|
|
7698
|
+
if (existsSync11(preflushPath)) {
|
|
9348
7699
|
hits.push({ path: preflushPath, shape: "preflush", dir });
|
|
9349
7700
|
}
|
|
9350
7701
|
}
|
|
@@ -9364,19 +7715,19 @@ app13.get("/", async (c) => {
|
|
|
9364
7715
|
const sessionKeyParam = c.req.query("sessionKey");
|
|
9365
7716
|
const download = c.req.query("download") === "1";
|
|
9366
7717
|
const account = resolveAccount();
|
|
9367
|
-
const
|
|
7718
|
+
const accountLogDir = account ? resolve10(account.accountDir, "logs") : null;
|
|
9368
7719
|
const logDirs = [];
|
|
9369
|
-
if (
|
|
7720
|
+
if (accountLogDir) logDirs.push(accountLogDir);
|
|
9370
7721
|
logDirs.push(LOG_DIR);
|
|
9371
7722
|
if (fileParam) {
|
|
9372
|
-
const safe =
|
|
7723
|
+
const safe = basename3(fileParam);
|
|
9373
7724
|
const searched = [];
|
|
9374
7725
|
for (const dir of logDirs) {
|
|
9375
|
-
const filePath =
|
|
7726
|
+
const filePath = resolve10(dir, safe);
|
|
9376
7727
|
searched.push(filePath);
|
|
9377
7728
|
try {
|
|
9378
|
-
const buffer =
|
|
9379
|
-
const onDiskBytes =
|
|
7729
|
+
const buffer = readFileSync10(filePath);
|
|
7730
|
+
const onDiskBytes = statSync4(filePath).size;
|
|
9380
7731
|
const headers = {
|
|
9381
7732
|
"Content-Type": "text/plain; charset=utf-8",
|
|
9382
7733
|
"Content-Length": String(buffer.byteLength)
|
|
@@ -9448,9 +7799,9 @@ app13.get("/", async (c) => {
|
|
|
9448
7799
|
const hit = hits[0];
|
|
9449
7800
|
console.info(`[admin/logs] resolved sessionKey=${sessionKeySlice} conversationId=${conversationIdSlice} shape=${hit.shape} stalePreflushCount=${stalePreflushCount}`);
|
|
9450
7801
|
try {
|
|
9451
|
-
const filename =
|
|
7802
|
+
const filename = basename3(hit.path);
|
|
9452
7803
|
if (stalePreflushCount > 0 && !download) {
|
|
9453
|
-
const content =
|
|
7804
|
+
const content = readFileSync10(hit.path, "utf-8");
|
|
9454
7805
|
return c.json({
|
|
9455
7806
|
log: content,
|
|
9456
7807
|
filename,
|
|
@@ -9458,8 +7809,8 @@ app13.get("/", async (c) => {
|
|
|
9458
7809
|
warnings: stalePreflushPaths.map((path2) => ({ kind: "stale-preflush", path: path2 }))
|
|
9459
7810
|
});
|
|
9460
7811
|
}
|
|
9461
|
-
const buffer =
|
|
9462
|
-
const onDiskBytes =
|
|
7812
|
+
const buffer = readFileSync10(hit.path);
|
|
7813
|
+
const onDiskBytes = statSync4(hit.path).size;
|
|
9463
7814
|
const headers = {
|
|
9464
7815
|
"Content-Type": "text/plain; charset=utf-8",
|
|
9465
7816
|
"Content-Length": String(buffer.byteLength)
|
|
@@ -9492,19 +7843,19 @@ app13.get("/", async (c) => {
|
|
|
9492
7843
|
const seen = /* @__PURE__ */ new Set();
|
|
9493
7844
|
const logs = {};
|
|
9494
7845
|
for (const dir of logDirs) {
|
|
9495
|
-
if (!
|
|
7846
|
+
if (!existsSync12(dir)) continue;
|
|
9496
7847
|
let files;
|
|
9497
7848
|
try {
|
|
9498
|
-
files =
|
|
7849
|
+
files = readdirSync3(dir).filter((f) => f.endsWith(".log"));
|
|
9499
7850
|
} catch (err) {
|
|
9500
7851
|
const reason = err instanceof Error ? err.message : String(err);
|
|
9501
7852
|
console.warn(`[admin/logs] readdir-fail dir=${dir} reason=${reason}`);
|
|
9502
7853
|
continue;
|
|
9503
7854
|
}
|
|
9504
|
-
files.filter((f) => !seen.has(f)).map((f) => ({ name: f, mtime:
|
|
7855
|
+
files.filter((f) => !seen.has(f)).map((f) => ({ name: f, mtime: statSync4(resolve10(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime).forEach(({ name }) => {
|
|
9505
7856
|
seen.add(name);
|
|
9506
7857
|
try {
|
|
9507
|
-
const content =
|
|
7858
|
+
const content = readFileSync10(resolve10(dir, name));
|
|
9508
7859
|
const tail = content.length > TAIL_BYTES ? content.subarray(content.length - TAIL_BYTES).toString("utf-8") : content.toString("utf-8");
|
|
9509
7860
|
logs[name] = tail.trim() || "(empty)";
|
|
9510
7861
|
} catch (err) {
|
|
@@ -9543,8 +7894,8 @@ var claude_info_default = app14;
|
|
|
9543
7894
|
|
|
9544
7895
|
// server/routes/admin/attachment.ts
|
|
9545
7896
|
import { readFile as readFile3, readdir } from "fs/promises";
|
|
9546
|
-
import { existsSync as
|
|
9547
|
-
import { resolve as
|
|
7897
|
+
import { existsSync as existsSync13 } from "fs";
|
|
7898
|
+
import { resolve as resolve11 } from "path";
|
|
9548
7899
|
var app15 = new Hono();
|
|
9549
7900
|
app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
9550
7901
|
const attachmentId = c.req.param("attachmentId");
|
|
@@ -9556,12 +7907,12 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
|
9556
7907
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(attachmentId)) {
|
|
9557
7908
|
return new Response("Not found", { status: 404 });
|
|
9558
7909
|
}
|
|
9559
|
-
const dir =
|
|
9560
|
-
if (!
|
|
7910
|
+
const dir = resolve11(ATTACHMENTS_ROOT, accountId, attachmentId);
|
|
7911
|
+
if (!existsSync13(dir)) {
|
|
9561
7912
|
return new Response("Not found", { status: 404 });
|
|
9562
7913
|
}
|
|
9563
|
-
const metaPath =
|
|
9564
|
-
if (!
|
|
7914
|
+
const metaPath = resolve11(dir, `${attachmentId}.meta.json`);
|
|
7915
|
+
if (!existsSync13(metaPath)) {
|
|
9565
7916
|
return new Response("Not found", { status: 404 });
|
|
9566
7917
|
}
|
|
9567
7918
|
let meta;
|
|
@@ -9575,7 +7926,7 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
|
9575
7926
|
if (!dataFile) {
|
|
9576
7927
|
return new Response("Not found", { status: 404 });
|
|
9577
7928
|
}
|
|
9578
|
-
const filePath =
|
|
7929
|
+
const filePath = resolve11(dir, dataFile);
|
|
9579
7930
|
const buffer = await readFile3(filePath);
|
|
9580
7931
|
return new Response(new Uint8Array(buffer), {
|
|
9581
7932
|
headers: {
|
|
@@ -9588,24 +7939,24 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
|
9588
7939
|
var attachment_default = app15;
|
|
9589
7940
|
|
|
9590
7941
|
// server/routes/admin/agents.ts
|
|
9591
|
-
import { resolve as
|
|
9592
|
-
import { readdirSync as
|
|
7942
|
+
import { resolve as resolve12 } from "path";
|
|
7943
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync11, existsSync as existsSync14, rmSync } from "fs";
|
|
9593
7944
|
var app16 = new Hono();
|
|
9594
7945
|
app16.get("/", (c) => {
|
|
9595
7946
|
const account = resolveAccount();
|
|
9596
7947
|
if (!account) return c.json({ agents: [] });
|
|
9597
|
-
const agentsDir =
|
|
9598
|
-
if (!
|
|
7948
|
+
const agentsDir = resolve12(account.accountDir, "agents");
|
|
7949
|
+
if (!existsSync14(agentsDir)) return c.json({ agents: [] });
|
|
9599
7950
|
const agents = [];
|
|
9600
7951
|
try {
|
|
9601
|
-
const entries =
|
|
7952
|
+
const entries = readdirSync4(agentsDir, { withFileTypes: true });
|
|
9602
7953
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
9603
7954
|
if (!entry.isDirectory()) continue;
|
|
9604
7955
|
if (entry.name === "admin") continue;
|
|
9605
|
-
const configPath2 =
|
|
9606
|
-
if (!
|
|
7956
|
+
const configPath2 = resolve12(agentsDir, entry.name, "config.json");
|
|
7957
|
+
if (!existsSync14(configPath2)) continue;
|
|
9607
7958
|
try {
|
|
9608
|
-
const config = JSON.parse(
|
|
7959
|
+
const config = JSON.parse(readFileSync11(configPath2, "utf-8"));
|
|
9609
7960
|
agents.push({
|
|
9610
7961
|
slug: entry.name,
|
|
9611
7962
|
displayName: config.displayName ?? entry.name,
|
|
@@ -9631,8 +7982,8 @@ app16.delete("/:slug", async (c) => {
|
|
|
9631
7982
|
if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
|
|
9632
7983
|
return c.json({ error: "Invalid agent slug" }, 400);
|
|
9633
7984
|
}
|
|
9634
|
-
const agentDir =
|
|
9635
|
-
if (!
|
|
7985
|
+
const agentDir = resolve12(account.accountDir, "agents", slug);
|
|
7986
|
+
if (!existsSync14(agentDir)) {
|
|
9636
7987
|
return c.json({ error: "Agent not found" }, 404);
|
|
9637
7988
|
}
|
|
9638
7989
|
try {
|
|
@@ -9661,8 +8012,8 @@ app16.post("/:slug/project", async (c) => {
|
|
|
9661
8012
|
if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
|
|
9662
8013
|
return c.json({ error: "Invalid agent slug" }, 400);
|
|
9663
8014
|
}
|
|
9664
|
-
const agentDir =
|
|
9665
|
-
if (!
|
|
8015
|
+
const agentDir = resolve12(account.accountDir, "agents", slug);
|
|
8016
|
+
if (!existsSync14(agentDir)) {
|
|
9666
8017
|
return c.json({ error: "Agent not found on disk" }, 404);
|
|
9667
8018
|
}
|
|
9668
8019
|
try {
|
|
@@ -9678,7 +8029,7 @@ var agents_default = app16;
|
|
|
9678
8029
|
// server/routes/admin/sessions.ts
|
|
9679
8030
|
import crypto2 from "crypto";
|
|
9680
8031
|
import { resolve as resolvePath } from "path";
|
|
9681
|
-
import { appendFileSync as
|
|
8032
|
+
import { appendFileSync as appendFileSync4, existsSync as existsSync15 } from "fs";
|
|
9682
8033
|
function validateAndShapeAttachments(raws, conversationAccountId, conversationId, messageId, streamLogPath) {
|
|
9683
8034
|
const chips = [];
|
|
9684
8035
|
let valid = 0;
|
|
@@ -9687,11 +8038,11 @@ function validateAndShapeAttachments(raws, conversationAccountId, conversationId
|
|
|
9687
8038
|
let reason = null;
|
|
9688
8039
|
if (!a.attachmentId || !a.filename || !a.mimeType || !a.storagePath) reason = "schema-fail";
|
|
9689
8040
|
else if (a.accountId !== conversationAccountId) reason = "account-mismatch";
|
|
9690
|
-
else if (!
|
|
8041
|
+
else if (!existsSync15(a.storagePath)) reason = "missing-file";
|
|
9691
8042
|
if (reason) {
|
|
9692
8043
|
invalid++;
|
|
9693
8044
|
try {
|
|
9694
|
-
|
|
8045
|
+
appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate-invalid] conversationId=${conversationId.slice(0, 8)} messageId=${messageId.slice(0, 8)} attachmentId=${(a.attachmentId || "").slice(0, 8)} reason=${reason}
|
|
9695
8046
|
`);
|
|
9696
8047
|
} catch {
|
|
9697
8048
|
}
|
|
@@ -9754,7 +8105,7 @@ function reconstructAssistantEvents(content, components, conversationId, message
|
|
|
9754
8105
|
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate-invalid] conversationId=${conversationId.slice(0, 8)} messageId=${messageId.slice(0, 8)} name=${c.name || "<unnamed>"} reason=${invalidReason}
|
|
9755
8106
|
`;
|
|
9756
8107
|
try {
|
|
9757
|
-
|
|
8108
|
+
appendFileSync4(streamLogPath, line);
|
|
9758
8109
|
} catch {
|
|
9759
8110
|
}
|
|
9760
8111
|
continue;
|
|
@@ -9860,13 +8211,14 @@ app17.delete("/:id", requireAdminSession, async (c) => {
|
|
|
9860
8211
|
return c.json({ error: "Failed to delete session" }, 500);
|
|
9861
8212
|
}
|
|
9862
8213
|
});
|
|
9863
|
-
app17.
|
|
8214
|
+
app17.post("/:id/resume", async (c) => {
|
|
9864
8215
|
const conversationId = c.req.param("id");
|
|
9865
8216
|
const sessionKey = c.req.query("session_key") ?? "";
|
|
9866
8217
|
if (!sessionKey) {
|
|
9867
8218
|
console.error(`[session] middleware-reject status=400 code=session-missing reason="session_key required" path=${c.req.path}`);
|
|
9868
8219
|
return c.json({ error: "session_key required", code: "session-missing" }, 400);
|
|
9869
8220
|
}
|
|
8221
|
+
let bridged = false;
|
|
9870
8222
|
let result = validateSession(sessionKey, "admin");
|
|
9871
8223
|
if (!result.ok) {
|
|
9872
8224
|
if (result.reason === "session-not-registered") {
|
|
@@ -9879,6 +8231,7 @@ app17.get("/:id/messages", async (c) => {
|
|
|
9879
8231
|
console.error(`[session] middleware-reject status=401 code=session-not-registered reason="cookie-bridge-rejected:${bridge.reason}" path=${c.req.path} sessionKey=${tail}\u2026`);
|
|
9880
8232
|
return c.json({ error: "Invalid or expired admin session", code: "session-not-registered" }, 401);
|
|
9881
8233
|
}
|
|
8234
|
+
bridged = true;
|
|
9882
8235
|
result = validateSession(sessionKey, "admin");
|
|
9883
8236
|
if (!result.ok) {
|
|
9884
8237
|
const tail = sessionKey.slice(0, 8);
|
|
@@ -9893,34 +8246,12 @@ app17.get("/:id/messages", async (c) => {
|
|
|
9893
8246
|
}
|
|
9894
8247
|
}
|
|
9895
8248
|
const accountId = getAccountIdForSession(sessionKey);
|
|
9896
|
-
if (!accountId) return c.json({ error: "Account not found for session" }, 401);
|
|
9897
|
-
const owned = await verifyConversationOwnership(conversationId, accountId);
|
|
9898
|
-
if (!owned) return c.json({ error: "Conversation not found" }, 404);
|
|
9899
|
-
try {
|
|
9900
|
-
const messages = await getRecentMessages(conversationId, 50);
|
|
9901
|
-
const sanitised = messages.map((m) => ({
|
|
9902
|
-
...m,
|
|
9903
|
-
attachments: m.attachments.map((a) => ({
|
|
9904
|
-
attachmentId: a.attachmentId,
|
|
9905
|
-
filename: a.filename,
|
|
9906
|
-
mimeType: a.mimeType,
|
|
9907
|
-
sizeBytes: a.sizeBytes,
|
|
9908
|
-
ordinal: a.ordinal
|
|
9909
|
-
}))
|
|
9910
|
-
}));
|
|
9911
|
-
return c.json({ messages: sanitised });
|
|
9912
|
-
} catch (err) {
|
|
9913
|
-
console.error(`[sessions-messages] Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
9914
|
-
return c.json({ error: "Failed to fetch messages" }, 500);
|
|
9915
|
-
}
|
|
9916
|
-
});
|
|
9917
|
-
app17.post("/:id/resume", requireAdminSession, async (c) => {
|
|
9918
|
-
const conversationId = c.req.param("id");
|
|
9919
|
-
const sessionKey = c.var.sessionKey;
|
|
9920
|
-
const accountId = getAccountIdForSession(sessionKey);
|
|
9921
8249
|
const userId = getUserIdForSession(sessionKey);
|
|
9922
|
-
if (!accountId
|
|
9923
|
-
return c.json({ error: "Session missing account
|
|
8250
|
+
if (!accountId) {
|
|
8251
|
+
return c.json({ error: "Session missing account context" }, 400);
|
|
8252
|
+
}
|
|
8253
|
+
if (!userId && !bridged) {
|
|
8254
|
+
return c.json({ error: "Session missing user context" }, 400);
|
|
9924
8255
|
}
|
|
9925
8256
|
const updatedAt = await verifyAndGetConversationUpdatedAt(conversationId, accountId);
|
|
9926
8257
|
if (updatedAt === null) return c.json({ error: "Conversation not found" }, 404);
|
|
@@ -9975,21 +8306,22 @@ app17.post("/:id/resume", requireAdminSession, async (c) => {
|
|
|
9975
8306
|
});
|
|
9976
8307
|
const textRuns = rehydrated.reduce((n, m) => n + (m.events?.filter((e) => e.type === "text").length ?? 0), 0);
|
|
9977
8308
|
const userMessageCount = rehydrated.filter((m) => m.role !== "assistant").length;
|
|
8309
|
+
const reason = bridged ? "post-restart" : "page-refresh";
|
|
9978
8310
|
try {
|
|
9979
|
-
|
|
8311
|
+
appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [admin-resume] reason=${reason} sessionKey=${sessionKey.slice(0, 8)} conversationId=${conversationId.slice(0, 8)} ${tag} loadedMessages=${messages.length} componentCount=${totalComponents} userAttachmentCount=${totalAttachments}
|
|
9980
8312
|
`);
|
|
9981
8313
|
if (totalComponents > 0) {
|
|
9982
|
-
|
|
8314
|
+
appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate] conversationId=${conversationId.slice(0, 8)} count=${totalComponents} valid=${totalValid} invalid=${totalInvalid} textRuns=${textRuns}
|
|
9983
8315
|
`);
|
|
9984
8316
|
}
|
|
9985
8317
|
if (totalAttachments > 0 || totalAttachmentInvalid > 0) {
|
|
9986
|
-
|
|
8318
|
+
appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate] conversationId=${conversationId.slice(0, 8)} userMessages=${userMessageCount} attachments=${totalAttachments} invalid=${totalAttachmentInvalid}
|
|
9987
8319
|
`);
|
|
9988
8320
|
}
|
|
9989
8321
|
} catch {
|
|
9990
8322
|
}
|
|
9991
8323
|
const age = formatAge(updatedAt);
|
|
9992
|
-
console.log(`[admin-resume] ${(/* @__PURE__ */ new Date()).toISOString()} conversationId=${conversationId.slice(0, 8)}\u2026 age=${age} loaded=${messages.length} messages ${tag} components=${totalComponents} attachments=${totalAttachments} sessionKey=${sessionKey.slice(0, 8)}\u2026`);
|
|
8324
|
+
console.log(`[admin-resume] ${(/* @__PURE__ */ new Date()).toISOString()} reason=${reason} conversationId=${conversationId.slice(0, 8)}\u2026 age=${age} loaded=${messages.length} messages ${tag} components=${totalComponents} attachments=${totalAttachments} sessionKey=${sessionKey.slice(0, 8)}\u2026`);
|
|
9993
8325
|
return c.json({ conversationId, messages: rehydrated });
|
|
9994
8326
|
});
|
|
9995
8327
|
app17.post("/:id/label", requireAdminSession, async (c) => {
|
|
@@ -10116,13 +8448,13 @@ async function cdpNavigateNewTab(url, opts = {}) {
|
|
|
10116
8448
|
// server/routes/admin/device-browser.ts
|
|
10117
8449
|
var app19 = new Hono();
|
|
10118
8450
|
app19.post("/navigate", async (c) => {
|
|
10119
|
-
const
|
|
8451
|
+
const TAG19 = "[device-url:click]";
|
|
10120
8452
|
let body;
|
|
10121
8453
|
try {
|
|
10122
8454
|
body = await c.req.json();
|
|
10123
8455
|
} catch (err) {
|
|
10124
8456
|
const detail = err instanceof Error ? err.message : String(err);
|
|
10125
|
-
console.error(`${
|
|
8457
|
+
console.error(`${TAG19} reject reason=body-not-json detail=${detail} browser=fallback navigateResult=error`);
|
|
10126
8458
|
return c.json(
|
|
10127
8459
|
{ ok: false, navigateResult: "error", browser: "fallback", detail: "Request body was not valid JSON" },
|
|
10128
8460
|
400
|
|
@@ -10132,7 +8464,7 @@ app19.post("/navigate", async (c) => {
|
|
|
10132
8464
|
const intent = typeof body.intent === "string" ? body.intent : "";
|
|
10133
8465
|
const hostname2 = typeof body.hostname === "string" ? body.hostname : "";
|
|
10134
8466
|
if (!url) {
|
|
10135
|
-
console.error(`${
|
|
8467
|
+
console.error(`${TAG19} reject reason=missing-url intent=${JSON.stringify(intent)} browser=fallback navigateResult=error`);
|
|
10136
8468
|
return c.json(
|
|
10137
8469
|
{ ok: false, navigateResult: "error", browser: "fallback", detail: "url field is required" },
|
|
10138
8470
|
400
|
|
@@ -10142,7 +8474,7 @@ app19.post("/navigate", async (c) => {
|
|
|
10142
8474
|
try {
|
|
10143
8475
|
parsed = new URL(url);
|
|
10144
8476
|
} catch {
|
|
10145
|
-
console.error(`${
|
|
8477
|
+
console.error(`${TAG19} reject reason=url-malformed intent=${JSON.stringify(intent)} url=${url} browser=fallback navigateResult=error`);
|
|
10146
8478
|
return c.json(
|
|
10147
8479
|
{ ok: false, navigateResult: "error", browser: "fallback", detail: "url is not a valid URL" },
|
|
10148
8480
|
400
|
|
@@ -10150,7 +8482,7 @@ app19.post("/navigate", async (c) => {
|
|
|
10150
8482
|
}
|
|
10151
8483
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
10152
8484
|
console.error(
|
|
10153
|
-
`${
|
|
8485
|
+
`${TAG19} reject reason=scheme-not-allowed scheme=${parsed.protocol} intent=${JSON.stringify(intent)} browser=fallback navigateResult=error`
|
|
10154
8486
|
);
|
|
10155
8487
|
return c.json(
|
|
10156
8488
|
{
|
|
@@ -10166,7 +8498,7 @@ app19.post("/navigate", async (c) => {
|
|
|
10166
8498
|
const cdpOk = await ensureCdp(transport);
|
|
10167
8499
|
if (!cdpOk) {
|
|
10168
8500
|
console.error(
|
|
10169
|
-
`${
|
|
8501
|
+
`${TAG19} intent=${JSON.stringify(intent)} browser=fallback navigateResult=cdp-unreachable hostname=${JSON.stringify(hostname2)}`
|
|
10170
8502
|
);
|
|
10171
8503
|
return c.json(
|
|
10172
8504
|
{
|
|
@@ -10182,7 +8514,7 @@ app19.post("/navigate", async (c) => {
|
|
|
10182
8514
|
const browser = outcome.result === "ok" ? "vnc" : "fallback";
|
|
10183
8515
|
const detailStr = outcome.detail ? ` detail=${JSON.stringify(outcome.detail.length > 230 ? outcome.detail.slice(0, 227) + "..." : outcome.detail)}` : "";
|
|
10184
8516
|
console.error(
|
|
10185
|
-
`${
|
|
8517
|
+
`${TAG19} intent=${JSON.stringify(intent)} browser=${browser} navigateResult=${outcome.result} hostname=${JSON.stringify(hostname2)} targetId=${outcome.targetId ?? "none"}${detailStr}`
|
|
10186
8518
|
);
|
|
10187
8519
|
if (outcome.result !== "ok") {
|
|
10188
8520
|
return c.json(
|
|
@@ -10213,18 +8545,18 @@ var ALLOWED_EVENTS = /* @__PURE__ */ new Set([
|
|
|
10213
8545
|
]);
|
|
10214
8546
|
var app20 = new Hono();
|
|
10215
8547
|
app20.post("/", async (c) => {
|
|
10216
|
-
const
|
|
8548
|
+
const TAG19 = "[admin:events]";
|
|
10217
8549
|
let body;
|
|
10218
8550
|
try {
|
|
10219
8551
|
body = await c.req.json();
|
|
10220
8552
|
} catch (err) {
|
|
10221
8553
|
const detail = err instanceof Error ? err.message : String(err);
|
|
10222
|
-
console.error(`${
|
|
8554
|
+
console.error(`${TAG19} reject reason=body-not-json detail=${detail}`);
|
|
10223
8555
|
return c.json({ ok: false, detail: "Request body was not valid JSON" }, 400);
|
|
10224
8556
|
}
|
|
10225
8557
|
const event = typeof body.event === "string" ? body.event : "";
|
|
10226
8558
|
if (!ALLOWED_EVENTS.has(event)) {
|
|
10227
|
-
console.error(`${
|
|
8559
|
+
console.error(`${TAG19} reject reason=event-not-allowed event=${JSON.stringify(event)}`);
|
|
10228
8560
|
return c.json({ ok: false, detail: `Event "${event}" is not allowed` }, 400);
|
|
10229
8561
|
}
|
|
10230
8562
|
const rawFields = body.fields && typeof body.fields === "object" ? body.fields : {};
|
|
@@ -10247,8 +8579,8 @@ var events_default = app20;
|
|
|
10247
8579
|
|
|
10248
8580
|
// server/routes/admin/cloudflare.ts
|
|
10249
8581
|
import { homedir } from "os";
|
|
10250
|
-
import { resolve as
|
|
10251
|
-
import { readFileSync as
|
|
8582
|
+
import { resolve as resolve14 } from "path";
|
|
8583
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
10252
8584
|
|
|
10253
8585
|
// app/lib/dns-label.ts
|
|
10254
8586
|
var VALID_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -10264,14 +8596,14 @@ function isValidDomain(value) {
|
|
|
10264
8596
|
}
|
|
10265
8597
|
|
|
10266
8598
|
// app/lib/alias-domains.ts
|
|
10267
|
-
import { existsSync as
|
|
10268
|
-
import { dirname as
|
|
10269
|
-
import { resolve as
|
|
10270
|
-
var ALIAS_DOMAINS_PATH =
|
|
8599
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
|
|
8600
|
+
import { dirname as dirname4 } from "path";
|
|
8601
|
+
import { resolve as resolve13 } from "path";
|
|
8602
|
+
var ALIAS_DOMAINS_PATH = resolve13(MAXY_DIR, "alias-domains.json");
|
|
10271
8603
|
function readExisting() {
|
|
10272
|
-
if (!
|
|
8604
|
+
if (!existsSync16(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
|
|
10273
8605
|
try {
|
|
10274
|
-
const parsed = JSON.parse(
|
|
8606
|
+
const parsed = JSON.parse(readFileSync12(ALIAS_DOMAINS_PATH, "utf-8"));
|
|
10275
8607
|
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
10276
8608
|
return new Set(parsed.filter((h) => typeof h === "string"));
|
|
10277
8609
|
} catch {
|
|
@@ -10282,135 +8614,18 @@ function addAliasDomain(hostname2) {
|
|
|
10282
8614
|
const existing = readExisting();
|
|
10283
8615
|
if (existing.has(hostname2)) return;
|
|
10284
8616
|
existing.add(hostname2);
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
}
|
|
10288
|
-
|
|
10289
|
-
// app/lib/action-relay-queue.ts
|
|
10290
|
-
import {
|
|
10291
|
-
existsSync as existsSync20,
|
|
10292
|
-
mkdirSync as mkdirSync9,
|
|
10293
|
-
readdirSync as readdirSync6,
|
|
10294
|
-
readFileSync as readFileSync16,
|
|
10295
|
-
unlinkSync as unlinkSync2,
|
|
10296
|
-
writeFileSync as writeFileSync10
|
|
10297
|
-
} from "fs";
|
|
10298
|
-
import { join as join9 } from "path";
|
|
10299
|
-
var TAG19 = "[action-relay-queue]";
|
|
10300
|
-
var FILE_PREFIX = "action-completion-relay-";
|
|
10301
|
-
var FILE_SUFFIX = ".json";
|
|
10302
|
-
function queueDir(accountDir) {
|
|
10303
|
-
return join9(accountDir, "queue");
|
|
10304
|
-
}
|
|
10305
|
-
function relayFilePath(accountDir, actionId) {
|
|
10306
|
-
return join9(queueDir(accountDir), `${FILE_PREFIX}${actionId}${FILE_SUFFIX}`);
|
|
10307
|
-
}
|
|
10308
|
-
function enqueueActionCompletionRelay(opts) {
|
|
10309
|
-
const dir = queueDir(opts.accountDir);
|
|
10310
|
-
mkdirSync9(dir, { recursive: true });
|
|
10311
|
-
const filePath = relayFilePath(opts.accountDir, opts.actionId);
|
|
10312
|
-
const record = {
|
|
10313
|
-
actionId: opts.actionId,
|
|
10314
|
-
conversationId: opts.conversationId,
|
|
10315
|
-
message: opts.message,
|
|
10316
|
-
queuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10317
|
-
};
|
|
10318
|
-
const body = JSON.stringify(record);
|
|
10319
|
-
try {
|
|
10320
|
-
writeFileSync10(filePath, body, { flag: "wx", encoding: "utf-8" });
|
|
10321
|
-
console.log(
|
|
10322
|
-
`${TAG19} phase=enqueued actionId=${opts.actionId} conversationId=${opts.conversationId} path=${filePath} bytes=${Buffer.byteLength(body, "utf-8")}`
|
|
10323
|
-
);
|
|
10324
|
-
return { enqueued: true, path: filePath };
|
|
10325
|
-
} catch (e) {
|
|
10326
|
-
if (e.code === "EEXIST") {
|
|
10327
|
-
console.log(
|
|
10328
|
-
`${TAG19} phase=enqueue-skipped actionId=${opts.actionId} reason=already-queued`
|
|
10329
|
-
);
|
|
10330
|
-
return { enqueued: false, reason: "already-queued", path: filePath };
|
|
10331
|
-
}
|
|
10332
|
-
throw e;
|
|
10333
|
-
}
|
|
10334
|
-
}
|
|
10335
|
-
function consumeActionCompletionRelays(accountDir) {
|
|
10336
|
-
const dir = queueDir(accountDir);
|
|
10337
|
-
if (!existsSync20(dir)) return [];
|
|
10338
|
-
let entries;
|
|
10339
|
-
try {
|
|
10340
|
-
entries = readdirSync6(dir);
|
|
10341
|
-
} catch (e) {
|
|
10342
|
-
console.error(
|
|
10343
|
-
`${TAG19} phase=readdir-failed dir=${dir} error=${e instanceof Error ? e.message : String(e)}`
|
|
10344
|
-
);
|
|
10345
|
-
return [];
|
|
10346
|
-
}
|
|
10347
|
-
const records = [];
|
|
10348
|
-
for (const name of entries) {
|
|
10349
|
-
if (!name.startsWith(FILE_PREFIX) || !name.endsWith(FILE_SUFFIX)) continue;
|
|
10350
|
-
const filePath = join9(dir, name);
|
|
10351
|
-
let raw;
|
|
10352
|
-
try {
|
|
10353
|
-
raw = readFileSync16(filePath, "utf-8");
|
|
10354
|
-
} catch (e) {
|
|
10355
|
-
console.error(
|
|
10356
|
-
`${TAG19} phase=read-failed file=${name} error=${e instanceof Error ? e.message : String(e)}`
|
|
10357
|
-
);
|
|
10358
|
-
continue;
|
|
10359
|
-
}
|
|
10360
|
-
let parsed;
|
|
10361
|
-
try {
|
|
10362
|
-
parsed = JSON.parse(raw);
|
|
10363
|
-
} catch (e) {
|
|
10364
|
-
console.error(
|
|
10365
|
-
`${TAG19} phase=parse-failed file=${name} error=${e instanceof Error ? e.message : String(e)}`
|
|
10366
|
-
);
|
|
10367
|
-
continue;
|
|
10368
|
-
}
|
|
10369
|
-
if (!isActionCompletionRelay(parsed)) {
|
|
10370
|
-
console.error(
|
|
10371
|
-
`${TAG19} phase=shape-invalid file=${name} keys=${parsed && typeof parsed === "object" ? Object.keys(parsed).join(",") : "non-object"}`
|
|
10372
|
-
);
|
|
10373
|
-
continue;
|
|
10374
|
-
}
|
|
10375
|
-
records.push({ rec: parsed, filePath });
|
|
10376
|
-
}
|
|
10377
|
-
records.sort((a, b) => a.rec.queuedAt.localeCompare(b.rec.queuedAt));
|
|
10378
|
-
const out = [];
|
|
10379
|
-
const now = Date.now();
|
|
10380
|
-
for (const { rec, filePath } of records) {
|
|
10381
|
-
const queuedAtMs = Date.parse(rec.queuedAt);
|
|
10382
|
-
const ageMs = Number.isFinite(queuedAtMs) ? now - queuedAtMs : 0;
|
|
10383
|
-
out.push({ ...rec, ageMs, filePath });
|
|
10384
|
-
}
|
|
10385
|
-
return out;
|
|
10386
|
-
}
|
|
10387
|
-
function deleteConsumedRelay(filePath) {
|
|
10388
|
-
try {
|
|
10389
|
-
unlinkSync2(filePath);
|
|
10390
|
-
} catch (e) {
|
|
10391
|
-
const code = e.code;
|
|
10392
|
-
if (code !== "ENOENT") {
|
|
10393
|
-
console.error(
|
|
10394
|
-
`${TAG19} phase=unlink-failed file=${filePath} error=${e instanceof Error ? e.message : String(e)}`
|
|
10395
|
-
);
|
|
10396
|
-
}
|
|
10397
|
-
}
|
|
10398
|
-
}
|
|
10399
|
-
function isActionCompletionRelay(value) {
|
|
10400
|
-
if (!value || typeof value !== "object") return false;
|
|
10401
|
-
const v = value;
|
|
10402
|
-
return typeof v.actionId === "string" && typeof v.conversationId === "string" && typeof v.message === "string" && typeof v.queuedAt === "string";
|
|
8617
|
+
mkdirSync5(dirname4(ALIAS_DOMAINS_PATH), { recursive: true });
|
|
8618
|
+
writeFileSync6(ALIAS_DOMAINS_PATH, JSON.stringify([...existing], null, 2) + "\n", "utf-8");
|
|
10403
8619
|
}
|
|
10404
8620
|
|
|
10405
8621
|
// server/routes/admin/cloudflare.ts
|
|
10406
|
-
import { existsSync as existsSyncFs, readFileSync as readFileSyncFs } from "fs";
|
|
10407
8622
|
var SETUP_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
10408
8623
|
var DOMAINS_TIMEOUT_MS = 40 * 1e3;
|
|
10409
8624
|
function loadBrandInfo() {
|
|
10410
|
-
const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ??
|
|
10411
|
-
const brandPath =
|
|
8625
|
+
const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve14(process.cwd(), "..");
|
|
8626
|
+
const brandPath = resolve14(platformRoot2, "config", "brand.json");
|
|
10412
8627
|
try {
|
|
10413
|
-
const parsed = JSON.parse(
|
|
8628
|
+
const parsed = JSON.parse(readFileSync13(brandPath, "utf-8"));
|
|
10414
8629
|
const hostname2 = typeof parsed.hostname === "string" && parsed.hostname ? parsed.hostname : "maxy";
|
|
10415
8630
|
const configDir2 = typeof parsed.configDir === "string" && parsed.configDir ? parsed.configDir : ".maxy";
|
|
10416
8631
|
return { hostname: hostname2, configDir: configDir2 };
|
|
@@ -10513,7 +8728,7 @@ app21.get("/domains", requireAdminSession, async (c) => {
|
|
|
10513
8728
|
streamLogPath = streamLogPathFor(accountId, correlationId).streamLogPath;
|
|
10514
8729
|
log(`phase=stream-log-resolved path=${streamLogPath}`);
|
|
10515
8730
|
const brand = loadBrandInfo();
|
|
10516
|
-
const scriptPath =
|
|
8731
|
+
const scriptPath = resolve14(homedir(), "list-cf-domains.sh");
|
|
10517
8732
|
const result = await runFormSpawn({
|
|
10518
8733
|
scriptPath,
|
|
10519
8734
|
args: [brand.hostname],
|
|
@@ -10706,89 +8921,23 @@ actionId: ${actionId}`,
|
|
|
10706
8921
|
};
|
|
10707
8922
|
return ok(success);
|
|
10708
8923
|
});
|
|
10709
|
-
var RELAY_MAX_BODY = 8 * 1024;
|
|
10710
|
-
var RELAY_MAX_MESSAGE = 2048;
|
|
10711
|
-
var ACTION_ID_RE = /^[a-z0-9-]+$/;
|
|
10712
|
-
app21.post("/relay-completion", requireAdminSession, async (c) => {
|
|
10713
|
-
const sessionKey = c.var.sessionKey;
|
|
10714
|
-
let raw;
|
|
10715
|
-
try {
|
|
10716
|
-
raw = await c.req.text();
|
|
10717
|
-
} catch {
|
|
10718
|
-
return c.json({ ok: false, reason: "invalid-body" }, 400);
|
|
10719
|
-
}
|
|
10720
|
-
if (Buffer.byteLength(raw, "utf-8") > RELAY_MAX_BODY) {
|
|
10721
|
-
return c.json({ ok: false, reason: "body-too-large" }, 413);
|
|
10722
|
-
}
|
|
10723
|
-
let body;
|
|
10724
|
-
try {
|
|
10725
|
-
body = JSON.parse(raw);
|
|
10726
|
-
} catch {
|
|
10727
|
-
return c.json({ ok: false, reason: "invalid-json" }, 400);
|
|
10728
|
-
}
|
|
10729
|
-
if (!body || typeof body !== "object") {
|
|
10730
|
-
return c.json({ ok: false, reason: "invalid-body" }, 400);
|
|
10731
|
-
}
|
|
10732
|
-
const { actionId, message } = body;
|
|
10733
|
-
if (typeof actionId !== "string" || !ACTION_ID_RE.test(actionId) || actionId.length > 200) {
|
|
10734
|
-
console.error(`[cloudflare-relay-completion] phase=enqueue-skipped reason=invalid-actionid sessionKey=${sessionKey.slice(-8)}`);
|
|
10735
|
-
return c.json({ ok: false, reason: "invalid-actionid" }, 400);
|
|
10736
|
-
}
|
|
10737
|
-
if (typeof message !== "string" || message.length === 0 || message.length > RELAY_MAX_MESSAGE) {
|
|
10738
|
-
return c.json({ ok: false, reason: "invalid-message" }, 400);
|
|
10739
|
-
}
|
|
10740
|
-
const conversationId = getConversationIdForSession(sessionKey);
|
|
10741
|
-
if (!conversationId) {
|
|
10742
|
-
console.error(`[cloudflare-relay-completion] phase=enqueue-skipped reason=missing-conversation-id sessionKey=${sessionKey.slice(-8)}`);
|
|
10743
|
-
return c.json({ ok: false, reason: "missing-conversation-id" }, 400);
|
|
10744
|
-
}
|
|
10745
|
-
const account = resolveAccount();
|
|
10746
|
-
if (!account) {
|
|
10747
|
-
console.error(`[cloudflare-relay-completion] phase=enqueue-skipped reason=missing-account-dir actionId=${actionId}`);
|
|
10748
|
-
return c.json({ ok: false, reason: "missing-account-dir" }, 500);
|
|
10749
|
-
}
|
|
10750
|
-
const logPath2 = actionLogPath(actionId);
|
|
10751
|
-
let auditOutcome = "log-absent";
|
|
10752
|
-
if (existsSyncFs(logPath2)) {
|
|
10753
|
-
try {
|
|
10754
|
-
const lines = readFileSyncFs(logPath2, "utf-8").split("\n");
|
|
10755
|
-
const reconciled = reconcileCloudflareSetupFromLog(lines);
|
|
10756
|
-
auditOutcome = reconciled ? reconciled.kind : "log-incomplete";
|
|
10757
|
-
} catch (e) {
|
|
10758
|
-
auditOutcome = `read-failed:${e instanceof Error ? e.message : String(e)}`;
|
|
10759
|
-
}
|
|
10760
|
-
}
|
|
10761
|
-
console.log(`[cloudflare-relay-completion] phase=enqueue-attempt actionId=${actionId} auditOutcome=${auditOutcome} conversationId=${conversationId}`);
|
|
10762
|
-
try {
|
|
10763
|
-
enqueueActionCompletionRelay({
|
|
10764
|
-
accountDir: account.accountDir,
|
|
10765
|
-
actionId,
|
|
10766
|
-
conversationId,
|
|
10767
|
-
message
|
|
10768
|
-
});
|
|
10769
|
-
} catch (e) {
|
|
10770
|
-
console.error(`[cloudflare-relay-completion] phase=enqueue-failed actionId=${actionId} error=${e instanceof Error ? e.message : String(e)}`);
|
|
10771
|
-
return c.json({ ok: false, reason: "enqueue-failed" }, 500);
|
|
10772
|
-
}
|
|
10773
|
-
return c.body(null, 204);
|
|
10774
|
-
});
|
|
10775
8924
|
var cloudflare_default = app21;
|
|
10776
8925
|
|
|
10777
8926
|
// server/routes/admin/files.ts
|
|
10778
8927
|
import { createReadStream as createReadStream3 } from "fs";
|
|
10779
8928
|
import { readdir as readdir2, readFile as readFile4, stat as stat4, mkdir as mkdir3, writeFile as writeFile4, unlink as unlink2 } from "fs/promises";
|
|
10780
8929
|
import { realpathSync as realpathSync4 } from "fs";
|
|
10781
|
-
import { basename as
|
|
8930
|
+
import { basename as basename4, dirname as dirname5, join as join8, resolve as resolve16, sep as sep2 } from "path";
|
|
10782
8931
|
import { Readable as Readable2 } from "stream";
|
|
10783
8932
|
|
|
10784
8933
|
// app/lib/data-path.ts
|
|
10785
8934
|
import { realpathSync as realpathSync3 } from "fs";
|
|
10786
|
-
import { resolve as
|
|
10787
|
-
var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ??
|
|
10788
|
-
var DATA_ROOT =
|
|
8935
|
+
import { resolve as resolve15, normalize, sep, relative } from "path";
|
|
8936
|
+
var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve15(process.cwd(), "../platform");
|
|
8937
|
+
var DATA_ROOT = resolve15(PLATFORM_ROOT6, "..", "data");
|
|
10789
8938
|
function resolveDataPath(raw) {
|
|
10790
8939
|
const cleaned = normalize("/" + (raw ?? "").replace(/\\/g, "/")).replace(/^\/+/, "");
|
|
10791
|
-
const absolute =
|
|
8940
|
+
const absolute = resolve15(DATA_ROOT, cleaned);
|
|
10792
8941
|
let dataRootReal;
|
|
10793
8942
|
try {
|
|
10794
8943
|
dataRootReal = realpathSync3(DATA_ROOT);
|
|
@@ -11136,7 +9285,7 @@ async function cascadeDeleteDocument(params) {
|
|
|
11136
9285
|
var UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11137
9286
|
async function readMeta(absDir, baseName) {
|
|
11138
9287
|
try {
|
|
11139
|
-
const raw = await readFile4(
|
|
9288
|
+
const raw = await readFile4(join8(absDir, `${baseName}.meta.json`), "utf8");
|
|
11140
9289
|
const parsed = JSON.parse(raw);
|
|
11141
9290
|
if (typeof parsed?.filename === "string") {
|
|
11142
9291
|
return { filename: parsed.filename, mimeType: typeof parsed.mimeType === "string" ? parsed.mimeType : void 0 };
|
|
@@ -11147,7 +9296,7 @@ async function readMeta(absDir, baseName) {
|
|
|
11147
9296
|
}
|
|
11148
9297
|
async function readAccountNames() {
|
|
11149
9298
|
const map = /* @__PURE__ */ new Map();
|
|
11150
|
-
const accountsDir =
|
|
9299
|
+
const accountsDir = resolve16(DATA_ROOT, "accounts");
|
|
11151
9300
|
let names;
|
|
11152
9301
|
try {
|
|
11153
9302
|
names = await readdir2(accountsDir);
|
|
@@ -11156,7 +9305,7 @@ async function readAccountNames() {
|
|
|
11156
9305
|
}
|
|
11157
9306
|
for (const name of names) {
|
|
11158
9307
|
if (!UUID_RE4.test(name)) continue;
|
|
11159
|
-
const configPath2 =
|
|
9308
|
+
const configPath2 = resolve16(accountsDir, name, "account.json");
|
|
11160
9309
|
try {
|
|
11161
9310
|
const raw = await readFile4(configPath2, "utf8");
|
|
11162
9311
|
const parsed = JSON.parse(raw);
|
|
@@ -11174,7 +9323,7 @@ async function readAccountNames() {
|
|
|
11174
9323
|
}
|
|
11175
9324
|
async function enrich(absolute, entry, accountNames) {
|
|
11176
9325
|
if (entry.kind === "directory" && UUID_RE4.test(entry.name)) {
|
|
11177
|
-
const meta = await readMeta(
|
|
9326
|
+
const meta = await readMeta(join8(absolute, entry.name), entry.name);
|
|
11178
9327
|
if (meta?.filename) {
|
|
11179
9328
|
entry.displayName = meta.filename;
|
|
11180
9329
|
entry.mimeType = meta.mimeType;
|
|
@@ -11233,7 +9382,7 @@ app22.get("/", requireAdminSession, async (c) => {
|
|
|
11233
9382
|
continue;
|
|
11234
9383
|
}
|
|
11235
9384
|
try {
|
|
11236
|
-
const entryPath =
|
|
9385
|
+
const entryPath = join8(absolute, name);
|
|
11237
9386
|
const s = await stat4(entryPath);
|
|
11238
9387
|
entries.push({
|
|
11239
9388
|
name,
|
|
@@ -11288,7 +9437,7 @@ app22.get("/download", requireAdminSession, async (c) => {
|
|
|
11288
9437
|
if (!info.isFile()) {
|
|
11289
9438
|
return c.json({ error: "Path is not a file" }, 400);
|
|
11290
9439
|
}
|
|
11291
|
-
const filename =
|
|
9440
|
+
const filename = basename4(absolute);
|
|
11292
9441
|
const mimeType = detectMimeType(absolute);
|
|
11293
9442
|
const nodeStream = createReadStream3(absolute);
|
|
11294
9443
|
const webStream = Readable2.toWeb(nodeStream);
|
|
@@ -11345,10 +9494,10 @@ app22.post("/upload", requireAdminSession, async (c) => {
|
|
|
11345
9494
|
error: `Unsupported file type: "${file.type}". Supported: ${[...SUPPORTED_MIME_TYPES].join(", ")}.`
|
|
11346
9495
|
}, 422);
|
|
11347
9496
|
}
|
|
11348
|
-
const safeName =
|
|
9497
|
+
const safeName = basename4(file.name).replace(/[\0/\\]/g, "_");
|
|
11349
9498
|
const finalName = `${Date.now()}-${safeName}`;
|
|
11350
|
-
const destDir =
|
|
11351
|
-
const destPath =
|
|
9499
|
+
const destDir = resolve16(DATA_ROOT, "uploads", accountId);
|
|
9500
|
+
const destPath = resolve16(destDir, finalName);
|
|
11352
9501
|
try {
|
|
11353
9502
|
await mkdir3(destDir, { recursive: true });
|
|
11354
9503
|
const dataRootReal = realpathSync4(DATA_ROOT);
|
|
@@ -11390,7 +9539,7 @@ app22.delete("/", requireAdminSession, async (c) => {
|
|
|
11390
9539
|
return c.json({ error: resolution.error }, resolution.status);
|
|
11391
9540
|
}
|
|
11392
9541
|
const { absolute, relative: relPath2 } = resolution;
|
|
11393
|
-
const base =
|
|
9542
|
+
const base = basename4(absolute);
|
|
11394
9543
|
const segments = relPath2.split("/").filter(Boolean);
|
|
11395
9544
|
if (base === "account.json" || segments.includes(".git")) {
|
|
11396
9545
|
console.error(`[data] file-delete blocked path="${relPath2}" reason="protected"`);
|
|
@@ -11406,7 +9555,7 @@ app22.delete("/", requireAdminSession, async (c) => {
|
|
|
11406
9555
|
}
|
|
11407
9556
|
const dot = base.lastIndexOf(".");
|
|
11408
9557
|
const stem = dot === -1 ? base : base.slice(0, dot);
|
|
11409
|
-
const sidecarPath = UUID_RE4.test(stem) && base !== `${stem}.meta.json` ?
|
|
9558
|
+
const sidecarPath = UUID_RE4.test(stem) && base !== `${stem}.meta.json` ? join8(dirname5(absolute), `${stem}.meta.json`) : null;
|
|
11410
9559
|
await unlink2(absolute);
|
|
11411
9560
|
if (sidecarPath) {
|
|
11412
9561
|
try {
|
|
@@ -11957,11 +10106,6 @@ var GRAPH_LABEL_COLOURS = {
|
|
|
11957
10106
|
// confusion doesn't apply, but kept distinguishable for the legend)
|
|
11958
10107
|
Email: "#6F7F4A",
|
|
11959
10108
|
EmailAccount: "#91A063",
|
|
11960
|
-
// Review signals (Task 626 — previously written by review-detector but
|
|
11961
|
-
// unregistered here, producing an `unknown label` 400 whenever the
|
|
11962
|
-
// filter popover advertised the label. Muted brick — still reads as
|
|
11963
|
-
// alert against the cream background but doesn't shout fire-engine red.)
|
|
11964
|
-
ReviewAlert: "#A85C5C",
|
|
11965
10109
|
// Public-agent projection (Task 837) — burnished bronze sits between
|
|
11966
10110
|
// people-terracotta and email-moss but shares neither hue, signalling
|
|
11967
10111
|
// "operator-defined persona that traverses to KnowledgeDocuments,
|
|
@@ -12886,8 +11030,8 @@ var adherence_default = app30;
|
|
|
12886
11030
|
// server/routes/admin/sidebar-artefacts.ts
|
|
12887
11031
|
import neo4j3 from "neo4j-driver";
|
|
12888
11032
|
import { readFile as readFile5, readdir as readdir3, stat as stat5 } from "fs/promises";
|
|
12889
|
-
import { resolve as
|
|
12890
|
-
import { existsSync as
|
|
11033
|
+
import { resolve as resolve17, relative as relative2, isAbsolute } from "path";
|
|
11034
|
+
import { existsSync as existsSync17 } from "fs";
|
|
12891
11035
|
var LIMIT = 50;
|
|
12892
11036
|
var TEXT_MIME_PREFIXES = ["text/", "application/json", "application/markdown"];
|
|
12893
11037
|
var ADMIN_AGENT_FILES = ["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"];
|
|
@@ -12903,7 +11047,7 @@ app31.get("/", requireAdminSession, async (c) => {
|
|
|
12903
11047
|
if (docs === null) {
|
|
12904
11048
|
return c.json({ error: "Failed to load artefacts" }, 500);
|
|
12905
11049
|
}
|
|
12906
|
-
const accountDir =
|
|
11050
|
+
const accountDir = resolve17(ACCOUNTS_DIR, accountId);
|
|
12907
11051
|
const agents = await fetchAgentTemplateRows(accountDir);
|
|
12908
11052
|
const artefacts = [...docs, ...agents].sort(
|
|
12909
11053
|
(a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")
|
|
@@ -12966,8 +11110,8 @@ async function readArtefactContent(accountId, attachmentId, mimeType, displayNam
|
|
|
12966
11110
|
logSkip(displayName, "non-text-mime", mimeType);
|
|
12967
11111
|
return { content: "", skipReason: "non-text-mime" };
|
|
12968
11112
|
}
|
|
12969
|
-
const accountDir =
|
|
12970
|
-
const dir =
|
|
11113
|
+
const accountDir = resolve17(ATTACHMENTS_ROOT, accountId);
|
|
11114
|
+
const dir = resolve17(accountDir, attachmentId);
|
|
12971
11115
|
try {
|
|
12972
11116
|
validateFilePathInAccount(dir, accountDir);
|
|
12973
11117
|
} catch {
|
|
@@ -12981,7 +11125,7 @@ async function readArtefactContent(accountId, attachmentId, mimeType, displayNam
|
|
|
12981
11125
|
logSkip(displayName, "missing-on-disk", mimeType);
|
|
12982
11126
|
return { content: "", skipReason: "missing-on-disk" };
|
|
12983
11127
|
}
|
|
12984
|
-
return { content: await readFile5(
|
|
11128
|
+
return { content: await readFile5(resolve17(dir, dataFile), "utf-8"), skipReason: null };
|
|
12985
11129
|
} catch (err) {
|
|
12986
11130
|
const message = err instanceof Error ? err.message : String(err);
|
|
12987
11131
|
console.error(`[admin/sidebar-artefacts] read-failed attachmentId=${attachmentId.slice(0, 8)} error="${message}"`);
|
|
@@ -12997,8 +11141,8 @@ function logSkip(name, reason, mimeType) {
|
|
|
12997
11141
|
async function fetchAgentTemplateRows(accountDir) {
|
|
12998
11142
|
const rows = [];
|
|
12999
11143
|
for (const filename of ADMIN_AGENT_FILES) {
|
|
13000
|
-
const overridePath =
|
|
13001
|
-
const bundledPath =
|
|
11144
|
+
const overridePath = resolve17(accountDir, "agents", "admin", filename);
|
|
11145
|
+
const bundledPath = resolve17(PLATFORM_ROOT, "templates", "agents", "admin", filename);
|
|
13002
11146
|
const labelStem = filename.replace(/\.md$/, "");
|
|
13003
11147
|
const row = await readAgentTemplateRow({
|
|
13004
11148
|
id: `agent-template:admin:${filename}`,
|
|
@@ -13012,12 +11156,12 @@ async function fetchAgentTemplateRows(accountDir) {
|
|
|
13012
11156
|
});
|
|
13013
11157
|
if (row) rows.push(row);
|
|
13014
11158
|
}
|
|
13015
|
-
const overrideDir =
|
|
13016
|
-
const bundledDir =
|
|
11159
|
+
const overrideDir = resolve17(accountDir, "specialists", "agents");
|
|
11160
|
+
const bundledDir = resolve17(PLATFORM_ROOT, "templates", "specialists", "agents");
|
|
13017
11161
|
const specialistNames = await unionSpecialistFilenames(overrideDir, bundledDir);
|
|
13018
11162
|
for (const filename of specialistNames) {
|
|
13019
|
-
const overridePath =
|
|
13020
|
-
const bundledPath =
|
|
11163
|
+
const overridePath = resolve17(overrideDir, filename);
|
|
11164
|
+
const bundledPath = resolve17(bundledDir, filename);
|
|
13021
11165
|
const row = await readAgentTemplateRow({
|
|
13022
11166
|
id: `agent-template:specialist:${filename}`,
|
|
13023
11167
|
displayName: filename.replace(/\.md$/, ""),
|
|
@@ -13035,7 +11179,7 @@ async function fetchAgentTemplateRows(accountDir) {
|
|
|
13035
11179
|
async function unionSpecialistFilenames(overrideDir, bundledDir) {
|
|
13036
11180
|
const names = /* @__PURE__ */ new Set();
|
|
13037
11181
|
for (const dir of [overrideDir, bundledDir]) {
|
|
13038
|
-
if (!
|
|
11182
|
+
if (!existsSync17(dir)) continue;
|
|
13039
11183
|
try {
|
|
13040
11184
|
const entries = await readdir3(dir);
|
|
13041
11185
|
for (const entry of entries) {
|
|
@@ -13050,7 +11194,7 @@ async function unionSpecialistFilenames(overrideDir, bundledDir) {
|
|
|
13050
11194
|
}
|
|
13051
11195
|
async function readAgentTemplateRow(inp) {
|
|
13052
11196
|
let chosenPath = null;
|
|
13053
|
-
if (
|
|
11197
|
+
if (existsSync17(inp.overridePath)) {
|
|
13054
11198
|
try {
|
|
13055
11199
|
validateFilePathInAccount(inp.overridePath, inp.overrideRoot);
|
|
13056
11200
|
chosenPath = inp.overridePath;
|
|
@@ -13061,7 +11205,7 @@ async function readAgentTemplateRow(inp) {
|
|
|
13061
11205
|
);
|
|
13062
11206
|
return null;
|
|
13063
11207
|
}
|
|
13064
|
-
} else if (
|
|
11208
|
+
} else if (existsSync17(inp.bundledPath)) {
|
|
13065
11209
|
if (!isWithin(inp.bundledPath, inp.bundledRoot)) {
|
|
13066
11210
|
console.error(
|
|
13067
11211
|
`[admin/sidebar-artefacts] agent-template-read-failed agent=${inp.displayName} kind=${inp.logName} error="bundled path outside PLATFORM_ROOT"`
|
|
@@ -13102,8 +11246,8 @@ var sidebar_artefacts_default = app31;
|
|
|
13102
11246
|
|
|
13103
11247
|
// server/routes/admin/sidebar-artefact-save.ts
|
|
13104
11248
|
import { mkdir as mkdir4, readdir as readdir4, stat as stat6, writeFile as writeFile5 } from "fs/promises";
|
|
13105
|
-
import { resolve as
|
|
13106
|
-
import { existsSync as
|
|
11249
|
+
import { resolve as resolve18 } from "path";
|
|
11250
|
+
import { existsSync as existsSync18 } from "fs";
|
|
13107
11251
|
var ADMIN_AGENT_FILES2 = /* @__PURE__ */ new Set(["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"]);
|
|
13108
11252
|
var UUID_RE5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
13109
11253
|
var app32 = new Hono();
|
|
@@ -13115,7 +11259,7 @@ app32.post("/", requireAdminSession, async (c) => {
|
|
|
13115
11259
|
if (!body || typeof body.id !== "string" || typeof body.content !== "string") {
|
|
13116
11260
|
return c.json({ error: "id and content required" }, 400);
|
|
13117
11261
|
}
|
|
13118
|
-
const accountDir =
|
|
11262
|
+
const accountDir = resolve18(ACCOUNTS_DIR, accountId);
|
|
13119
11263
|
const resolved = await resolveSavePath(body.id, accountId, accountDir);
|
|
13120
11264
|
if (resolved.kind === "reject") {
|
|
13121
11265
|
console.error(
|
|
@@ -13156,22 +11300,22 @@ async function resolveSavePath(id, accountId, accountDir) {
|
|
|
13156
11300
|
if (role !== "admin" || !ADMIN_AGENT_FILES2.has(filename)) {
|
|
13157
11301
|
return { kind: "reject", status: 400, reason: "invalid-id" };
|
|
13158
11302
|
}
|
|
13159
|
-
const parent =
|
|
11303
|
+
const parent = resolve18(accountDir, "agents", "admin");
|
|
13160
11304
|
await mkdir4(parent, { recursive: true });
|
|
13161
11305
|
try {
|
|
13162
11306
|
validateFilePathInAccount(parent, accountDir);
|
|
13163
11307
|
} catch {
|
|
13164
11308
|
return { kind: "reject", status: 400, reason: "containment-rejected" };
|
|
13165
11309
|
}
|
|
13166
|
-
return { kind: "admin-template", path:
|
|
11310
|
+
return { kind: "admin-template", path: resolve18(parent, filename) };
|
|
13167
11311
|
}
|
|
13168
11312
|
if (UUID_RE5.test(id)) {
|
|
13169
|
-
const dir =
|
|
13170
|
-
if (!
|
|
11313
|
+
const dir = resolve18(ATTACHMENTS_ROOT, accountId, id);
|
|
11314
|
+
if (!existsSync18(dir)) {
|
|
13171
11315
|
return { kind: "reject", status: 400, reason: "not-found" };
|
|
13172
11316
|
}
|
|
13173
11317
|
try {
|
|
13174
|
-
validateFilePathInAccount(dir,
|
|
11318
|
+
validateFilePathInAccount(dir, resolve18(ATTACHMENTS_ROOT, accountId));
|
|
13175
11319
|
} catch {
|
|
13176
11320
|
return { kind: "reject", status: 400, reason: "containment-rejected" };
|
|
13177
11321
|
}
|
|
@@ -13180,7 +11324,7 @@ async function resolveSavePath(id, accountId, accountDir) {
|
|
|
13180
11324
|
if (!dataFile) {
|
|
13181
11325
|
return { kind: "reject", status: 400, reason: "not-found" };
|
|
13182
11326
|
}
|
|
13183
|
-
return { kind: "knowledge-doc", path:
|
|
11327
|
+
return { kind: "knowledge-doc", path: resolve18(dir, dataFile) };
|
|
13184
11328
|
}
|
|
13185
11329
|
return { kind: "reject", status: 400, reason: "invalid-id" };
|
|
13186
11330
|
}
|
|
@@ -13191,8 +11335,8 @@ var sidebar_artefact_save_default = app32;
|
|
|
13191
11335
|
|
|
13192
11336
|
// server/routes/admin/sidebar-artefact-content.ts
|
|
13193
11337
|
import { readFile as readFile6, readdir as readdir5 } from "fs/promises";
|
|
13194
|
-
import { existsSync as
|
|
13195
|
-
import { resolve as
|
|
11338
|
+
import { existsSync as existsSync19 } from "fs";
|
|
11339
|
+
import { resolve as resolve19 } from "path";
|
|
13196
11340
|
var UUID_RE6 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
13197
11341
|
var app33 = new Hono();
|
|
13198
11342
|
app33.get("/", requireAdminSession, async (c) => {
|
|
@@ -13204,14 +11348,14 @@ app33.get("/", requireAdminSession, async (c) => {
|
|
|
13204
11348
|
console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
|
|
13205
11349
|
return new Response("Not found", { status: 404 });
|
|
13206
11350
|
}
|
|
13207
|
-
const dir =
|
|
13208
|
-
if (!
|
|
11351
|
+
const dir = resolve19(ATTACHMENTS_ROOT, accountId, id);
|
|
11352
|
+
if (!existsSync19(dir)) {
|
|
13209
11353
|
console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
|
|
13210
11354
|
return new Response("Not found", { status: 404 });
|
|
13211
11355
|
}
|
|
13212
11356
|
let meta;
|
|
13213
11357
|
try {
|
|
13214
|
-
meta = JSON.parse(await readFile6(
|
|
11358
|
+
meta = JSON.parse(await readFile6(resolve19(dir, `${id}.meta.json`), "utf-8"));
|
|
13215
11359
|
} catch {
|
|
13216
11360
|
console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
|
|
13217
11361
|
return new Response("Not found", { status: 404 });
|
|
@@ -13223,7 +11367,7 @@ app33.get("/", requireAdminSession, async (c) => {
|
|
|
13223
11367
|
return new Response("Not found", { status: 404 });
|
|
13224
11368
|
}
|
|
13225
11369
|
const start = Date.now();
|
|
13226
|
-
const buffer = await readFile6(
|
|
11370
|
+
const buffer = await readFile6(resolve19(dir, dataFile));
|
|
13227
11371
|
const ms = Date.now() - start;
|
|
13228
11372
|
console.log(
|
|
13229
11373
|
`[admin/sidebar-artefact-content] account=${accountId} id=${id.slice(0, 8)} mime=${meta.mimeType} bytes=${buffer.length} ms=${ms}`
|
|
@@ -13267,8 +11411,8 @@ app34.route("/sidebar-artefact-content", sidebar_artefact_content_default);
|
|
|
13267
11411
|
var admin_default = app34;
|
|
13268
11412
|
|
|
13269
11413
|
// server/routes/sites.ts
|
|
13270
|
-
import { existsSync as
|
|
13271
|
-
import { resolve as
|
|
11414
|
+
import { existsSync as existsSync20, readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync5 } from "fs";
|
|
11415
|
+
import { resolve as resolve20 } from "path";
|
|
13272
11416
|
var SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
|
|
13273
11417
|
var MIME = {
|
|
13274
11418
|
".html": "text/html; charset=utf-8",
|
|
@@ -13326,28 +11470,28 @@ app35.get("/:rel{.*}", (c) => {
|
|
|
13326
11470
|
}
|
|
13327
11471
|
segments.push(seg);
|
|
13328
11472
|
}
|
|
13329
|
-
const rootDir =
|
|
13330
|
-
let filePath = segments.length === 0 ? rootDir :
|
|
11473
|
+
const rootDir = resolve20(account.accountDir, "sites");
|
|
11474
|
+
let filePath = segments.length === 0 ? rootDir : resolve20(rootDir, ...segments);
|
|
13331
11475
|
if (filePath !== rootDir && !filePath.startsWith(rootDir + "/")) {
|
|
13332
11476
|
console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
|
|
13333
11477
|
return c.text("Forbidden", 403);
|
|
13334
11478
|
}
|
|
13335
11479
|
let stat7;
|
|
13336
11480
|
try {
|
|
13337
|
-
stat7 =
|
|
11481
|
+
stat7 = existsSync20(filePath) ? statSync5(filePath) : null;
|
|
13338
11482
|
} catch {
|
|
13339
11483
|
stat7 = null;
|
|
13340
11484
|
}
|
|
13341
11485
|
if (stat7?.isDirectory()) {
|
|
13342
|
-
filePath =
|
|
11486
|
+
filePath = resolve20(filePath, "index.html");
|
|
13343
11487
|
} else if (stat7 === null && isDirRequest) {
|
|
13344
|
-
filePath =
|
|
11488
|
+
filePath = resolve20(filePath, "index.html");
|
|
13345
11489
|
}
|
|
13346
11490
|
if (!filePath.startsWith(rootDir + "/")) {
|
|
13347
11491
|
console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
|
|
13348
11492
|
return c.text("Forbidden", 403);
|
|
13349
11493
|
}
|
|
13350
|
-
if (!
|
|
11494
|
+
if (!existsSync20(filePath)) {
|
|
13351
11495
|
console.error(`[sites] not-found path=${reqPath} status=404`);
|
|
13352
11496
|
return c.text("Not found", 404);
|
|
13353
11497
|
}
|
|
@@ -13366,7 +11510,7 @@ app35.get("/:rel{.*}", (c) => {
|
|
|
13366
11510
|
}
|
|
13367
11511
|
let body;
|
|
13368
11512
|
try {
|
|
13369
|
-
body =
|
|
11513
|
+
body = readFileSync14(realPath);
|
|
13370
11514
|
} catch (err) {
|
|
13371
11515
|
const code = err?.code;
|
|
13372
11516
|
if (code === "EISDIR") {
|
|
@@ -13498,14 +11642,14 @@ function clientFrom(c) {
|
|
|
13498
11642
|
);
|
|
13499
11643
|
}
|
|
13500
11644
|
var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT || "";
|
|
13501
|
-
var BRAND_JSON_PATH = PLATFORM_ROOT7 ?
|
|
11645
|
+
var BRAND_JSON_PATH = PLATFORM_ROOT7 ? join9(PLATFORM_ROOT7, "config", "brand.json") : "";
|
|
13502
11646
|
var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", domain: "getmaxy.com" };
|
|
13503
|
-
if (BRAND_JSON_PATH && !
|
|
11647
|
+
if (BRAND_JSON_PATH && !existsSync21(BRAND_JSON_PATH)) {
|
|
13504
11648
|
console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
|
|
13505
11649
|
}
|
|
13506
|
-
if (BRAND_JSON_PATH &&
|
|
11650
|
+
if (BRAND_JSON_PATH && existsSync21(BRAND_JSON_PATH)) {
|
|
13507
11651
|
try {
|
|
13508
|
-
const parsed = JSON.parse(
|
|
11652
|
+
const parsed = JSON.parse(readFileSync15(BRAND_JSON_PATH, "utf-8"));
|
|
13509
11653
|
BRAND = { ...BRAND, ...parsed };
|
|
13510
11654
|
} catch (err) {
|
|
13511
11655
|
console.error(`[brand] Failed to parse brand.json: ${err.message}`);
|
|
@@ -13524,11 +11668,11 @@ var brandLoginOpts = {
|
|
|
13524
11668
|
bodyFont: BRAND.defaultFonts?.body,
|
|
13525
11669
|
logoContainsName: !!BRAND.logoContainsName
|
|
13526
11670
|
};
|
|
13527
|
-
var ALIAS_DOMAINS_PATH2 =
|
|
11671
|
+
var ALIAS_DOMAINS_PATH2 = join9(homedir2(), BRAND.configDir, "alias-domains.json");
|
|
13528
11672
|
function loadAliasDomains() {
|
|
13529
11673
|
try {
|
|
13530
|
-
if (!
|
|
13531
|
-
const parsed = JSON.parse(
|
|
11674
|
+
if (!existsSync21(ALIAS_DOMAINS_PATH2)) return null;
|
|
11675
|
+
const parsed = JSON.parse(readFileSync15(ALIAS_DOMAINS_PATH2, "utf-8"));
|
|
13532
11676
|
if (!Array.isArray(parsed)) {
|
|
13533
11677
|
console.error("[alias-domains] malformed alias-domains.json \u2014 expected array");
|
|
13534
11678
|
return null;
|
|
@@ -13868,20 +12012,20 @@ app36.get("/agent-assets/:slug/:filename", (c) => {
|
|
|
13868
12012
|
console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
|
|
13869
12013
|
return c.text("Not found", 404);
|
|
13870
12014
|
}
|
|
13871
|
-
const filePath =
|
|
13872
|
-
const expectedDir =
|
|
12015
|
+
const filePath = resolve21(account.accountDir, "agents", slug, "assets", filename);
|
|
12016
|
+
const expectedDir = resolve21(account.accountDir, "agents", slug, "assets");
|
|
13873
12017
|
if (!filePath.startsWith(expectedDir + "/")) {
|
|
13874
12018
|
console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
|
|
13875
12019
|
return c.text("Forbidden", 403);
|
|
13876
12020
|
}
|
|
13877
|
-
if (!
|
|
12021
|
+
if (!existsSync21(filePath)) {
|
|
13878
12022
|
console.error(`[agent-assets] serve slug=${slug} file=${filename} status=404`);
|
|
13879
12023
|
return c.text("Not found", 404);
|
|
13880
12024
|
}
|
|
13881
12025
|
const ext = "." + filename.split(".").pop()?.toLowerCase();
|
|
13882
12026
|
const contentType = IMAGE_MIME[ext] || "application/octet-stream";
|
|
13883
12027
|
console.log(`[agent-assets] serve slug=${slug} file=${filename} status=200`);
|
|
13884
|
-
const body =
|
|
12028
|
+
const body = readFileSync15(filePath);
|
|
13885
12029
|
return c.body(body, 200, {
|
|
13886
12030
|
"Content-Type": contentType,
|
|
13887
12031
|
"Cache-Control": "public, max-age=3600"
|
|
@@ -13898,20 +12042,20 @@ app36.get("/generated/:filename", (c) => {
|
|
|
13898
12042
|
console.error(`[generated] serve file=${filename} status=404`);
|
|
13899
12043
|
return c.text("Not found", 404);
|
|
13900
12044
|
}
|
|
13901
|
-
const filePath =
|
|
13902
|
-
const expectedDir =
|
|
12045
|
+
const filePath = resolve21(account.accountDir, "generated", filename);
|
|
12046
|
+
const expectedDir = resolve21(account.accountDir, "generated");
|
|
13903
12047
|
if (!filePath.startsWith(expectedDir + "/")) {
|
|
13904
12048
|
console.error(`[generated] serve file=${filename} status=403`);
|
|
13905
12049
|
return c.text("Forbidden", 403);
|
|
13906
12050
|
}
|
|
13907
|
-
if (!
|
|
12051
|
+
if (!existsSync21(filePath)) {
|
|
13908
12052
|
console.error(`[generated] serve file=${filename} status=404`);
|
|
13909
12053
|
return c.text("Not found", 404);
|
|
13910
12054
|
}
|
|
13911
12055
|
const ext = "." + filename.split(".").pop()?.toLowerCase();
|
|
13912
12056
|
const contentType = IMAGE_MIME[ext] || "application/octet-stream";
|
|
13913
12057
|
console.log(`[generated] serve file=${filename} status=200`);
|
|
13914
|
-
const body =
|
|
12058
|
+
const body = readFileSync15(filePath);
|
|
13915
12059
|
return c.body(body, 200, {
|
|
13916
12060
|
"Content-Type": contentType,
|
|
13917
12061
|
"Cache-Control": "public, max-age=86400"
|
|
@@ -13921,9 +12065,9 @@ app36.route("/sites", sites_default);
|
|
|
13921
12065
|
var htmlCache = /* @__PURE__ */ new Map();
|
|
13922
12066
|
var brandLogoPath = "/brand/maxy-monochrome.png";
|
|
13923
12067
|
var brandIconPath = "/brand/maxy-monochrome.png";
|
|
13924
|
-
if (BRAND_JSON_PATH &&
|
|
12068
|
+
if (BRAND_JSON_PATH && existsSync21(BRAND_JSON_PATH)) {
|
|
13925
12069
|
try {
|
|
13926
|
-
const fullBrand = JSON.parse(
|
|
12070
|
+
const fullBrand = JSON.parse(readFileSync15(BRAND_JSON_PATH, "utf-8"));
|
|
13927
12071
|
if (fullBrand.assets?.logo) brandLogoPath = `/brand/${fullBrand.assets.logo}`;
|
|
13928
12072
|
brandIconPath = fullBrand.assets?.icon ? `/brand/${fullBrand.assets.icon}` : brandLogoPath;
|
|
13929
12073
|
} catch {
|
|
@@ -13940,9 +12084,9 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
|
|
|
13940
12084
|
function readInstalledVersion() {
|
|
13941
12085
|
try {
|
|
13942
12086
|
if (!PLATFORM_ROOT7) return "unknown";
|
|
13943
|
-
const versionFile =
|
|
13944
|
-
if (!
|
|
13945
|
-
const content =
|
|
12087
|
+
const versionFile = join9(PLATFORM_ROOT7, "config", `.${BRAND.hostname}-version`);
|
|
12088
|
+
if (!existsSync21(versionFile)) return "unknown";
|
|
12089
|
+
const content = readFileSync15(versionFile, "utf-8").trim();
|
|
13946
12090
|
return content || "unknown";
|
|
13947
12091
|
} catch {
|
|
13948
12092
|
return "unknown";
|
|
@@ -13983,7 +12127,7 @@ var clientErrorReporterScript = `<script>
|
|
|
13983
12127
|
function cachedHtml(file) {
|
|
13984
12128
|
let html = htmlCache.get(file);
|
|
13985
12129
|
if (!html) {
|
|
13986
|
-
html =
|
|
12130
|
+
html = readFileSync15(resolve21(process.cwd(), "public", file), "utf-8");
|
|
13987
12131
|
const productNameEsc = escapeHtml(BRAND.productName);
|
|
13988
12132
|
html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
|
|
13989
12133
|
html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
|
|
@@ -13999,26 +12143,26 @@ ${clientErrorReporterScript}
|
|
|
13999
12143
|
}
|
|
14000
12144
|
var brandedHtmlCache = /* @__PURE__ */ new Map();
|
|
14001
12145
|
function loadBrandingCache(agentSlug) {
|
|
14002
|
-
const configDir2 =
|
|
12146
|
+
const configDir2 = join9(homedir2(), BRAND.configDir);
|
|
14003
12147
|
try {
|
|
14004
|
-
const accountJsonPath =
|
|
14005
|
-
if (!
|
|
14006
|
-
const account = JSON.parse(
|
|
12148
|
+
const accountJsonPath = join9(configDir2, "account.json");
|
|
12149
|
+
if (!existsSync21(accountJsonPath)) return null;
|
|
12150
|
+
const account = JSON.parse(readFileSync15(accountJsonPath, "utf-8"));
|
|
14007
12151
|
const accountId = account.accountId;
|
|
14008
12152
|
if (!accountId) return null;
|
|
14009
|
-
const cachePath =
|
|
14010
|
-
if (!
|
|
14011
|
-
return JSON.parse(
|
|
12153
|
+
const cachePath = join9(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
|
|
12154
|
+
if (!existsSync21(cachePath)) return null;
|
|
12155
|
+
return JSON.parse(readFileSync15(cachePath, "utf-8"));
|
|
14012
12156
|
} catch {
|
|
14013
12157
|
return null;
|
|
14014
12158
|
}
|
|
14015
12159
|
}
|
|
14016
12160
|
function resolveDefaultSlug() {
|
|
14017
12161
|
try {
|
|
14018
|
-
const configDir2 =
|
|
14019
|
-
const accountJsonPath =
|
|
14020
|
-
if (!
|
|
14021
|
-
const account = JSON.parse(
|
|
12162
|
+
const configDir2 = join9(homedir2(), BRAND.configDir);
|
|
12163
|
+
const accountJsonPath = join9(configDir2, "account.json");
|
|
12164
|
+
if (!existsSync21(accountJsonPath)) return null;
|
|
12165
|
+
const account = JSON.parse(readFileSync15(accountJsonPath, "utf-8"));
|
|
14022
12166
|
return account.defaultAgent || null;
|
|
14023
12167
|
} catch {
|
|
14024
12168
|
return null;
|
|
@@ -14091,7 +12235,7 @@ app36.use("/vnc-popout.html", logViewerFetch);
|
|
|
14091
12235
|
app36.get("/vnc-popout.html", (c) => {
|
|
14092
12236
|
let html = htmlCache.get("vnc-popout.html");
|
|
14093
12237
|
if (!html) {
|
|
14094
|
-
html =
|
|
12238
|
+
html = readFileSync15(resolve21(process.cwd(), "public", "vnc-popout.html"), "utf-8");
|
|
14095
12239
|
const name = escapeHtml(BRAND.productName);
|
|
14096
12240
|
html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
|
|
14097
12241
|
html = html.replace("</head>", ` ${brandScript}
|
|
@@ -14181,8 +12325,8 @@ try {
|
|
|
14181
12325
|
(async () => {
|
|
14182
12326
|
try {
|
|
14183
12327
|
let userId = "";
|
|
14184
|
-
if (
|
|
14185
|
-
const users = JSON.parse(
|
|
12328
|
+
if (existsSync21(USERS_FILE)) {
|
|
12329
|
+
const users = JSON.parse(readFileSync15(USERS_FILE, "utf-8").trim() || "[]");
|
|
14186
12330
|
userId = users[0]?.userId ?? "";
|
|
14187
12331
|
}
|
|
14188
12332
|
await backfillNullUserIdConversations(userId);
|
|
@@ -14197,15 +12341,8 @@ try {
|
|
|
14197
12341
|
console.error(`[migration] runBootMigrations rejected: ${err instanceof Error ? err.message : String(err)}`);
|
|
14198
12342
|
}
|
|
14199
12343
|
})();
|
|
14200
|
-
(async () => {
|
|
14201
|
-
try {
|
|
14202
|
-
await startReviewDetector();
|
|
14203
|
-
} catch (err) {
|
|
14204
|
-
console.error(`[review] startReviewDetector rejected: ${err instanceof Error ? err.message : String(err)}`);
|
|
14205
|
-
}
|
|
14206
|
-
})();
|
|
14207
12344
|
startGraphHealthTimer();
|
|
14208
|
-
var configDirForWhatsApp =
|
|
12345
|
+
var configDirForWhatsApp = basename5(MAXY_DIR) || ".maxy";
|
|
14209
12346
|
var bootAccount = resolveAccount();
|
|
14210
12347
|
var bootAccountConfig = bootAccount?.config;
|
|
14211
12348
|
var bootPublicAgent = bootAccount ? resolvePublicAgent(bootAccount.accountDir, { accountId: bootAccount.accountId })?.slug ?? null : null;
|
|
@@ -14242,48 +12379,6 @@ for (const dir of bootEnabled) {
|
|
|
14242
12379
|
bootDelivered.push(dir);
|
|
14243
12380
|
}
|
|
14244
12381
|
console.error(`[plugins] readiness enabled=${bootEnabled.length} delivered=${bootDelivered.length} dist-missing=[${bootDistMissing.join(",")}] missing=[${bootMissing.join(",")}]`);
|
|
14245
|
-
(async () => {
|
|
14246
|
-
if (!bootAccount) {
|
|
14247
|
-
console.log("[action-completion-relay] phase=consumed-skip reason=no-account");
|
|
14248
|
-
return;
|
|
14249
|
-
}
|
|
14250
|
-
let records;
|
|
14251
|
-
try {
|
|
14252
|
-
records = consumeActionCompletionRelays(bootAccount.accountDir);
|
|
14253
|
-
} catch (err) {
|
|
14254
|
-
console.error(`[action-completion-relay] phase=consume-failed error=${err instanceof Error ? err.message : String(err)}`);
|
|
14255
|
-
return;
|
|
14256
|
-
}
|
|
14257
|
-
if (records.length === 0) {
|
|
14258
|
-
console.log(`[action-completion-relay] phase=consumed-empty accountId=${bootAccount.accountId.slice(0, 8)}\u2026`);
|
|
14259
|
-
return;
|
|
14260
|
-
}
|
|
14261
|
-
console.log(`[action-completion-relay] phase=consume-batch count=${records.length} accountId=${bootAccount.accountId.slice(0, 8)}\u2026`);
|
|
14262
|
-
for (const rec of records) {
|
|
14263
|
-
const sessionKey = `cloudflare-relay-boot:${rec.actionId}`;
|
|
14264
|
-
let outcome = "injected";
|
|
14265
|
-
let dispatchError;
|
|
14266
|
-
try {
|
|
14267
|
-
registerSession(sessionKey, "admin", bootAccount.accountId);
|
|
14268
|
-
setConversationIdForSession(sessionKey, rec.conversationId);
|
|
14269
|
-
for await (const _ev of invokeAgent({ type: "admin" }, rec.message, sessionKey)) {
|
|
14270
|
-
}
|
|
14271
|
-
} catch (err) {
|
|
14272
|
-
outcome = "dispatch-failed";
|
|
14273
|
-
dispatchError = err instanceof Error ? err.message : String(err);
|
|
14274
|
-
} finally {
|
|
14275
|
-
unregisterSession(sessionKey);
|
|
14276
|
-
}
|
|
14277
|
-
if (outcome === "injected") {
|
|
14278
|
-
deleteConsumedRelay(rec.filePath);
|
|
14279
|
-
console.log(`[action-completion-relay] phase=consumed actionId=${rec.actionId} conversationId=${rec.conversationId} ageMs=${rec.ageMs} outcome=injected`);
|
|
14280
|
-
} else {
|
|
14281
|
-
console.error(`[action-completion-relay] phase=consumed actionId=${rec.actionId} conversationId=${rec.conversationId} ageMs=${rec.ageMs} outcome=${outcome} reason=${JSON.stringify(dispatchError ?? "")} fileRetained=${JSON.stringify(rec.filePath)}`);
|
|
14282
|
-
}
|
|
14283
|
-
}
|
|
14284
|
-
})().catch((err) => {
|
|
14285
|
-
console.error(`[action-completion-relay] boot-drain rejected: ${err instanceof Error ? err.message : String(err)}`);
|
|
14286
|
-
});
|
|
14287
12382
|
if (bootAccountConfig?.whatsapp) {
|
|
14288
12383
|
console.error(`[whatsapp:boot] loading whatsapp config from account.json publicAgent=${bootPublicAgent ?? "none"}`);
|
|
14289
12384
|
} else {
|
|
@@ -14291,7 +12386,7 @@ if (bootAccountConfig?.whatsapp) {
|
|
|
14291
12386
|
}
|
|
14292
12387
|
init({
|
|
14293
12388
|
configDir: configDirForWhatsApp,
|
|
14294
|
-
platformRoot:
|
|
12389
|
+
platformRoot: resolve21(process.env.MAXY_PLATFORM_ROOT ?? join9(__dirname, "..")),
|
|
14295
12390
|
accountConfig: bootAccountConfig,
|
|
14296
12391
|
onMessage: async (msg) => {
|
|
14297
12392
|
try {
|
|
@@ -14428,11 +12523,6 @@ process.on("SIGTERM", async () => {
|
|
|
14428
12523
|
} catch (err) {
|
|
14429
12524
|
console.error(`[server] shutdown error: ${String(err)}`);
|
|
14430
12525
|
}
|
|
14431
|
-
try {
|
|
14432
|
-
await shutdownReviewDetector();
|
|
14433
|
-
} catch (err) {
|
|
14434
|
-
console.error(`[server] review detector shutdown error: ${String(err)}`);
|
|
14435
|
-
}
|
|
14436
12526
|
console.error("[server] graceful shutdown complete \u2014 exiting");
|
|
14437
12527
|
process.exit(0);
|
|
14438
12528
|
});
|